@dougbots/pi-agents 0.1.1 → 0.2.0
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 +34 -0
- package/extensions/agents/agent-profile.ts +107 -0
- package/extensions/agents/index.ts +161 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,36 @@ Profile fields:
|
|
|
49
49
|
- `excludeTools` — denylist of tool names (removed from available set)
|
|
50
50
|
- `permissions.bash` — `{ allow?: string[], deny?: string[] }` with glob patterns
|
|
51
51
|
|
|
52
|
+
### Runtime agent profiles
|
|
53
|
+
|
|
54
|
+
A runtime agent profile is a session-scoped overlay for role profiles. It can
|
|
55
|
+
change execution settings without changing the role's system prompt, tools, or
|
|
56
|
+
permission boundary. Define global profiles in `~/.pi/agent/agent-profiles.json`
|
|
57
|
+
and project overrides in `.pi/agent-profiles.json`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"cloud": {
|
|
62
|
+
"description": "Temporary cloud fallbacks",
|
|
63
|
+
"agents": {
|
|
64
|
+
"explore": { "model": "sparky/deepseek-flash" },
|
|
65
|
+
"mule": { "model": "sparky/deepseek-flash" },
|
|
66
|
+
"reviewer": { "model": "sparky/deepseek-flash" }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Only `model` and `thinkingLevel` may be overridden. Project profile entries
|
|
73
|
+
merge over global entries by profile and agent name. Malformed entries are
|
|
74
|
+
ignored and reported as a Pi warning. A selected profile is persisted in the Pi
|
|
75
|
+
session and restored by `/resume`.
|
|
76
|
+
|
|
77
|
+
`PI_AGENT_PROFILE=<name>` is an explicit launch-time override; `PROFILE=<name>`
|
|
78
|
+
is accepted as a convenience alias. Both take precedence over a saved session
|
|
79
|
+
selection. Explicit `--model` still takes precedence over an agent-profile's
|
|
80
|
+
model.
|
|
81
|
+
|
|
52
82
|
## Commands
|
|
53
83
|
|
|
54
84
|
| Command | Description |
|
|
@@ -57,12 +87,16 @@ Profile fields:
|
|
|
57
87
|
| `/agent none` | Deactivate agent, restore all tools |
|
|
58
88
|
| `/agent` | List available agents |
|
|
59
89
|
| `/agents` | Same as `/agent` |
|
|
90
|
+
| `/agent-profile <name>` | Select a session-scoped runtime agent profile |
|
|
91
|
+
| `/agent-profile none` | Clear the session runtime agent profile |
|
|
92
|
+
| `/agent-profile` | Show the active and available runtime agent profiles |
|
|
60
93
|
|
|
61
94
|
## Boot with agent
|
|
62
95
|
|
|
63
96
|
```bash
|
|
64
97
|
PI_AGENT=reviewer pi
|
|
65
98
|
PI_AGENT=jockey pi -c
|
|
99
|
+
PI_AGENT=explore PI_AGENT_PROFILE=cloud pi
|
|
66
100
|
```
|
|
67
101
|
|
|
68
102
|
## Avenor integration
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
2
|
+
|
|
3
|
+
/** Runtime-safe fields a named agent-profile may override on a base agent. */
|
|
4
|
+
export interface AgentProfileOverride {
|
|
5
|
+
model?: string;
|
|
6
|
+
thinkingLevel?: ThinkingLevel;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AgentProfileSet {
|
|
10
|
+
description?: string;
|
|
11
|
+
agents?: Record<string, AgentProfileOverride>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AgentProfilesConfig {
|
|
15
|
+
[name: string]: AgentProfileSet;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const thinkingLevels = new Set<ThinkingLevel>([
|
|
19
|
+
"off", "minimal", "low", "medium", "high", "xhigh",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
23
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Discard unsupported fields from untrusted JSON config. */
|
|
27
|
+
export function executionOverride(value: unknown): AgentProfileOverride | undefined {
|
|
28
|
+
if (!isRecord(value)) return undefined;
|
|
29
|
+
const override: AgentProfileOverride = {};
|
|
30
|
+
if (typeof value.model === "string") override.model = value.model;
|
|
31
|
+
if (typeof value.thinkingLevel === "string" && thinkingLevels.has(value.thinkingLevel as ThinkingLevel)) {
|
|
32
|
+
override.thinkingLevel = value.thinkingLevel as ThinkingLevel;
|
|
33
|
+
}
|
|
34
|
+
return Object.keys(override).length ? override : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Merge profile config sources: project entries override global entries by
|
|
39
|
+
* profile and agent name. Invalid JSON shapes are ignored rather than making
|
|
40
|
+
* the extension fail to load.
|
|
41
|
+
*/
|
|
42
|
+
export function mergeAgentProfilesConfigs(
|
|
43
|
+
configs: readonly unknown[],
|
|
44
|
+
onInvalid?: (message: string) => void,
|
|
45
|
+
): AgentProfilesConfig {
|
|
46
|
+
const merged: AgentProfilesConfig = {};
|
|
47
|
+
|
|
48
|
+
for (const config of configs) {
|
|
49
|
+
if (!isRecord(config)) {
|
|
50
|
+
onInvalid?.("top-level value must be an object");
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const [name, rawProfile] of Object.entries(config)) {
|
|
55
|
+
if (!isRecord(rawProfile)) {
|
|
56
|
+
onInvalid?.(`profile "${name}" must be an object`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (rawProfile.agents !== undefined && !isRecord(rawProfile.agents)) {
|
|
60
|
+
onInvalid?.(`profile "${name}" has a non-object agents value`);
|
|
61
|
+
}
|
|
62
|
+
if (rawProfile.description !== undefined && typeof rawProfile.description !== "string") {
|
|
63
|
+
onInvalid?.(`profile "${name}" has a non-string description`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const current = Object.hasOwn(merged, name) ? merged[name] : undefined;
|
|
67
|
+
const rawAgents = isRecord(rawProfile.agents) ? rawProfile.agents : {};
|
|
68
|
+
const profile: AgentProfileSet = {
|
|
69
|
+
...(typeof current?.description === "string" ? { description: current.description } : {}),
|
|
70
|
+
...(typeof rawProfile.description === "string" ? { description: rawProfile.description } : {}),
|
|
71
|
+
agents: {
|
|
72
|
+
...current?.agents,
|
|
73
|
+
...rawAgents as Record<string, AgentProfileOverride>,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// defineProperty avoids the __proto__ setter on normal objects and makes
|
|
78
|
+
// profile-name lookups consistently own-property-only.
|
|
79
|
+
Object.defineProperty(merged, name, {
|
|
80
|
+
value: profile,
|
|
81
|
+
enumerable: true,
|
|
82
|
+
configurable: true,
|
|
83
|
+
writable: true,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return merged;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Return the latest persisted profile selection, including an explicit clear. */
|
|
92
|
+
export function findPersistedProfileName(entries: readonly unknown[]): string | undefined {
|
|
93
|
+
let profileName: string | undefined;
|
|
94
|
+
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (!entry || typeof entry !== "object") continue;
|
|
97
|
+
const customEntry = entry as { type?: unknown; customType?: unknown; data?: unknown };
|
|
98
|
+
if (customEntry.type !== "custom" || customEntry.customType !== "pi-agents:profile") continue;
|
|
99
|
+
if (!customEntry.data || typeof customEntry.data !== "object") continue;
|
|
100
|
+
|
|
101
|
+
const name = (customEntry.data as { name?: unknown }).name;
|
|
102
|
+
if (typeof name === "string") profileName = name;
|
|
103
|
+
if (name === null) profileName = undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return profileName;
|
|
107
|
+
}
|
|
@@ -2,6 +2,13 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
2
2
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { readFileSync, existsSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
executionOverride,
|
|
7
|
+
findPersistedProfileName,
|
|
8
|
+
mergeAgentProfilesConfigs,
|
|
9
|
+
type AgentProfilesConfig,
|
|
10
|
+
type ThinkingLevel,
|
|
11
|
+
} from "./agent-profile.ts";
|
|
5
12
|
import { findCliModelOverride, parseModelSpecifier } from "./model-selection.ts";
|
|
6
13
|
|
|
7
14
|
/* ------------------------------------------------------------------ */
|
|
@@ -17,7 +24,7 @@ interface AgentProfile {
|
|
|
17
24
|
description?: string;
|
|
18
25
|
model: string; // "provider/model-id"
|
|
19
26
|
systemPrompt: string; // inline text or "file:/absolute/path/to/prompt.md"
|
|
20
|
-
thinkingLevel?:
|
|
27
|
+
thinkingLevel?: ThinkingLevel;
|
|
21
28
|
tools?: string[]; // allowlist — if set, only these tools are callable
|
|
22
29
|
excludeTools?: string[]; // denylist — removed from available tools
|
|
23
30
|
permissions?: {
|
|
@@ -30,10 +37,10 @@ interface AgentsConfig {
|
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
/* ------------------------------------------------------------------ */
|
|
33
|
-
/* Config loading
|
|
34
|
-
/* with project (.pi/agents.json), project wins. */
|
|
40
|
+
/* Config loading */
|
|
35
41
|
/* ------------------------------------------------------------------ */
|
|
36
42
|
|
|
43
|
+
/** Merge global ~/.pi/agent/agents.json with project .pi/agents.json. */
|
|
37
44
|
function loadAgentsConfig(): AgentsConfig {
|
|
38
45
|
const agentDir = getAgentDir();
|
|
39
46
|
const globalPath = join(agentDir, "agents.json");
|
|
@@ -51,10 +58,40 @@ function loadAgentsConfig(): AgentsConfig {
|
|
|
51
58
|
console.error(`[agents] Failed to parse ${label} agents.json (${path}):`, err);
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
|
-
// Later entries override earlier — project wins over global
|
|
61
|
+
// Later entries override earlier — project wins over global.
|
|
55
62
|
return configs.reduce<AgentsConfig>((merged, cfg) => ({ ...merged, ...cfg }), {});
|
|
56
63
|
}
|
|
57
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Load named runtime overlays. These can change only execution settings,
|
|
67
|
+
* leaving an agent's prompt, tools, and permission boundary authoritative.
|
|
68
|
+
*/
|
|
69
|
+
function loadAgentProfilesConfig(): { profiles: AgentProfilesConfig; errors: string[] } {
|
|
70
|
+
const agentDir = getAgentDir();
|
|
71
|
+
const globalPath = join(agentDir, "agent-profiles.json");
|
|
72
|
+
const projectPath = join(process.cwd(), ".pi", "agent-profiles.json");
|
|
73
|
+
|
|
74
|
+
let profiles: AgentProfilesConfig = {};
|
|
75
|
+
const errors: string[] = [];
|
|
76
|
+
for (const [label, path] of [
|
|
77
|
+
["global", globalPath],
|
|
78
|
+
["project", projectPath],
|
|
79
|
+
] as const) {
|
|
80
|
+
if (!existsSync(path)) continue;
|
|
81
|
+
try {
|
|
82
|
+
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
83
|
+
profiles = mergeAgentProfilesConfigs([profiles, config], (message) => {
|
|
84
|
+
errors.push(`${label} ${path}: ${message}`);
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const message = `Failed to parse ${label} agent-profiles.json (${path}): ${String(err)}`;
|
|
88
|
+
console.error(`[agents] ${message}`);
|
|
89
|
+
errors.push(message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { profiles, errors };
|
|
93
|
+
}
|
|
94
|
+
|
|
58
95
|
function resolveSystemPrompt(raw: string): string {
|
|
59
96
|
if (raw.startsWith("file:")) {
|
|
60
97
|
const path = raw.slice(5);
|
|
@@ -82,7 +119,7 @@ function checkBashPermission(
|
|
|
82
119
|
command: string,
|
|
83
120
|
): "allow" | "deny" {
|
|
84
121
|
if (!perms) return "allow";
|
|
85
|
-
// Deny wins over allow
|
|
122
|
+
// Deny wins over allow.
|
|
86
123
|
for (const pattern of perms.deny ?? []) {
|
|
87
124
|
if (matchPattern(pattern, command)) return "deny";
|
|
88
125
|
}
|
|
@@ -99,15 +136,62 @@ function checkBashPermission(
|
|
|
99
136
|
|
|
100
137
|
export default async function (pi: ExtensionAPI) {
|
|
101
138
|
const agents = loadAgentsConfig();
|
|
139
|
+
const { profiles: agentProfiles, errors: agentProfileConfigErrors } = loadAgentProfilesConfig();
|
|
102
140
|
// Built-in flags are not exposed through pi.getFlag(). Avenor forwards its
|
|
103
141
|
// model option to pi as --model, so argv is the extension-level signal that
|
|
104
142
|
// the caller's model should take precedence over the profile default.
|
|
105
143
|
const cliModelOverride = findCliModelOverride(process.argv.slice(2));
|
|
106
144
|
let activeAgent: { name: string; profile: AgentProfile } | null = null;
|
|
145
|
+
let activeAgentProfileName: string | undefined;
|
|
146
|
+
|
|
147
|
+
function getEffectiveAgentProfile(name: string): AgentProfile | undefined {
|
|
148
|
+
const base = agents[name];
|
|
149
|
+
if (!base) return undefined;
|
|
150
|
+
|
|
151
|
+
const override = activeAgentProfileName
|
|
152
|
+
? executionOverride(agentProfiles[activeAgentProfileName]?.agents?.[name])
|
|
153
|
+
: undefined;
|
|
154
|
+
return { ...base, ...override };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function setAgentProfileStatus(ctx: ExtensionContext) {
|
|
158
|
+
ctx.ui.setStatus(
|
|
159
|
+
"agent-profile",
|
|
160
|
+
activeAgentProfileName ? `agent-profile:${activeAgentProfileName}` : "",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Apply a named agent-profile, optionally saving the choice in this session. */
|
|
165
|
+
async function selectAgentProfile(
|
|
166
|
+
name: string | undefined,
|
|
167
|
+
ctx: ExtensionContext,
|
|
168
|
+
persist: boolean,
|
|
169
|
+
): Promise<boolean> {
|
|
170
|
+
if (name && !agentProfiles[name]) {
|
|
171
|
+
ctx.ui.notify(`Agent profile "${name}" not found in agent-profiles.json`, "error");
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const previousName = activeAgentProfileName;
|
|
176
|
+
activeAgentProfileName = name;
|
|
177
|
+
setAgentProfileStatus(ctx);
|
|
178
|
+
|
|
179
|
+
// The active role must receive a new effective model/thinking level now;
|
|
180
|
+
// future roles resolve the overlay when /agent is invoked. Do not persist
|
|
181
|
+
// a selection that cannot be applied to the current active role.
|
|
182
|
+
if (activeAgent && !(await applyAgent(activeAgent.name, ctx))) {
|
|
183
|
+
activeAgentProfileName = previousName;
|
|
184
|
+
setAgentProfileStatus(ctx);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (persist) pi.appendEntry("pi-agents:profile", { name: name ?? null });
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
107
191
|
|
|
108
192
|
/* ---- Apply agent profile to the current session ---- */
|
|
109
193
|
async function applyAgent(name: string, ctx: ExtensionContext): Promise<boolean> {
|
|
110
|
-
const profile =
|
|
194
|
+
const profile = getEffectiveAgentProfile(name);
|
|
111
195
|
if (!profile) {
|
|
112
196
|
ctx.ui.notify(`Agent "${name}" not found in agents.json`, "error");
|
|
113
197
|
return false;
|
|
@@ -131,10 +215,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
131
215
|
await pi.setModel(model);
|
|
132
216
|
}
|
|
133
217
|
|
|
134
|
-
|
|
135
|
-
if (profile.thinkingLevel) {
|
|
136
|
-
pi.setThinkingLevel(profile.thinkingLevel);
|
|
137
|
-
}
|
|
218
|
+
if (profile.thinkingLevel) pi.setThinkingLevel(profile.thinkingLevel);
|
|
138
219
|
|
|
139
220
|
// Tool restrictions — best-effort against currently registered tools.
|
|
140
221
|
// tool_call handler enforces restrictions regardless of registration timing.
|
|
@@ -142,23 +223,25 @@ export default async function (pi: ExtensionAPI) {
|
|
|
142
223
|
const allTools = pi.getAllTools();
|
|
143
224
|
let allowed: typeof allTools;
|
|
144
225
|
if (profile.tools) {
|
|
145
|
-
|
|
146
|
-
allowed = allTools.filter((t) => profile.tools!.includes(t.name));
|
|
147
|
-
// Also include any future tools whose name matches (checked at call time)
|
|
226
|
+
allowed = allTools.filter((tool) => profile.tools!.includes(tool.name));
|
|
148
227
|
} else {
|
|
149
228
|
allowed = [...allTools];
|
|
150
229
|
}
|
|
151
230
|
if (profile.excludeTools) {
|
|
152
|
-
allowed = allowed.filter((
|
|
231
|
+
allowed = allowed.filter((tool) => !profile.excludeTools!.includes(tool.name));
|
|
153
232
|
}
|
|
154
|
-
pi.setActiveTools(allowed.map((
|
|
233
|
+
pi.setActiveTools(allowed.map((tool) => tool.name));
|
|
155
234
|
} catch {
|
|
156
|
-
// setActiveTools may fail if called too early; tool_call gate is the fallback
|
|
235
|
+
// setActiveTools may fail if called too early; tool_call gate is the fallback.
|
|
157
236
|
}
|
|
158
237
|
|
|
159
238
|
activeAgent = { name, profile };
|
|
160
239
|
ctx.ui.setStatus("agent", `agent:${name}`);
|
|
161
|
-
|
|
240
|
+
const selected = activeAgentProfileName ? ` [${activeAgentProfileName}]` : "";
|
|
241
|
+
ctx.ui.notify(
|
|
242
|
+
`Agent "${name}" active${selected}${profile.description ? ": " + profile.description : ""}`,
|
|
243
|
+
"info",
|
|
244
|
+
);
|
|
162
245
|
return true;
|
|
163
246
|
}
|
|
164
247
|
|
|
@@ -166,44 +249,55 @@ export default async function (pi: ExtensionAPI) {
|
|
|
166
249
|
function deactivateAgent(ctx: ExtensionContext) {
|
|
167
250
|
activeAgent = null;
|
|
168
251
|
try {
|
|
169
|
-
pi.setActiveTools(pi.getAllTools().map((
|
|
252
|
+
pi.setActiveTools(pi.getAllTools().map((tool) => tool.name));
|
|
170
253
|
} catch { /* no-op if tools not fully registered */ }
|
|
171
254
|
ctx.ui.setStatus("agent", "");
|
|
172
255
|
ctx.ui.notify("Agent deactivated — all tools restored", "info");
|
|
173
256
|
}
|
|
174
257
|
|
|
175
|
-
/* ---- session_start: apply PI_AGENT
|
|
258
|
+
/* ---- session_start: restore profile, then apply PI_AGENT ---- */
|
|
176
259
|
pi.on("session_start", async (_event, ctx) => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
260
|
+
if (agentProfileConfigErrors.length) {
|
|
261
|
+
ctx.ui.notify(
|
|
262
|
+
`Ignored malformed agent-profile configuration:\n${agentProfileConfigErrors.join("\n")}`,
|
|
263
|
+
"warning",
|
|
264
|
+
);
|
|
180
265
|
}
|
|
266
|
+
|
|
267
|
+
// The environment is an explicit launch-time override. A persisted command
|
|
268
|
+
// selection is restored otherwise, including a saved `none` selection.
|
|
269
|
+
const environmentProfile = process.env.PI_AGENT_PROFILE ?? process.env.PROFILE;
|
|
270
|
+
const persistedProfile = findPersistedProfileName(ctx.sessionManager.getEntries());
|
|
271
|
+
const selectedProfile = environmentProfile ?? persistedProfile;
|
|
272
|
+
if (selectedProfile) await selectAgentProfile(selectedProfile, ctx, false);
|
|
273
|
+
else setAgentProfileStatus(ctx);
|
|
274
|
+
|
|
275
|
+
const envAgent = process.env.PI_AGENT;
|
|
276
|
+
if (envAgent && agents[envAgent]) await applyAgent(envAgent, ctx);
|
|
181
277
|
});
|
|
182
278
|
|
|
183
279
|
/* ---- before_agent_start: inject agent system prompt ---- */
|
|
184
280
|
pi.on("before_agent_start", async (event, _ctx) => {
|
|
185
281
|
if (!activeAgent) return undefined;
|
|
186
282
|
|
|
187
|
-
// Re-apply tool restrictions each turn (catches late-registered tools)
|
|
283
|
+
// Re-apply tool restrictions each turn (catches late-registered tools).
|
|
188
284
|
try {
|
|
189
285
|
const profile = activeAgent.profile;
|
|
190
286
|
const allTools = pi.getAllTools();
|
|
191
287
|
let allowed: typeof allTools;
|
|
192
288
|
if (profile.tools) {
|
|
193
|
-
allowed = allTools.filter((
|
|
289
|
+
allowed = allTools.filter((tool) => profile.tools.includes(tool.name));
|
|
194
290
|
} else {
|
|
195
291
|
allowed = [...allTools];
|
|
196
292
|
}
|
|
197
293
|
if (profile.excludeTools) {
|
|
198
|
-
allowed = allowed.filter((
|
|
294
|
+
allowed = allowed.filter((tool) => !profile.excludeTools.includes(tool.name));
|
|
199
295
|
}
|
|
200
|
-
pi.setActiveTools(allowed.map((
|
|
296
|
+
pi.setActiveTools(allowed.map((tool) => tool.name));
|
|
201
297
|
} catch { /* best effort */ }
|
|
202
298
|
|
|
203
299
|
const agentPrompt = resolveSystemPrompt(activeAgent.profile.systemPrompt);
|
|
204
|
-
return {
|
|
205
|
-
systemPrompt: agentPrompt + "\n\n" + event.systemPrompt,
|
|
206
|
-
};
|
|
300
|
+
return { systemPrompt: agentPrompt + "\n\n" + event.systemPrompt };
|
|
207
301
|
});
|
|
208
302
|
|
|
209
303
|
/* ---- tool_call: enforce tool allowlist/denylist + bash permissions ---- */
|
|
@@ -211,22 +305,18 @@ export default async function (pi: ExtensionAPI) {
|
|
|
211
305
|
if (!activeAgent) return;
|
|
212
306
|
const profile = activeAgent.profile;
|
|
213
307
|
|
|
214
|
-
// Tool allowlist gate
|
|
215
308
|
if (profile.tools && !profile.tools.includes(event.toolName)) {
|
|
216
309
|
return { block: true, reason: `Tool "${event.toolName}" not allowed for agent "${activeAgent.name}"` };
|
|
217
310
|
}
|
|
218
311
|
|
|
219
|
-
// Tool denylist gate
|
|
220
312
|
if (profile.excludeTools && profile.excludeTools.includes(event.toolName)) {
|
|
221
313
|
ctx.ui.notify(`Blocked tool: ${event.toolName}`, "warning");
|
|
222
314
|
return { block: true, reason: `Tool "${event.toolName}" excluded for agent "${activeAgent.name}"` };
|
|
223
315
|
}
|
|
224
316
|
|
|
225
|
-
// Bash permission gate
|
|
226
317
|
if (event.toolName === "bash" && profile.permissions?.bash) {
|
|
227
318
|
const command = (event.input as { command?: string })?.command ?? "";
|
|
228
|
-
|
|
229
|
-
if (verdict === "deny") {
|
|
319
|
+
if (checkBashPermission(profile.permissions.bash, command) === "deny") {
|
|
230
320
|
ctx.ui.notify(`Blocked: ${command.slice(0, 80)}`, "warning");
|
|
231
321
|
return { block: true, reason: `Denied by ${activeAgent.name} bash permissions` };
|
|
232
322
|
}
|
|
@@ -237,12 +327,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
237
327
|
pi.registerCommand("agent", {
|
|
238
328
|
description: "Switch to an agent profile (or 'none' to deactivate)",
|
|
239
329
|
getArgumentCompletions: (prefix) => {
|
|
240
|
-
const matches = Object.keys(agents).filter((
|
|
330
|
+
const matches = Object.keys(agents).filter((name) => name.startsWith(prefix ?? ""));
|
|
241
331
|
return matches.length
|
|
242
|
-
? matches.map((
|
|
243
|
-
value:
|
|
244
|
-
label:
|
|
245
|
-
description: agents[
|
|
332
|
+
? matches.map((name) => ({
|
|
333
|
+
value: name,
|
|
334
|
+
label: name,
|
|
335
|
+
description: agents[name].description ?? "",
|
|
246
336
|
}))
|
|
247
337
|
: null;
|
|
248
338
|
},
|
|
@@ -251,7 +341,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
251
341
|
if (!name) {
|
|
252
342
|
const current = activeAgent ? ` (active: ${activeAgent.name})` : "";
|
|
253
343
|
const list = Object.entries(agents)
|
|
254
|
-
.map(([
|
|
344
|
+
.map(([agentName, profile]) => ` ${agentName}${profile.description ? ": " + profile.description : ""}`)
|
|
255
345
|
.join("\n");
|
|
256
346
|
ctx.ui.notify(`Agent profiles${current}:\n${list}`, "info");
|
|
257
347
|
return;
|
|
@@ -270,9 +360,39 @@ export default async function (pi: ExtensionAPI) {
|
|
|
270
360
|
handler: async (_args, ctx) => {
|
|
271
361
|
const current = activeAgent ? ` [active: ${activeAgent.name}]` : "";
|
|
272
362
|
const list = Object.entries(agents)
|
|
273
|
-
.map(([
|
|
363
|
+
.map(([name, profile]) => ` ${name}${profile.description ? ": " + profile.description : ""}`)
|
|
274
364
|
.join("\n");
|
|
275
365
|
ctx.ui.notify(`Agent profiles${current}:\n${list}`, "info");
|
|
276
366
|
},
|
|
277
367
|
});
|
|
368
|
+
|
|
369
|
+
/* ---- /agent-profile <name> — select a session-scoped runtime overlay ---- */
|
|
370
|
+
pi.registerCommand("agent-profile", {
|
|
371
|
+
description: "Select a session-scoped agent runtime profile (or 'none' to clear)",
|
|
372
|
+
getArgumentCompletions: (prefix) => {
|
|
373
|
+
const names = ["none", ...Object.keys(agentProfiles)];
|
|
374
|
+
const matches = names.filter((name) => name.startsWith(prefix ?? ""));
|
|
375
|
+
return matches.length
|
|
376
|
+
? matches.map((name) => ({
|
|
377
|
+
value: name,
|
|
378
|
+
label: name,
|
|
379
|
+
description: name === "none"
|
|
380
|
+
? "Clear the session agent profile"
|
|
381
|
+
: agentProfiles[name]?.description ?? "",
|
|
382
|
+
}))
|
|
383
|
+
: null;
|
|
384
|
+
},
|
|
385
|
+
handler: async (args, ctx) => {
|
|
386
|
+
const name = args?.trim();
|
|
387
|
+
if (!name) {
|
|
388
|
+
const current = activeAgentProfileName ?? "none";
|
|
389
|
+
const list = Object.entries(agentProfiles)
|
|
390
|
+
.map(([profileName, profile]) => ` ${profileName}${profile.description ? ": " + profile.description : ""}`)
|
|
391
|
+
.join("\n");
|
|
392
|
+
ctx.ui.notify(`Agent runtime profiles (active: ${current})${list ? `:\n${list}` : ""}`, "info");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
await selectAgentProfile(name === "none" ? undefined : name, ctx, true);
|
|
396
|
+
},
|
|
397
|
+
});
|
|
278
398
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dougbots/pi-agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Agent profiles for pi — preconfigured model, system prompt, tool restrictions, and permission gates",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --experimental-strip-types --test tests/*.test.mts"
|