Skip to content

Commit bf385d1

Browse files
Merge pull request #868 from corbitsdev/cl-7619-return-search_agents-as-id-and-description-by-default
Return compact search_agents results by default
2 parents 6682910 + f3fb374 commit bf385d1

7 files changed

Lines changed: 138 additions & 57 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
1313

1414
## [Unreleased]
1515

16+
### Changed
17+
18+
- `search_agents` default results are id, description, and spawn metadata.
19+
Pass `include_body=true` to include the loaded system prompt / body
20+
(still truncated).
21+
1622
### Fixed
1723

1824
- Dry-fleet transcript and `/status` report the outcome tally only
@@ -24,6 +30,7 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
2430
`buzzing`, `grinding`, `thinking`, `doing`, `cooking`, `creating`,
2531
`imagining`, `inventing`) instead of going blank.
2632

33+
2734
## [0.3.19] - 2026-09-10
2835

2936
### Security

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ Workers ask the spawning parent with **`ask_director`** (not the human). That pa
216216

217217
When the parent TUI is not blocked in `wait_agents`, the runner publishes an authoritative snapshot of currently pending top-level questions on each store notification, including empty snapshots before fleet-count updates. During synchronous session rotation, a runner-owned barrier suppresses both publications before delivery-generation invalidation, transcript clearing, and worker cancellation; successful reset reconciles a fresh snapshot before resuming asynchronous backend rebuild. The bridge drops resolved, cancelled, replaced, terminal, and removed asks and delivers each session/question identity once while pending. A coalesced wake starts only when the parent is not processing and every operator gate is closed, including parent-idle fleet holds where the shell stays busy. Worker gates do not manufacture parent processing. Replies use `send_input`'s `target` field with the worker session ID, never its shared catalog ID. Synthetic wakes use `SessionPort.deliver` through queued-delivery's idle-send path without entering the user follow-up queue or composer `/feedback` capture.
218218

219-
When profiles exist (local `.agents/agents/` and/or enabled **`kind: "agent"`** plugins, including **data-only** markdown plugins with no `index.ts`), the chat model also receives **`search_agents`** — a lexical index over profile id, description, and role text so the model can discover ids before calling `spawn_agent(agent=...)`. Results include each match's full loaded system prompt / body so the parent can inspect plugin or Claude marketplace agents without `read_file` on paths outside the session cwd (path-escape blocks those roots by design; writes remain blocked). `spawn_agent` and `search_agents` are core tools on the primary session.
219+
When profiles exist (local `.agents/agents/` and/or enabled **`kind: "agent"`** plugins, including **data-only** markdown plugins with no `index.ts`), the chat model also receives **`search_agents`** — a lexical index over profile id, description, and role text so the model can discover ids before calling `spawn_agent(agent=...)`. Default results are id, description, and spawn metadata (orchestrator flag, source). Pass `include_body=true` to include each match's loaded system prompt / body (truncated) so the parent can inspect plugin or Claude marketplace agents without `read_file` on paths outside the session cwd (path-escape blocks those roots by design; writes remain blocked). `spawn_agent` and `search_agents` are core tools on the primary session.
220220

221221
Built-in directors with `spawn.maySpawn` may themselves call `spawn_agent` (one hop only): nested dispatch installs the mailbox-scoped fleet verbs (`spawn_agent`, `wait_agents`, `list_agents`, …) with `allowOrchestrator: false` so the tree bottoms out. Profile-sourced `orchestrator: true` is rejected before a session starts because it has no trusted tier/authority semantics today. Fleet discovery (`search_agents`) stays Tier 1 only. Unknown `agent` ids fail closed.
222222

