pi-open-agents 0.1.24 → 0.1.26
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 +6 -0
- package/package.json +1 -1
- package/src/discovery/loader.ts +13 -2
- package/src/index.ts +30 -2
- package/src/primary/manager.ts +25 -1
- package/src/tui/banner.ts +12 -2
package/README.md
CHANGED
|
@@ -191,6 +191,12 @@ This means:
|
|
|
191
191
|
- Each subagent runs with exactly the model and tools it declares
|
|
192
192
|
- Skills load per-agent, with wildcard support (`security-*`, `git-*`)
|
|
193
193
|
|
|
194
|
+
> **`skills:` is subagent-only (#19).** pi discovers skills once at session
|
|
195
|
+
> startup and exposes no per-agent filter, so a **primary** cannot role-scope
|
|
196
|
+
> them. Declaring `skills:` on a primary (or `all`) agent warns on apply and
|
|
197
|
+
> the field is ignored; the subagent child receives exactly its listed skills,
|
|
198
|
+
> injected into its system prompt.
|
|
199
|
+
|
|
194
200
|
**Per-call model override:** the caller can override the agent's default for a
|
|
195
201
|
single call — `subagent({ agent: "deep-research", task: "...", model: "zai/glm-5.3-flash" })`.
|
|
196
202
|
Handy for "run this one task on the cheap model" without forking an agent
|
package/package.json
CHANGED
package/src/discovery/loader.ts
CHANGED
|
@@ -92,6 +92,9 @@ export async function loadAgents(options: LoadAgentsOptions = {}): Promise<Agent
|
|
|
92
92
|
const projectAgents = new Map<string, AgentDefinition>();
|
|
93
93
|
const warnings: AgentLoadResult["warnings"] = [];
|
|
94
94
|
|
|
95
|
+
const GLOBAL_FAMILY_RANK: Record<string, number> = { pi: 2, opencode: 1, shared: 1 };
|
|
96
|
+
const globalRanks = new Map<string, number>();
|
|
97
|
+
|
|
95
98
|
for (const discPath of paths) {
|
|
96
99
|
// For each subdir pattern, scan the directory
|
|
97
100
|
for (const subdir of discPath.subdirs) {
|
|
@@ -113,8 +116,16 @@ export async function loadAgents(options: LoadAgentsOptions = {}): Promise<Agent
|
|
|
113
116
|
if (agent.disable) continue;
|
|
114
117
|
|
|
115
118
|
// Add to the appropriate map
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
if (discPath.source === "project") {
|
|
120
|
+
projectAgents.set(agent.name, agent);
|
|
121
|
+
} else {
|
|
122
|
+
const rank = GLOBAL_FAMILY_RANK[discPath.family] ?? 0;
|
|
123
|
+
const prevRank = globalRanks.get(agent.name);
|
|
124
|
+
if (prevRank === undefined || rank >= prevRank) {
|
|
125
|
+
globalAgents.set(agent.name, agent);
|
|
126
|
+
globalRanks.set(agent.name, rank);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
118
129
|
} catch (error) {
|
|
119
130
|
warnings.push({
|
|
120
131
|
filePath,
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ import type { AgentDefinition, ThinkingLevel } from "./types.ts";
|
|
|
22
22
|
import { loadAgents, selectableAgents, spawnableAgents } from "./discovery/loader.ts";
|
|
23
23
|
import { AgentManager } from "./primary/manager.ts";
|
|
24
24
|
import { refreshAgentTools, buildSystemPrompt, buildSubagentGuidance } from "./primary/executor.ts";
|
|
25
|
-
import { updateBanner } from "./tui/banner.ts";
|
|
25
|
+
import { setShowAgentDescription, updateBanner } from "./tui/banner.ts";
|
|
26
26
|
import { registerTuiControls } from "./tui/selector.ts";
|
|
27
27
|
import { registerAgentTools } from "./tui/tools.ts";
|
|
28
28
|
import { registerSubagentTool } from "./subagent/tool.ts";
|
|
@@ -34,6 +34,7 @@ import { registerPermissionGate } from "./permission/gate.ts";
|
|
|
34
34
|
interface Settings {
|
|
35
35
|
defaultAgent?: string;
|
|
36
36
|
defaultThinkingLevel?: ThinkingLevel;
|
|
37
|
+
showAgentDescription?: boolean;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function loadSettings(cwd: string): Settings {
|
|
@@ -75,6 +76,7 @@ export default function piOpenAgents(pi: ExtensionAPI): void {
|
|
|
75
76
|
pi.on("session_start", async (event, ctx) => {
|
|
76
77
|
// Load settings for thinking fallback
|
|
77
78
|
const settings = loadSettings(ctx.cwd);
|
|
79
|
+
setShowAgentDescription(settings.showAgentDescription);
|
|
78
80
|
|
|
79
81
|
// Load all agent definitions from discovery paths
|
|
80
82
|
const result = await loadAgents({
|
|
@@ -115,6 +117,16 @@ export default function piOpenAgents(pi: ExtensionAPI): void {
|
|
|
115
117
|
// (--model, --system-prompt, --tools). Do NOT apply defaultAgent — it would
|
|
116
118
|
// override the executor's model/tools/prompt settings.
|
|
117
119
|
if (isSubagentProcess(process.env)) {
|
|
120
|
+
// A forked child inherits the parent's session entries (pi core copies
|
|
121
|
+
// them into the branched session) and never re-announces (this early
|
|
122
|
+
// return). Without this, the permission system would resolve the
|
|
123
|
+
// parent's active_agent entry and attribute the parent's policy to the
|
|
124
|
+
// child. Append an explicit terminator so the child falls back to the
|
|
125
|
+
// global policy — the pre-patch baseline. Falls back to the global policy
|
|
126
|
+
// because nothing has resolved an agent name yet in this process — the
|
|
127
|
+
// permission system caches the last name it resolved (knownAgentName) and
|
|
128
|
+
// does not clear it between sessions.
|
|
129
|
+
pi.appendEntry("active_agent", { name: null });
|
|
118
130
|
updateBanner(ctx.ui, undefined, 0);
|
|
119
131
|
return;
|
|
120
132
|
}
|
|
@@ -140,10 +152,26 @@ export default function piOpenAgents(pi: ExtensionAPI): void {
|
|
|
140
152
|
|
|
141
153
|
if (agentEntry?.data?.name) {
|
|
142
154
|
manager.restoreFromSession(agentEntry.data.name);
|
|
143
|
-
//
|
|
155
|
+
// Only announce the agent when the restore actually produced an active
|
|
156
|
+
// agent. restoreFromSession() silently no-ops on an unknown name
|
|
157
|
+
// (deleted, renamed, or disable:true — the loader skips those), and
|
|
158
|
+
// announcing one would make the permission system apply a disabled
|
|
159
|
+
// agent's policy to this session.
|
|
144
160
|
const active = manager.getActive();
|
|
145
161
|
if (active) {
|
|
162
|
+
pi.appendEntry("active_agent", { name: active.name });
|
|
163
|
+
// Re-apply tools on resume (model preserved from session)
|
|
146
164
|
refreshAgentTools(active, pi);
|
|
165
|
+
} else {
|
|
166
|
+
// The restore no-op'd (see above), so no new entry was written. An
|
|
167
|
+
// older active_agent entry written by a previous apply() is still the
|
|
168
|
+
// newest one, and the consumer's backward scan would resolve it —
|
|
169
|
+
// silently applying that (now disabled/unknown) agent's policy.
|
|
170
|
+
// Append an explicit terminator so it stops here and falls back to
|
|
171
|
+
// the global policy — nothing has resolved an agent name yet in this
|
|
172
|
+
// process, since the consumer caches the last name it resolved
|
|
173
|
+
// (knownAgentName) and does not clear it between sessions.
|
|
174
|
+
pi.appendEntry("active_agent", { name: null });
|
|
147
175
|
}
|
|
148
176
|
updateBanner(ctx.ui, manager.getActive(), selectableAgents(manager.getAgents()).length);
|
|
149
177
|
return;
|
package/src/primary/manager.ts
CHANGED
|
@@ -5,11 +5,28 @@
|
|
|
5
5
|
* and coordinates with the executor for model/thinking/tools/prompt.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import type { Model } from "@earendil-works/pi-ai";
|
|
10
10
|
import type { AgentDefinition, ThinkingLevel } from "../types.ts";
|
|
11
11
|
import { applyAgentConfig } from "./executor.ts";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Warn when an agent that will act as PRIMARY declares `skills:`.
|
|
15
|
+
*
|
|
16
|
+
* `skills:` is honored only for subagent children (the executor injects them
|
|
17
|
+
* into the child's system prompt). pi loads session skills at startup and
|
|
18
|
+
* exposes no per-agent filter, so a primary cannot role-scope them — warn
|
|
19
|
+
* instead of silently ignoring (#19).
|
|
20
|
+
*/
|
|
21
|
+
export function warnAgentSkillsPrimary(agent: AgentDefinition | undefined, ui: ExtensionUIContext): void {
|
|
22
|
+
if (agent?.skills && agent.skills.length > 0 && agent.mode !== "subagent") {
|
|
23
|
+
ui.notify(
|
|
24
|
+
`Agent "${agent.name}": skills: (${agent.skills.join(", ")}) applies to subagent delegation only — primaries use pi's session-wide skill set (#19)`,
|
|
25
|
+
"warning",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
13
30
|
interface OriginalState {
|
|
14
31
|
model: Model<any> | undefined;
|
|
15
32
|
thinkingLevel: ThinkingLevel;
|
|
@@ -56,6 +73,8 @@ export class AgentManager {
|
|
|
56
73
|
const agent = this.agents.get(name);
|
|
57
74
|
if (!agent) return false;
|
|
58
75
|
|
|
76
|
+
warnAgentSkillsPrimary(agent, ctx.ui);
|
|
77
|
+
|
|
59
78
|
// Snapshot state on first apply (for restore on clear)
|
|
60
79
|
if (this.activeName === undefined) {
|
|
61
80
|
this.originalState = {
|
|
@@ -69,6 +88,7 @@ export class AgentManager {
|
|
|
69
88
|
this.active = agent;
|
|
70
89
|
|
|
71
90
|
await applyAgentConfig(agent, pi, ctx);
|
|
91
|
+
pi.appendEntry("active_agent", { name });
|
|
72
92
|
|
|
73
93
|
return true;
|
|
74
94
|
}
|
|
@@ -93,6 +113,10 @@ export class AgentManager {
|
|
|
93
113
|
|
|
94
114
|
this.activeName = undefined;
|
|
95
115
|
this.active = undefined;
|
|
116
|
+
// The consumer treats a null-name entry as "stop scanning" and then falls
|
|
117
|
+
// back to its last known name, so clearing does not restore the global
|
|
118
|
+
// policy for a session that already resolved a non-null agent.
|
|
119
|
+
pi.appendEntry("active_agent", { name: null });
|
|
96
120
|
}
|
|
97
121
|
|
|
98
122
|
/**
|
package/src/tui/banner.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Status banner widget for the TUI.
|
|
3
3
|
*
|
|
4
|
-
* Shows the active agent name,
|
|
4
|
+
* Shows the active agent name, description (when enabled), and model above the editor.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { AgentDefinition } from "../types.ts";
|
|
9
9
|
|
|
10
|
+
let showDescription = true;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configure whether the active agent's description is shown in the status banner.
|
|
14
|
+
* Absent/undefined falls back to true, preserving the default behavior.
|
|
15
|
+
*/
|
|
16
|
+
export function setShowAgentDescription(value: boolean | undefined): void {
|
|
17
|
+
showDescription = value ?? true;
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
/**
|
|
11
21
|
* Update the status widget to reflect the active agent.
|
|
12
22
|
* Pass undefined to show "no agent selected" hint.
|
|
@@ -20,7 +30,7 @@ export function updateBanner(
|
|
|
20
30
|
const parts: string[] = [];
|
|
21
31
|
parts.push(ui.theme.bold(ui.theme.fg("accent", activeAgent.name)));
|
|
22
32
|
|
|
23
|
-
if (activeAgent.description) {
|
|
33
|
+
if (showDescription && activeAgent.description) {
|
|
24
34
|
parts.push(ui.theme.fg("muted", `— ${activeAgent.description}`));
|
|
25
35
|
}
|
|
26
36
|
|