Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ provider registration defaults to `us` for model discovery, and `/login` prompts
region to authenticate against — which then takes over routing via `modifyModels` above. The OAuth
loopback callback port can be overridden with `HARNESS_OAUTH_PORT` (default `8237`).

While an interactive session is running, `/subagents` shows the bundled agent roster with each
agent's effective model, reasoning level, and purpose. Use `/subagents all` to include project-local
`.pi/agents/*.md` definitions; project settings are applied only for trusted projects.

## Spawn the CLI as a subprocess

```ts
Expand Down
14 changes: 14 additions & 0 deletions packages/harness/src/extensions/subagent/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { fileURLToPath } from "node:url";
import { StringEnum } from "@earendil-works/pi-ai";
import type {
ExtensionAPI,
ExtensionCommandContext,
ExtensionContext,
ExtensionFactory,
Theme,
Expand All @@ -28,7 +29,9 @@ import {
} from "./format";
import { runPool } from "./process/pool";
import { renderSubagentCall, renderSubagentResult } from "./render";
import { formatSubagentRoster } from "./roster";
import { isFailedResult, runAgent, type SingleRunResult } from "./run-agent";
import { loadSubagentSettings } from "./settings";
import { SubagentStatusEditor } from "./status-editor";
import { renderSubagentFooterLines } from "./status-footer";
import { showSubagentStatusOverlay } from "./status-overlay";
Expand Down Expand Up @@ -179,6 +182,17 @@ export function createSubagentExtension(
skillPaths: [fileURLToPath(new URL("./skills", import.meta.url))],
}));

pi.registerCommand("subagents", {
description:
"Show the configured subagent roster. Usage: /subagents [all]",
handler: async (args: string, ctx: ExtensionCommandContext) => {
const scope = args.trim().toLowerCase() === "all" ? "both" : "bundled";
const discovery = discoverAgents(ctx.cwd, scope);
const settings = loadSubagentSettings(ctx.cwd, ctx.isProjectTrusted());
ctx.ui.notify(formatSubagentRoster(discovery.agents, settings), "info");
},
});

pi.registerTool(
defineTool({
name: "subagent",
Expand Down
42 changes: 42 additions & 0 deletions packages/harness/src/extensions/subagent/roster.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import type { AgentConfig } from "./agents";
import { formatSubagentRoster } from "./roster";

const agent: AgentConfig = {
name: "code-reviewer",
description: "Review diffs for correctness and quality",
model: "gpt-5.6-sol",
systemPrompt: "",
source: "bundled",
};

describe("formatSubagentRoster", () => {
it("renders the effective model and reasoning settings", () => {
const roster = formatSubagentRoster([agent], {
agentOverrides: { "code-reviewer": { thinking: "low" } },
});

expect(roster).toContain("Subagent");
expect(roster).toContain("code-reviewer");
expect(roster).toContain("gpt-5.6-sol");
expect(roster).toContain("low");
expect(roster).toContain("Review diffs for correctness and quality");
});

it("marks project agents and truncates long purposes", () => {
const roster = formatSubagentRoster(
[
{
...agent,
source: "project",
description: "a".repeat(100),
},
],
{},
);

expect(roster).toContain("code-reviewer (project)");
expect(roster).toContain("a".repeat(71));
expect(roster).toContain("…");
});
});
51 changes: 51 additions & 0 deletions packages/harness/src/extensions/subagent/roster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { AgentConfig } from "./agents";
import { applyAgentOverrides, type SubagentSettings } from "./settings";

const COLUMNS = [
{ title: "Subagent", width: 24 },
{ title: "Model", width: 24 },
{ title: "Reasoning", width: 10 },
{ title: "Purpose", width: 72 },
] as const;

function truncate(value: string, width: number): string {
if (value.length <= width) return value;
return `${value.slice(0, Math.max(0, width - 1))}…`;
}

function cell(value: string, width: number): string {
return truncate(value, width).padEnd(width, " ");
}

function row(values: string[]): string {
return values
.map((value, index) => cell(value, COLUMNS[index].width))
.join(" ");
}

export function formatSubagentRoster(
agents: AgentConfig[],
settings: SubagentSettings,
): string {
if (agents.length === 0) return "No subagents available.";

const separatorWidth =
COLUMNS.reduce((total, column) => total + column.width, 0) +
(COLUMNS.length - 1) * 2;
const lines = [
row(COLUMNS.map((column) => column.title)),
"-".repeat(separatorWidth),
];
for (const agent of agents) {
const effective = applyAgentOverrides(agent, settings);
lines.push(
row([
agent.source === "project" ? `${agent.name} (project)` : agent.name,
effective.model ?? "inherit",
effective.thinking ?? "default",
effective.description,
]),
);
}
return lines.join("\n");
}
Loading