docs/PLUGINS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,9 @@ Relative install paths and paths outside that root are ignored so a poisoned
170170
registry cannot load project trees as origin `user`. Profile `source: "claude"`.
171171
Discovered modules still require `settings.plugins[id].enabled` before agents or
172172
tools wire into the session. `search_agents` labels those profiles with
173-
`[source: claude]` and injects each profile's full loaded system prompt / body so
174-
the parent never needs `read_file` on `~/.claude/plugins/...` (path-escape still
173+
`[source: claude]`. Default results are id, description, and spawn metadata;
174+
pass `include_body=true` to include the loaded system prompt / body so the
175+
parent never needs `read_file` on `~/.claude/plugins/...` (path-escape still
175176
blocks those roots for path tools; writes/deletes outside cwd stay denied). JS
176177
Claude plugins (if any) stay on explicit `pluginPaths`.
177178

src/agent/agent-search.test.ts

Lines changed: 102 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -45,60 +45,92 @@ describe("createAgentIndex", () => {
4545

4646
describe("formatAgentSearchResults", () => {
4747
test("includes spawn hint and ids", () => {
48-
const text = formatAgentSearchResults([defined(fixtures[1])]);
48+
const text = formatAgentSearchResults([defined(fixtures[1])], false);
4949
expect(text).toContain("critique");
5050
expect(text).toContain("spawn_agent(agent=");
5151
});
5252

5353
test("includes source label when present", () => {
54-
const text = formatAgentSearchResults([
55-
{
56-
id: "marketplace-scout",
57-
description: "From Claude install",
58-
source: "claude",
59-
},
60-
]);
54+
const text = formatAgentSearchResults(
55+
[
56+
{
57+
id: "marketplace-scout",
58+
description: "From Claude install",
59+
source: "claude",
60+
},
61+
],
62+
false,
63+
);
6164
expect(text).toContain("[source: claude]");
6265
expect(text).toContain("marketplace-scout");
6366
});
6467

65-
test("injects full system prompt body so parent need not read_file plugin roots", () => {
68+
test("default omits system prompt body", () => {
69+
const uniqueBody =
70+
"UNIQUE_BODY_MARKER_draper_review_pull_requests_for_design_clarity_xyz";
71+
const text = formatAgentSearchResults(
72+
[
73+
{
74+
id: "draper",
75+
description: "PR design reviewer from marketplace",
76+
source: "claude",
77+
orchestrator: true,
78+
systemPromptRole: uniqueBody,
79+
},
80+
],
81+
false,
82+
);
83+
expect(text).toContain("### draper");
84+
expect(text).toContain("[source: claude]");
85+
expect(text).toContain("[orchestrator]");
86+
expect(text).toContain("PR design reviewer from marketplace");
87+
expect(text).not.toContain(uniqueBody);
88+
expect(text).not.toContain("System prompt / body:");
89+
});
90+
91+
test("include_body injects truncated system prompt body", () => {
6692
const body =
6793
"You are draper. Review pull requests for design clarity and maintainability.\n" +
6894
"Prefer concrete file/line citations.";
69-
const text = formatAgentSearchResults([
70-
{
71-
id: "draper",
72-
description: "PR design reviewer from marketplace",
73-
source: "claude",
74-
systemPromptRole: body,
75-
},
76-
]);
95+
const text = formatAgentSearchResults(
96+
[
97+
{
98+
id: "draper",
99+
description: "PR design reviewer from marketplace",
100+
source: "claude",
101+
systemPromptRole: body,
102+
},
103+
],
104+
true,
105+
);
77106
expect(text).toContain("### draper");
78107
expect(text).toContain("[source: claude]");
79108
expect(text).toContain("System prompt / body:");
80109
expect(text).toContain(body);
81-
expect(text).toContain("do not need read_file on plugin roots");
82110
});
83111

84112
test("omits body section when systemPromptRole is absent", () => {
85-
const text = formatAgentSearchResults([
86-
{ id: "no-body", description: "Metadata only" },
87-
]);
113+
const text = formatAgentSearchResults(
114+
[{ id: "no-body", description: "Metadata only" }],
115+
true,
116+
);
88117
expect(text).toContain("### no-body");
89118
expect(text).toContain("Metadata only");
90119
expect(text).not.toContain("System prompt / body:");
91120
});
92121

93122
test("truncates oversized systemPromptRole bodies with ellipsis marker", () => {
94123
const body = "x".repeat(MAX_AGENT_SEARCH_BODY_CHARS + 500);
95-
const text = formatAgentSearchResults([
96-
{
97-
id: "huge",
98-
description: "Oversized marketplace body",
99-
systemPromptRole: body,
100-
},
101-
]);
124+
const text = formatAgentSearchResults(
125+
[
126+
{
127+
id: "huge",
128+
description: "Oversized marketplace body",
129+
systemPromptRole: body,
130+
},
131+
],
132+
true,
133+
);
102134
expect(text).toContain("System prompt / body:");
103135
expect(text).toContain("…[truncated]");
104136
expect(text).not.toContain(body);
@@ -113,14 +145,17 @@ describe("formatAgentSearchResults", () => {
113145
test("redacts secret-shaped content in profile body at format layer", () => {
114146
// search_agents is not on the posix middleware path; scrub must happen here.
115147
const secret = "sk-live-abc123xyz789012345678";
116-
const text = formatAgentSearchResults([
117-
{
118-
id: "leaky",
119-
description: "Profile with credential-shaped body text",
120-
source: "claude",
121-
systemPromptRole: `Use API_KEY=${secret} when calling the provider.`,
122-
},
123-
]);
148+
const text = formatAgentSearchResults(
149+
[
150+
{
151+
id: "leaky",
152+
description: "Profile with credential-shaped body text",
153+
source: "claude",
154+
systemPromptRole: `Use API_KEY=${secret} when calling the provider.`,
155+
},
156+
],
157+
true,
158+
);
124159
expect(text).toContain("### leaky");
125160
expect(text).toContain("System prompt / body:");
126161
expect(text).toContain(CREDENTIAL_REDACTION);
@@ -130,7 +165,35 @@ describe("formatAgentSearchResults", () => {
130165
});
131166

132167
describe("createSearchAgentsTool", () => {
133-
test("handler surfaces loaded systemPromptRole for plugin-style profiles", async () => {
168+
test("handler default omits systemPromptRole body", async () => {
169+
const uniqueBody =
170+
"UNIQUE_BODY_MARKER_emil_product_sense_and_user_impact_xyz";
171+
const tool = createSearchAgentsTool(() => [
172+
{
173+
id: "emil",
174+
description: "Product-minded reviewer",
175+
source: "claude",
176+
systemPromptRole: uniqueBody,
177+
},
178+
{
179+
id: "greybeard",
180+
description: "Architect",
181+
systemPromptRole: "You are greybeard.",
182+
},
183+
]);
184+
if (tool.kind !== "string") throw new Error("expected string tool");
185+
const text = await tool.handler(
186+
{ query: "emil product" },
187+
new AbortController().signal,
188+
);
189+
expect(text).toContain("emil");
190+
expect(text).toContain("Product-minded reviewer");
191+
expect(text).toContain("[source: claude]");
192+
expect(text).not.toContain(uniqueBody);
193+
expect(text).not.toContain("System prompt / body:");
194+
});
195+
196+
test("handler include_body true surfaces loaded systemPromptRole", async () => {
134197
const body = "You are emil. Focus on product sense and user impact.";
135198
const tool = createSearchAgentsTool(() => [
136199
{
@@ -147,7 +210,7 @@ describe("createSearchAgentsTool", () => {
147210
]);
148211
if (tool.kind !== "string") throw new Error("expected string tool");
149212
const text = await tool.handler(
150-
{ query: "emil product" },
213+
{ query: "emil product", include_body: true },
151214
new AbortController().signal,
152215
);
153216
expect(text).toContain("emil");
@@ -199,7 +262,7 @@ describe("createSearchAgentsTool", () => {
199262
]);
200263
if (tool.kind !== "string") throw new Error("expected string tool");
201264
const text = await tool.handler(
202-
{ query: "leaky" },
265+
{ query: "leaky", include_body: true },
203266
new AbortController().signal,
204267
);
205268
expect(text).toContain("### leaky");

src/agent/agent-search.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,38 +72,42 @@ function truncateAgentBody(body: string): string {
7272
return `${body.slice(0, MAX_AGENT_SEARCH_BODY_CHARS)}\n…[truncated]`;
7373
}
7474

75-
// Format one profile for search_agents output. Injects the full loaded
76-
// systemPromptRole (markdown body / role text) so the parent can inspect plugin
77-
// and marketplace agents without read_file on paths outside the session cwd
78-
// (path-escape blocks those roots by design). Bodies longer than
75+
// Format one profile for search_agents output. Default is id, description, and
76+
// spawn metadata (orchestrator flag, source). The loaded systemPromptRole is
77+
// omitted unless includeBody is true. Bodies longer than
7978
// MAX_AGENT_SEARCH_BODY_CHARS are truncated with an ellipsis marker.
80-
function formatAgentProfileEntry(p: AgentProfile): string {
79+
function formatAgentProfileEntry(
80+
p: AgentProfile,
81+
includeBody: boolean,
82+
): string {
8183
const desc = (p.description ?? "").trim();
8284
const orch = p.orchestrator === true ? " [orchestrator]" : "";
8385
const source = p.source !== undefined ? ` [source: ${p.source}]` : "";
8486
const header =
8587
desc.length > 0
8688
? `### ${p.id}${orch}${source}\n${desc}`
8789
: `### ${p.id}${orch}${source}`;
90+
if (!includeBody) return header;
8891
const body = (p.systemPromptRole ?? "").trim();
8992
if (body.length === 0) return header;
9093
return `${header}\n\nSystem prompt / body:\n${truncateAgentBody(body)}`;
9194
}
9295

9396
export function formatAgentSearchResults(
9497
profiles: readonly AgentProfile[],
98+
includeBody: boolean,
9599
): string {
96100
if (profiles.length === 0) {
97101
return "No agent profiles matched. Try broader terms (e.g. review, explore, implement) or list_dir on .agents/agents/.";
98102
}
99-
const entries = profiles.map(formatAgentProfileEntry);
103+
const entries = profiles.map((p) => formatAgentProfileEntry(p, includeBody));
100104
// Live scrub for search_agents: this tool is not on the posix middleware path, so
101105
// SCRUBBABLE_TOOLS in tool-result-secret-scrub-plugin cannot reach it. Scrub here
102106
// before the formatted string becomes a tool result (marketplace/plugin bodies may
103107
// contain secret-shaped substrings).
104108
return scrubSecretShapedContent(
105109
[
106-
"Matching agent profiles (pass id to spawn_agent(agent=...)). Full system prompt / body is included so you do not need read_file on plugin roots outside the workspace:",
110+
"Matching agent profiles (pass id to spawn_agent(agent=...)):",
107111
"",
108112
...entries.flatMap((entry, i) => (i === 0 ? [entry] : ["", entry])),
109113
"",
@@ -115,7 +119,7 @@ export function formatAgentSearchResults(
115119
export const searchAgentsDefinition: ToolDefinition = {
116120
name: "search_agents",
117121
description:
118-
"Find spawnable agent profiles by capability, role, or team name (e.g. 'review', 'review team', 'architect', 'security'). Returns profile ids, descriptions, and the full loaded system prompt / body for each match so you can inspect plugin or Claude marketplace agents without reading files outside the workspace. Use the id in spawn_agent(agent=...). Call this when the user asks to spin up specialists or a team without naming exact ids.",
122+
"Find spawnable agent profiles by capability, role, or team name (e.g. 'review', 'review team', 'architect', 'security'). Returns profile ids, descriptions, and spawn metadata (orchestrator flag, source). Pass include_body=true to include the loaded system prompt / body for each match (truncated). Use the id in spawn_agent(agent=...). Call this when the user asks to spin up specialists or a team without naming exact ids.",
119123
inputSchema: {
120124
type: "object",
121125
properties: {
@@ -124,12 +128,17 @@ export const searchAgentsDefinition: ToolDefinition = {
124128
description:
125129
"What kind of agent or team you need — keywords from the user's request (e.g. 'review team', 'code quality', 'explore codebase').",
126130
},
131+
include_body: {
132+
type: "boolean",
133+
description:
134+
"When true, include each match's loaded system prompt / body (truncated). Default false: id, description, and spawn metadata only.",
135+
},
127136
},
128137
required: ["query"],
129138
},
130139
};
131140

132-
const SearchAgentsArgs = type({ query: "string" });
141+
const SearchAgentsArgs = type({ query: "string", "include_body?": "boolean" });
133142

134143
export function createSearchAgentsTool(
135144
getProfiles: () => readonly AgentProfile[],
@@ -140,15 +149,16 @@ export function createSearchAgentsTool(
140149
handler: async (rawArgs: Record<string, unknown>): Promise<string> => {
141150
const parsed = SearchAgentsArgs(rawArgs);
142151
if (parsed instanceof type.errors) {
143-
return "Error: search_agents requires query (string).";
152+
return "Error: search_agents requires query (string); include_body is optional boolean.";
144153
}
145154
const query = parsed.query.trim();
155+
const includeBody = parsed.include_body === true;
146156
// Empty and non-empty queries share createAgentIndex.search's default limit
147157
// (12) so a large marketplace catalog cannot dump every body into one result.
148158
if (query.length === 0 && getProfiles().length === 0) {
149159
return "No agent profiles are loaded.";
150160
}
151-
return formatAgentSearchResults(index.search(query));
161+
return formatAgentSearchResults(index.search(query), includeBody);
152162
},
153163
});
154164
}

src/agent/prompts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export function buildHarnessFacts(
9393
...(dynamicTools
9494
? [
9595
"- Core tools plus the advertised catalog (including skill_search) are resident. Use tool_search to load extra capabilities from plugins or integrations when needed.",
96-
"- Use search_agents before dispatching named specialists or teams (results include full profile bodies; do not read_file plugin paths outside the workspace).",
96+
"- Use search_agents before dispatching named specialists or teams (ids and descriptions by default; include_body=true for the loaded system prompt). Do not read_file plugin paths outside the workspace.",
9797
"- The user may send follow-up messages while workers run; they are queued. Enter delivers at the next parent tool.boundary; Alt+Enter on session-idle. A long parent tool holds that boundary. Update your plan, spawn or adjust workers, and keep the operator informed.",
9898
]
9999
: ["- The tools below are your full toolset."]),
@@ -245,7 +245,7 @@ const TOOL_SUMMARIES: Record<string, string> = {
245245
wait_agents:
246246
"wait for spawned workers by agent_id; returns awaiting_director when a worker asks, without collecting that session",
247247
search_agents:
248-
"find agent profiles by role or team before spawning with spawn_agent(agent=...); results include full system prompt / body so you need not read_file plugin roots outside the workspace",
248+
"find agent profiles by role or team before spawning with spawn_agent(agent=...); default results are id, description, and spawn metadata — pass include_body=true for the loaded system prompt / body",
249249
manage_tasks:
250250
"maintain your work checklist — create/replace, update status, append, cancel",
251251
ask_director:

src/subagent/agent-fleet.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -768,8 +768,8 @@ function resolveAgentDispatch(input: {
768768
const known = profiles.map((p) => p.id).sort();
769769
const hint =
770770
known.length > 0
771-
? ` Known profiles: ${known.join(", ")}. Call search_agents to discover more (results include full system prompt / body; do not read_file plugin paths outside the workspace).`
772-
: " No profiles are currently loaded. Call search_agents to discover available agents (results include full system prompt / body).";
771+
? ` Known profiles: ${known.join(", ")}. Call search_agents to discover more.`
772+
: " No profiles are currently loaded. Call search_agents to discover available agents.";
773773
return { error: `Error: unknown agent profile "${agentId}".${hint}` };
774774
}
775775
let effortPin: ReasoningEffort | undefined;

0 commit comments

Comments
 (0)