pi-maestro-teammate 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -27
- package/agents/delegate.md +4 -2
- package/agents/explorer.md +19 -0
- package/agents/goal-verifier.md +55 -0
- package/agents/workflow.md +29 -0
- package/package.json +8 -2
- package/src/agents/agents.ts +128 -21
- package/src/extension/index.ts +1655 -975
- package/src/extension/schemas.ts +120 -80
- package/src/models/model-catalog.ts +42 -4
- package/src/models/model-routing.ts +63 -5
- package/src/runs/child-extensions.ts +101 -0
- package/src/runs/execution.ts +741 -319
- package/src/shared/thinking.ts +21 -0
- package/src/shared/types.ts +9 -1
- package/src/tui/attach-overlay.ts +207 -80
- package/src/tui/input-text.ts +122 -0
- package/src/tui/model-mapping-overlay.ts +704 -74
- package/src/tui/progress-tree.ts +28 -2
- package/src/tui/render.ts +12 -6
- package/agents/coordinator.md +0 -26
package/README.md
CHANGED
|
@@ -44,11 +44,12 @@ Precedence is task-level `model` → top-level `model` → explicit `taskType` m
|
|
|
44
44
|
|
|
45
45
|
```
|
|
46
46
|
{ tasks: [
|
|
47
|
-
{ agent: "
|
|
48
|
-
{ agent: "
|
|
49
|
-
{ agent: "
|
|
47
|
+
{ agent: "explorer", name: "api", task: "Find all API endpoints" },
|
|
48
|
+
{ agent: "explorer", name: "db", task: "Map database schemas" },
|
|
49
|
+
{ agent: "explorer", name: "deps", task: "List external dependencies" }
|
|
50
50
|
],
|
|
51
|
-
concurrency: 3
|
|
51
|
+
concurrency: 3,
|
|
52
|
+
background: false
|
|
52
53
|
}
|
|
53
54
|
```
|
|
54
55
|
|
|
@@ -56,9 +57,11 @@ Precedence is task-level `model` → top-level `model` → explicit `taskType` m
|
|
|
56
57
|
|
|
57
58
|
```
|
|
58
59
|
{ tasks: [
|
|
59
|
-
{ agent: "
|
|
60
|
-
{ agent: "delegate", task: "Based on this context: {recon}\n\nRefactor the auth module" }
|
|
61
|
-
]
|
|
60
|
+
{ agent: "explorer", name: "recon", task: "Find the auth module structure" },
|
|
61
|
+
{ agent: "delegate", name: "implement", task: "Based on this context: {recon}\n\nRefactor the auth module" }
|
|
62
|
+
],
|
|
63
|
+
concurrency: 1,
|
|
64
|
+
background: false
|
|
62
65
|
}
|
|
63
66
|
```
|
|
64
67
|
|
|
@@ -66,24 +69,31 @@ Precedence is task-level `model` → top-level `model` → explicit `taskType` m
|
|
|
66
69
|
|
|
67
70
|
```
|
|
68
71
|
{ tasks: [
|
|
69
|
-
{ agent: "
|
|
72
|
+
{ agent: "explorer", name: "api", task: "List all API routes",
|
|
70
73
|
outputSchema: {
|
|
71
74
|
type: "object",
|
|
72
75
|
properties: { routes: { type: "array", items: { type: "string" } } },
|
|
73
76
|
required: ["routes"]
|
|
74
77
|
} },
|
|
75
|
-
{ agent: "
|
|
76
|
-
{ agent: "
|
|
77
|
-
]
|
|
78
|
+
{ agent: "explorer", name: "db", task: "Map the database schema" },
|
|
79
|
+
{ agent: "delegate", name: "verify", task: "Routes: {api.routes}\nDB: {db}\n\nCheck consistency" }
|
|
80
|
+
],
|
|
81
|
+
concurrency: 2,
|
|
82
|
+
background: false
|
|
78
83
|
}
|
|
79
84
|
```
|
|
80
85
|
|
|
81
|
-
`api` and `db` run in parallel. `
|
|
86
|
+
`api` and `db` run in parallel. `delegate` waits for both, with `{api.routes}` resolved from structured output and `{db}` from text output.
|
|
87
|
+
|
|
88
|
+
For production nested dispatch, give every task a unique `name`, set an explicit
|
|
89
|
+
provider-safe `concurrency`, and use `background: false` whenever the parent must
|
|
90
|
+
consume all child results before it continues. Background runs return immediately
|
|
91
|
+
and report completion to the root session.
|
|
82
92
|
|
|
83
93
|
### Structured Output
|
|
84
94
|
|
|
85
95
|
```
|
|
86
|
-
{ agent: "
|
|
96
|
+
{ agent: "explorer", task: "List all API routes",
|
|
87
97
|
outputSchema: {
|
|
88
98
|
type: "object",
|
|
89
99
|
properties: { routes: { type: "array", items: { type: "string" } } },
|
|
@@ -164,12 +174,12 @@ dispatch → running → turn complete → sleeping → teammate-send → runnin
|
|
|
164
174
|
|
|
165
175
|
Time spent sleeping is excluded from the displayed duration. `sleepMs` accumulates total sleep time; displayed uptime = wall clock − sleep time.
|
|
166
176
|
|
|
167
|
-
### Agent
|
|
177
|
+
### Agent Resolution
|
|
168
178
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
179
|
+
Agent names must resolve to one of the three reserved builtin roles (`delegate`,
|
|
180
|
+
`explorer`, `workflow`) or to a discovered project/user role. Unknown names return
|
|
181
|
+
an error with the available role catalog instead of silently using a generic
|
|
182
|
+
configuration. The legacy name `coordinator` resolves to `workflow`.
|
|
173
183
|
|
|
174
184
|
## TaskSpec Schema
|
|
175
185
|
|
|
@@ -221,14 +231,14 @@ The `chain` field is preserved for backward compatibility. It normalizes interna
|
|
|
221
231
|
```
|
|
222
232
|
// This chain:
|
|
223
233
|
{ chain: [
|
|
224
|
-
{ agent: "
|
|
234
|
+
{ agent: "explorer", task: "Find auth code" },
|
|
225
235
|
{ agent: "delegate", task: "Fix: {previous}" }
|
|
226
236
|
]
|
|
227
237
|
}
|
|
228
238
|
|
|
229
239
|
// Is equivalent to:
|
|
230
240
|
{ tasks: [
|
|
231
|
-
{ agent: "
|
|
241
|
+
{ agent: "explorer", name: "_step0", task: "Find auth code" },
|
|
232
242
|
{ agent: "delegate", name: "_step1", task: "Fix: {_step0}" }
|
|
233
243
|
]
|
|
234
244
|
}
|
|
@@ -241,18 +251,19 @@ All agents are managed by the root process in a single flat `activeRuns` pool, r
|
|
|
241
251
|
### How It Works
|
|
242
252
|
|
|
243
253
|
```
|
|
244
|
-
|
|
254
|
+
workflow calls teammate({ agent: "explorer", name: "recon" })
|
|
245
255
|
│ IPC: teammate_proxy_request (process.send)
|
|
246
256
|
▼
|
|
247
|
-
Root spawns
|
|
257
|
+
Root spawns explorer → registers in root's activeRuns/namedAgents
|
|
248
258
|
│ IPC: teammate_proxy_result (child.send)
|
|
249
259
|
▼
|
|
250
|
-
|
|
260
|
+
workflow receives result
|
|
251
261
|
```
|
|
252
262
|
|
|
253
263
|
All agents are flat peers:
|
|
254
264
|
- `teammate-send({ to: "name" })` = one lookup in `namedAgents` → stdin. Direct delivery.
|
|
255
|
-
- `teammate-list` = iterate
|
|
265
|
+
- `teammate-list({ view: "active" | "named" | "all" })` = iterate running instances.
|
|
266
|
+
- `teammate-list({ view: "roles" })` = list all available builtin, project, and user-defined roles with descriptions.
|
|
256
267
|
- `teammate-watch` = read agent's `outputLog`. Direct.
|
|
257
268
|
|
|
258
269
|
### Child Proxy Tools
|
|
@@ -263,12 +274,26 @@ Every child process automatically gets proxy versions of all 4 teammate tools (i
|
|
|
263
274
|
|
|
264
275
|
The root's IPC message listener (`child.on("message")`) intercepts these requests and executes them locally.
|
|
265
276
|
|
|
277
|
+
### Main-Session Interaction Relay
|
|
278
|
+
|
|
279
|
+
Parent extensions can register themselves for inheritance by teammate children. `pi-maestro-flow` uses this bridge automatically, so every child loads its permission hooks and receives the `ask-user-question` tool even when the role has an explicit `tools` whitelist.
|
|
280
|
+
|
|
281
|
+
When a child needs user input, it sends a reply-capable `teammate_interaction_request` to the root session:
|
|
282
|
+
|
|
283
|
+
- Permission requests are displayed in the main UI with `Allow once`, `Always allow`, and `Deny`. The selected action is returned to the blocked child tool call.
|
|
284
|
+
- `ask-user-question` requests are displayed in the main UI and return structured option/free-text answers to the child.
|
|
285
|
+
- Pending requests are tracked by `requestId` on the active agent and serialized through the root interaction queue, preventing overlapping prompts.
|
|
286
|
+
- Headless root sessions still run the parent permission broker: silent allow/deny decisions work, while permissions that require a dialog fail closed and questionnaires are cancelled.
|
|
287
|
+
|
|
288
|
+
The response uses `teammate_interaction_response` with the original `requestId`, so late or duplicate responses cannot resolve a different child request.
|
|
289
|
+
|
|
266
290
|
## Reliability
|
|
267
291
|
|
|
268
292
|
- **Model fallback chain** — primary model → `fallbackModels[]` from agent config → automatic retry
|
|
269
293
|
- **Flat agent pool** — all agents managed by root process; child proxy tools forward spawn requests to root; depth guard (`PI_TEAMMATE_DEPTH`) prevents runaway recursion
|
|
270
294
|
- **Resident lifecycle** — agents sleep after turn completion; process stays alive for follow-up; only killed on explicit abort or session shutdown
|
|
271
295
|
- **IPC disconnect guard** — child proxy resolves all pending requests with error on disconnect (root crash / agent abort)
|
|
296
|
+
- **Interaction fail-closed** — missing main UI, timeout, or relay failure never grants a child permission request
|
|
272
297
|
- **Windows-safe pi resolution** — `getPiSpawnCommand()` resolves the pi binary via env override, Windows script detection, or PATH
|
|
273
298
|
- **Abort signal** — SIGTERM → 5s grace → SIGKILL
|
|
274
299
|
|
|
@@ -331,11 +356,23 @@ pi install ./pi-teammate
|
|
|
331
356
|
MIT
|
|
332
357
|
# Agent discovery
|
|
333
358
|
|
|
334
|
-
|
|
359
|
+
The package always provides exactly three reserved builtin roles:
|
|
360
|
+
|
|
361
|
+
- `delegate`: general-purpose execution, including fixed prompt templates
|
|
362
|
+
- `explorer`: read-only code discovery and call-chain tracing
|
|
363
|
+
- `workflow`: dependency-aware DAG decomposition and teammate delegation
|
|
364
|
+
|
|
365
|
+
Other Agent Markdown files are discovered with the following precedence:
|
|
335
366
|
|
|
336
367
|
1. nearest project `.pi/agents/*.md`
|
|
337
|
-
2.
|
|
338
|
-
3.
|
|
368
|
+
2. nearest project `.agents/*.md`
|
|
369
|
+
3. `~/.agents/*.md`
|
|
370
|
+
4. legacy `~/.pi/agent/extensions/teammate/agents/*.md`
|
|
371
|
+
|
|
372
|
+
Project and user files cannot override the three reserved builtin names. Their
|
|
373
|
+
`name` and `description` fields are refreshed into the active system prompt on
|
|
374
|
+
every `before_agent_start`; the Markdown body is loaded only after the role is
|
|
375
|
+
selected. Files without both fields are ignored. Unknown role names are rejected.
|
|
339
376
|
|
|
340
377
|
Pi has no native `pi.agents` package manifest field. Builtin teammate agents are
|
|
341
378
|
resolved relative to the installed extension module, so npm, git, global and local
|
package/agents/delegate.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: delegate
|
|
3
|
-
description:
|
|
3
|
+
description: General-purpose teammate for direct tasks or reusable prompt templates
|
|
4
4
|
systemPromptMode: append
|
|
5
5
|
inheritProjectContext: true
|
|
6
6
|
tools: read, grep, find, ls, bash, edit, write
|
|
7
7
|
inheritSkills: false
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
You are
|
|
10
|
+
You are the general-purpose teammate agent. Execute the assigned task or resolved prompt template using the provided tools. Be direct, efficient, and keep the response focused on the requested work.
|
|
11
|
+
|
|
12
|
+
If the task specifies MODE: analysis, do not modify files. If it specifies MODE: write, implement the requested changes and verify them.
|
|
11
13
|
|
|
12
14
|
Guidelines:
|
|
13
15
|
- Read existing code before making changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explorer
|
|
3
|
+
description: Read-only codebase discovery and call-chain tracing specialist
|
|
4
|
+
systemPromptMode: replace
|
|
5
|
+
thinking: low
|
|
6
|
+
tools: read, grep, find, ls
|
|
7
|
+
inheritProjectContext: false
|
|
8
|
+
inheritSkills: false
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are a fast, read-only codebase exploration agent. Find concrete files, definitions, call sites, and data-flow relationships without modifying the workspace.
|
|
12
|
+
|
|
13
|
+
Your approach:
|
|
14
|
+
1. Parse the request into target, scope, and acceptance conditions
|
|
15
|
+
2. Search within the stated scope before widening it
|
|
16
|
+
3. Read the most relevant matches to verify them
|
|
17
|
+
4. Return concise findings with file and line anchors
|
|
18
|
+
|
|
19
|
+
Report ambiguity and negative evidence explicitly. Do not edit or create files.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: goal-verifier
|
|
3
|
+
description: Independent read-only verifier for Goal completion claims
|
|
4
|
+
systemPromptMode: replace
|
|
5
|
+
inheritProjectContext: false
|
|
6
|
+
tools: read, grep, find, ls, bash
|
|
7
|
+
inheritSkills: false
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<role>
|
|
11
|
+
You are the independent, read-only verifier spawned automatically after a normal Goal agent loop ends.
|
|
12
|
+
Your only job is to decide whether the supplied completion claim satisfies every explicit requirement of the original Goal.
|
|
13
|
+
|
|
14
|
+
You do not own Goal lifecycle transitions. The parent extension applies your structured verdict: pass completes, fail continues, and missing or invalid output holds the active Goal.
|
|
15
|
+
|
|
16
|
+
Core responsibilities:
|
|
17
|
+
- Evaluate the supplied session and canonical Workflow evidence before doing any spot check.
|
|
18
|
+
- Perform only the smallest necessary read-only checks when decisive evidence is missing or stale.
|
|
19
|
+
- Return a grounded pass or fail verdict through `structured_output`.
|
|
20
|
+
</role>
|
|
21
|
+
|
|
22
|
+
<verdict_policy>
|
|
23
|
+
Treat missing evidence as a valid failure verdict, never as a reason to omit the result.
|
|
24
|
+
|
|
25
|
+
| Condition | Verdict |
|
|
26
|
+
|-----------|---------|
|
|
27
|
+
| Every explicit requirement has concrete, consistent evidence | `pass=true`, `unmet=[]` |
|
|
28
|
+
| Any requirement is incomplete, contradicted, or unsupported | `pass=false`, list it in `unmet` |
|
|
29
|
+
| A read-only check fails or cannot run | `pass=false`, name the verification gap in `unmet` |
|
|
30
|
+
|
|
31
|
+
Do not edit files, delegate work, broaden the Goal, attempt fixes, or run a broad unit-test suite unless the Goal explicitly requires that suite.
|
|
32
|
+
</verdict_policy>
|
|
33
|
+
|
|
34
|
+
<evidence_policy>
|
|
35
|
+
Prefer evidence already supplied by the parent session. A successful tool call or result in the transcript is valid evidence for that observed action.
|
|
36
|
+
|
|
37
|
+
| Good evidence | Bad substitution |
|
|
38
|
+
|---------------|------------------|
|
|
39
|
+
| The transcript contains the requested Goal action and its result | Running unrelated repository tests |
|
|
40
|
+
| A focused read-only command confirms a completion claim | Exploring the whole codebase without a concrete gap |
|
|
41
|
+
|
|
42
|
+
Canonical Workflow evidence is relevant only when it belongs to the Goal being judged; note unrelated Workflow state without treating it as proof.
|
|
43
|
+
</evidence_policy>
|
|
44
|
+
|
|
45
|
+
<output_contract>
|
|
46
|
+
The `structured_output` tool is available and mandatory. Call it exactly once as the final action on every path, including failure, missing evidence, or check errors. Populate all four fields: `pass`, `reasoning`, `unmet`, and `evidence`. Do not emit prose after the tool call.
|
|
47
|
+
</output_contract>
|
|
48
|
+
|
|
49
|
+
<quality_gate>
|
|
50
|
+
Before calling `structured_output`, verify:
|
|
51
|
+
- [ ] Every explicit Goal requirement has a corresponding evidence item or `unmet` entry.
|
|
52
|
+
- [ ] `pass=true` is used only when `unmet` is empty and evidence is concrete.
|
|
53
|
+
- [ ] Missing evidence produces `pass=false`, not a prose-only or inconclusive response.
|
|
54
|
+
- [ ] No write, delegation, or unrelated broad test was performed.
|
|
55
|
+
</quality_gate>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow
|
|
3
|
+
description: Decomposes complex problems and dispatches dependency-aware teammate DAGs
|
|
4
|
+
systemPromptMode: replace
|
|
5
|
+
inheritProjectContext: true
|
|
6
|
+
thinking: high
|
|
7
|
+
tools: read, grep, find, ls, bash, edit, write, teammate, teammate-send, teammate-list, teammate-watch
|
|
8
|
+
inheritSkills: false
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are the workflow teammate responsible for solving multi-step problems through dependency-aware delegation.
|
|
12
|
+
|
|
13
|
+
When dispatching work with the teammate tool:
|
|
14
|
+
- Give every task a stable unique `name`
|
|
15
|
+
- Use `{name}` and `{name.field}` references to declare dependencies
|
|
16
|
+
- Use `outputSchema` when downstream tasks require structured fields
|
|
17
|
+
- Keep independent tasks in the same wave so the runtime can execute them in parallel
|
|
18
|
+
- Set `concurrency` explicitly to a provider-safe bound
|
|
19
|
+
- Set `background: false` whenever your next step depends on the child results
|
|
20
|
+
- Let the teammate runtime resolve and execute the DAG; do not simulate scheduling in prose
|
|
21
|
+
|
|
22
|
+
Your approach:
|
|
23
|
+
1. Analyze the requested outcome and identify independently verifiable tasks
|
|
24
|
+
2. Build the smallest useful DAG with explicit data flow
|
|
25
|
+
3. Dispatch the DAG and monitor only when status materially affects the next decision
|
|
26
|
+
4. Validate task outputs before synthesizing the final result
|
|
27
|
+
5. Recover or report a concrete blocker when a dependency fails
|
|
28
|
+
|
|
29
|
+
Use teammate-send for targeted follow-up and teammate-list or teammate-watch only when live state is needed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-maestro-teammate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Pi extension — teammate agent dispatch with DAG task graphs, RPC messaging, and compact TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -52,5 +52,11 @@
|
|
|
52
52
|
"@earendil-works/pi-ai": "0.80.3",
|
|
53
53
|
"@earendil-works/pi-coding-agent": "0.80.3",
|
|
54
54
|
"@earendil-works/pi-tui": "0.80.3"
|
|
55
|
-
}
|
|
55
|
+
},
|
|
56
|
+
"main": "index.js",
|
|
57
|
+
"directories": {
|
|
58
|
+
"test": "test"
|
|
59
|
+
},
|
|
60
|
+
"author": "",
|
|
61
|
+
"license": "ISC"
|
|
56
62
|
}
|
package/src/agents/agents.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent discovery and configuration.
|
|
3
3
|
*
|
|
4
|
-
* Discovers agent definitions from
|
|
5
|
-
*
|
|
6
|
-
* 2. User agents: ~/.pi/agent/extensions/teammate/agents/
|
|
7
|
-
* 3. Builtin agents: bundled agents/ directory in this package
|
|
4
|
+
* Discovers agent definitions from compatible project and user locations.
|
|
5
|
+
* Precedence: project .pi/agents > project .agents > ~/.agents > legacy user > builtin.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
import * as fs from "node:fs";
|
|
@@ -12,17 +10,28 @@ import * as os from "node:os";
|
|
|
12
10
|
import * as path from "node:path";
|
|
13
11
|
import { fileURLToPath } from "node:url";
|
|
14
12
|
import { parseFrontmatter } from "./frontmatter.ts";
|
|
13
|
+
import { parseTeammateThinkingLevel, type TeammateThinkingLevel } from "../shared/thinking.ts";
|
|
15
14
|
|
|
16
15
|
type SystemPromptMode = "append" | "replace";
|
|
17
16
|
export type AgentSource = "builtin" | "user" | "project";
|
|
18
17
|
|
|
18
|
+
export const BUILTIN_AGENT_NAMES = ["delegate", "explorer", "goal-verifier", "workflow"] as const;
|
|
19
|
+
export type BuiltinAgentName = (typeof BUILTIN_AGENT_NAMES)[number];
|
|
20
|
+
|
|
21
|
+
const LEGACY_AGENT_ALIASES: Readonly<Record<string, BuiltinAgentName>> = {
|
|
22
|
+
coordinator: "workflow",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const AGENT_CATALOG_START_MARKER = "<!-- teammate-agent-catalog:start -->";
|
|
26
|
+
const AGENT_CATALOG_END_MARKER = "<!-- teammate-agent-catalog:end -->";
|
|
27
|
+
|
|
19
28
|
export interface AgentConfig {
|
|
20
29
|
name: string;
|
|
21
30
|
description: string;
|
|
22
31
|
tools?: string[];
|
|
23
32
|
model?: string;
|
|
24
33
|
fallbackModels?: string[];
|
|
25
|
-
thinking?:
|
|
34
|
+
thinking?: TeammateThinkingLevel;
|
|
26
35
|
systemPromptMode: SystemPromptMode;
|
|
27
36
|
inheritProjectContext: boolean;
|
|
28
37
|
inheritSkills: boolean;
|
|
@@ -38,6 +47,11 @@ export interface AgentSummary {
|
|
|
38
47
|
source: AgentSource;
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
export interface AgentCatalogSnapshot {
|
|
51
|
+
signature: string;
|
|
52
|
+
systemPrompt: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
const BUILTIN_AGENTS_DIR = path.resolve(
|
|
42
56
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
43
57
|
"..",
|
|
@@ -123,7 +137,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
123
137
|
tools: rawTools && rawTools.length > 0 ? rawTools : undefined,
|
|
124
138
|
model: frontmatter.model,
|
|
125
139
|
fallbackModels: rawFallbackModels && rawFallbackModels.length > 0 ? rawFallbackModels : undefined,
|
|
126
|
-
thinking: frontmatter.thinking,
|
|
140
|
+
thinking: parseTeammateThinkingLevel(frontmatter.thinking),
|
|
127
141
|
systemPromptMode,
|
|
128
142
|
inheritProjectContext,
|
|
129
143
|
inheritSkills,
|
|
@@ -137,27 +151,43 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
137
151
|
return agents;
|
|
138
152
|
}
|
|
139
153
|
|
|
154
|
+
export function isBuiltinAgentName(name: string): name is BuiltinAgentName {
|
|
155
|
+
return (BUILTIN_AGENT_NAMES as readonly string[]).includes(name);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isReservedAgentName(name: string): boolean {
|
|
159
|
+
return isBuiltinAgentName(name) || Object.hasOwn(LEGACY_AGENT_ALIASES, name);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function canonicalAgentName(name: string): string {
|
|
163
|
+
return LEGACY_AGENT_ALIASES[name] ?? name;
|
|
164
|
+
}
|
|
165
|
+
|
|
140
166
|
/**
|
|
141
167
|
* Discover all agent definitions, merged by priority:
|
|
142
168
|
* project > user > builtin (name collisions: higher priority wins).
|
|
143
169
|
*/
|
|
144
|
-
export function discoverAgents(cwd: string): AgentConfig[] {
|
|
145
|
-
const
|
|
146
|
-
|
|
170
|
+
export function discoverAgents(cwd: string, homeDir = os.homedir()): AgentConfig[] {
|
|
171
|
+
const legacyUserAgentsDir = path.join(
|
|
172
|
+
homeDir,
|
|
147
173
|
".pi",
|
|
148
174
|
"agent",
|
|
149
175
|
"extensions",
|
|
150
176
|
"teammate",
|
|
151
177
|
"agents",
|
|
152
178
|
);
|
|
179
|
+
const userAgentsDir = path.join(homeDir, ".agents");
|
|
153
180
|
|
|
154
|
-
// Find
|
|
155
|
-
let
|
|
181
|
+
// Find the nearest ancestor containing either supported project directory.
|
|
182
|
+
let projectPiAgentsDir: string | null = null;
|
|
183
|
+
let projectCompatAgentsDir: string | null = null;
|
|
156
184
|
let currentDir = cwd;
|
|
157
185
|
while (true) {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
186
|
+
const piAgentsDir = path.join(currentDir, ".pi", "agents");
|
|
187
|
+
const compatAgentsDir = path.join(currentDir, ".agents");
|
|
188
|
+
if (fs.existsSync(piAgentsDir) || fs.existsSync(compatAgentsDir)) {
|
|
189
|
+
projectPiAgentsDir = fs.existsSync(piAgentsDir) ? piAgentsDir : null;
|
|
190
|
+
projectCompatAgentsDir = fs.existsSync(compatAgentsDir) ? compatAgentsDir : null;
|
|
161
191
|
break;
|
|
162
192
|
}
|
|
163
193
|
const parentDir = path.dirname(currentDir);
|
|
@@ -165,17 +195,36 @@ export function discoverAgents(cwd: string): AgentConfig[] {
|
|
|
165
195
|
currentDir = parentDir;
|
|
166
196
|
}
|
|
167
197
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
198
|
+
const builtinByName = new Map(
|
|
199
|
+
loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin")
|
|
200
|
+
.filter((agent) => isBuiltinAgentName(agent.name))
|
|
201
|
+
.map((agent) => [agent.name, agent]),
|
|
202
|
+
);
|
|
203
|
+
const builtinAgents = BUILTIN_AGENT_NAMES
|
|
204
|
+
.map((name) => builtinByName.get(name))
|
|
205
|
+
.filter((agent): agent is AgentConfig => agent !== undefined);
|
|
206
|
+
const loadCustomAgents = (dir: string, source: AgentSource): AgentConfig[] =>
|
|
207
|
+
loadAgentsFromDir(dir, source)
|
|
208
|
+
.filter((agent) => !isReservedAgentName(agent.name));
|
|
209
|
+
|
|
210
|
+
// Builtin names are reserved so project/user definitions cannot silently
|
|
211
|
+
// replace the stable general, exploration, and DAG orchestration roles.
|
|
212
|
+
const legacyUserAgents = loadCustomAgents(legacyUserAgentsDir, "user");
|
|
213
|
+
const userAgents = loadCustomAgents(userAgentsDir, "user");
|
|
214
|
+
const projectCompatAgents = projectCompatAgentsDir
|
|
215
|
+
? loadCustomAgents(projectCompatAgentsDir, "project")
|
|
216
|
+
: [];
|
|
217
|
+
const projectPiAgents = projectPiAgentsDir
|
|
218
|
+
? loadCustomAgents(projectPiAgentsDir, "project")
|
|
172
219
|
: [];
|
|
173
220
|
|
|
174
|
-
// Merge
|
|
221
|
+
// Merge from lowest to highest priority.
|
|
175
222
|
const agentMap = new Map<string, AgentConfig>();
|
|
176
223
|
for (const agent of builtinAgents) agentMap.set(agent.name, agent);
|
|
224
|
+
for (const agent of legacyUserAgents) agentMap.set(agent.name, agent);
|
|
177
225
|
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
178
|
-
for (const agent of
|
|
226
|
+
for (const agent of projectCompatAgents) agentMap.set(agent.name, agent);
|
|
227
|
+
for (const agent of projectPiAgents) agentMap.set(agent.name, agent);
|
|
179
228
|
|
|
180
229
|
return Array.from(agentMap.values());
|
|
181
230
|
}
|
|
@@ -188,7 +237,8 @@ export function resolveAgent(
|
|
|
188
237
|
agentName: string,
|
|
189
238
|
): AgentConfig | undefined {
|
|
190
239
|
const agents = discoverAgents(cwd);
|
|
191
|
-
|
|
240
|
+
const canonicalName = canonicalAgentName(agentName);
|
|
241
|
+
return agents.find((a) => a.name === canonicalName);
|
|
192
242
|
}
|
|
193
243
|
|
|
194
244
|
/** Return resolved role metadata without exposing the role prompt body. */
|
|
@@ -222,3 +272,60 @@ export function formatAgentCatalog(
|
|
|
222
272
|
|
|
223
273
|
return lines.join("\n");
|
|
224
274
|
}
|
|
275
|
+
|
|
276
|
+
/** Build the compact role directory appended to the active parent prompt. */
|
|
277
|
+
export function createAgentCatalogSnapshot(
|
|
278
|
+
cwd: string,
|
|
279
|
+
maxDescriptionLength = 160,
|
|
280
|
+
): AgentCatalogSnapshot {
|
|
281
|
+
const summaries = listAgentSummaries(cwd);
|
|
282
|
+
const byName = new Map(summaries.map((agent) => [agent.name, agent]));
|
|
283
|
+
const builtins = BUILTIN_AGENT_NAMES
|
|
284
|
+
.map((name) => byName.get(name))
|
|
285
|
+
.filter((agent): agent is AgentSummary => agent !== undefined);
|
|
286
|
+
const discovered = summaries
|
|
287
|
+
.filter((agent) => !isBuiltinAgentName(agent.name));
|
|
288
|
+
|
|
289
|
+
const formatLine = (agent: AgentSummary): string => {
|
|
290
|
+
const normalized = agent.description.replace(/\s+/g, " ").trim();
|
|
291
|
+
const description = normalized.length > maxDescriptionLength
|
|
292
|
+
? `${normalized.slice(0, Math.max(1, maxDescriptionLength - 1)).trimEnd()}…`
|
|
293
|
+
: normalized;
|
|
294
|
+
return `- ${agent.name}: ${description}`;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const lines = [
|
|
298
|
+
AGENT_CATALOG_START_MARKER,
|
|
299
|
+
"# Available Teammate Agents",
|
|
300
|
+
"",
|
|
301
|
+
"Built-in roles:",
|
|
302
|
+
...builtins.map(formatLine),
|
|
303
|
+
"",
|
|
304
|
+
"Discovered project and user roles:",
|
|
305
|
+
...(discovered.length > 0 ? discovered.map(formatLine) : ["(none)"]),
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
lines.push(
|
|
309
|
+
"",
|
|
310
|
+
"Use the exact agent name. Unknown names are invalid. Agent prompt bodies are loaded only after a role is selected.",
|
|
311
|
+
AGENT_CATALOG_END_MARKER,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
signature: summaries
|
|
316
|
+
.map((agent) => `${agent.name}:${agent.source}:${agent.description}`)
|
|
317
|
+
.join("\n"),
|
|
318
|
+
systemPrompt: lines.join("\n"),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Replace an existing role directory or append a fresh one to the prompt. */
|
|
323
|
+
export function appendAgentCatalog(systemPrompt: string, cwd: string): string {
|
|
324
|
+
const snapshot = createAgentCatalogSnapshot(cwd);
|
|
325
|
+
const start = systemPrompt.indexOf(AGENT_CATALOG_START_MARKER);
|
|
326
|
+
const end = systemPrompt.indexOf(AGENT_CATALOG_END_MARKER);
|
|
327
|
+
if (start >= 0 && end >= start) {
|
|
328
|
+
return `${systemPrompt.slice(0, start)}${snapshot.systemPrompt}${systemPrompt.slice(end + AGENT_CATALOG_END_MARKER.length)}`;
|
|
329
|
+
}
|
|
330
|
+
return `${systemPrompt}\n\n${snapshot.systemPrompt}`;
|
|
331
|
+
}
|