@pi-kaush/pi-agent-mode 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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - Add `profile` frontmatter: activate a named model profile from the shared machine-local `~/.pi/agent/profiles.yaml` (`@pi-kaush/pi-model-profiles`), walking candidates in order and applying the first available model and thinking level.
6
+ - Reject agents declaring both `profile` and `model`; precedence is explicit: profile first, then model, then the session default.
7
+ - Load `@pi-kaush/pi-model-profiles` lazily so a missing library degrades only profile support; `/agent` remains fully functional without it.
8
+
3
9
  ## 0.1.0
4
10
 
5
11
  - Add the `/agent` command for activating a configured Pi agent as a persistent mode in the current session.
package/README.md CHANGED
@@ -5,14 +5,13 @@ the session to the agent's model, thinking level, and tool set, appends the agen
5
5
  prompt to Pi's base instructions, and restores the pre-activation baseline when cleared.
6
6
  It does not spawn children and provides no delegated subagents.
7
7
 
8
- ## Install
8
+ ### Install
9
9
 
10
- ```sh
10
+ ```fish
11
11
  pi install npm:@pi-kaush/pi-agent-mode
12
12
  ```
13
13
 
14
- Restart Pi or run `/reload`. To pin a specific release, append its version, such as
15
- `@0.1.0`.
14
+ Restart Pi or run `/reload`.
16
15
 
17
16
  ## Usage
18
17
 
@@ -37,11 +36,27 @@ description: ... # required
37
36
  emoji: 🔍 # optional, shown in the footer status
38
37
  tools: read, grep # optional allowlist; unavailable tools are reported and omitted
39
38
  model: provider/model:high # optional; thinking suffix may be :off/:low/:medium/:high/:max
39
+ profile: coder # optional; named model candidates from ~/.pi/agent/profiles.yaml
40
40
  confirmProjectAgents: false # project agents default to requiring confirmation
41
41
  ---
42
42
  Prompt body appended to Pi's base instructions while the agent is active.
43
43
  ```
44
44
 
45
+ ## Model selection precedence
46
+
47
+ Agent frontmatter may declare a `model` (exact model, optional thinking
48
+ suffix) or a `profile` — a named entry in the shared machine-local
49
+ `~/.pi/agent/profiles.yaml` maintained by
50
+ [@pi-kaush/pi-model-profiles](../pi-model-profiles). An agent must not declare
51
+ both. Profile candidates are walked in order at activation and the first
52
+ model that exists and is authenticated wins; once the session is running,
53
+ models never switch mid-request. Agents without either keep the current
54
+ session model.
55
+
56
+ `pi-agent-mode` loads `@pi-kaush/pi-model-profiles` lazily: if the library is
57
+ missing, only agents that declare `profile:` fail (with an install hint) and
58
+ every other `/agent` capability is unencumbered.
59
+
45
60
  ## Behavior
46
61
 
47
62
  - The first activation captures the current model, thinking level, and active
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-kaush/pi-agent-mode",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Activate a configured Pi agent as a persistent mode in the current session.",
5
5
  "license": "MIT",
6
6
  "author": "Kaushik Gopal",
@@ -33,6 +33,9 @@
33
33
  "./src/index.ts"
34
34
  ]
35
35
  },
36
+ "dependencies": {
37
+ "@pi-kaush/pi-model-profiles": "^0.1.0"
38
+ },
36
39
  "peerDependencies": {
37
40
  "@earendil-works/pi-coding-agent": ">=0.80.6",
38
41
  "@earendil-works/pi-tui": ">=0.80.6"
@@ -23,6 +23,7 @@ export interface AgentConfig {
23
23
  emoji?: string;
24
24
  tools?: string[];
25
25
  model?: string;
26
+ profile?: string;
26
27
  confirmProjectAgents?: boolean;
27
28
  systemPrompt: string;
28
29
  source: "user" | "project";
@@ -35,6 +36,7 @@ interface AgentFrontmatter extends Record<string, unknown> {
35
36
  emoji?: string;
36
37
  tools?: string;
37
38
  model?: string;
39
+ profile?: string;
38
40
  confirmProjectAgents?: boolean;
39
41
  }
40
42
 
@@ -105,6 +107,10 @@ function loadAgentsFromDir(
105
107
  : undefined;
106
108
  const model =
107
109
  typeof frontmatter.model === "string" ? frontmatter.model : undefined;
110
+ const profile =
111
+ typeof frontmatter.profile === "string"
112
+ ? frontmatter.profile.trim()
113
+ : undefined;
108
114
 
109
115
  agents.push({
110
116
  name,
@@ -112,6 +118,7 @@ function loadAgentsFromDir(
112
118
  ...(emoji ? { emoji } : {}),
113
119
  ...(tools && tools.length > 0 ? { tools } : {}),
114
120
  ...(model ? { model } : {}),
121
+ ...(profile ? { profile } : {}),
115
122
  ...(typeof frontmatter.confirmProjectAgents === "boolean"
116
123
  ? { confirmProjectAgents: frontmatter.confirmProjectAgents }
117
124
  : {}),
package/src/index.ts CHANGED
@@ -14,6 +14,8 @@ import type {
14
14
  ExtensionAPI,
15
15
  ExtensionContext,
16
16
  } from "@earendil-works/pi-coding-agent";
17
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
18
+ import type { ModelProfilesConfig } from "@pi-kaush/pi-model-profiles";
17
19
  import {
18
20
  discoverAgents,
19
21
  formatAgentDisplayName,
@@ -32,7 +34,7 @@ interface ModelReference {
32
34
  id: string;
33
35
  }
34
36
 
35
- export { parseAgentModelSpec };
37
+ export { isModuleUnavailable, parseAgentModelSpec };
36
38
  export type { AgentModelSpec } from "./model-spec.ts";
37
39
 
38
40
  // Durable session state is namespaced so it cannot collide with other
@@ -99,6 +101,15 @@ function isActiveAgentState(value: unknown): value is ActiveAgentState {
99
101
  return true;
100
102
  }
101
103
 
104
+ function isModuleUnavailable(error: unknown): boolean {
105
+ const code = (error as { code?: string } | undefined)?.code;
106
+ return (
107
+ code === "MODULE_NOT_FOUND" ||
108
+ code === "ERR_MODULE_NOT_FOUND" ||
109
+ (error instanceof Error && error.message.includes("Cannot find module"))
110
+ );
111
+ }
112
+
102
113
  export function registerAgentMode(pi: ExtensionAPI) {
103
114
  let activeAgent: ActiveAgent | undefined;
104
115
 
@@ -200,6 +211,67 @@ export function registerAgentMode(pi: ExtensionAPI) {
200
211
  pi.setActiveTools(valid);
201
212
  }
202
213
 
214
+ /**
215
+ * Walk the agent's profile candidates in order and switch to the first
216
+ * model that exists and is authenticated. Fallback happens only during
217
+ * activation; running sessions never switch models mid-request.
218
+ */
219
+ async function applyProfileModels(
220
+ agent: AgentConfig,
221
+ ctx: ExtensionContext,
222
+ ): Promise<{ thinkingLevel?: ThinkingLevel } | false> {
223
+ let profiles: ModelProfilesConfig;
224
+ try {
225
+ // Runtime-only import: a missing @pi-kaush/pi-model-profiles degrades
226
+ // just profile support instead of failing the whole extension load.
227
+ const profilesModule = await import("@pi-kaush/pi-model-profiles");
228
+ profiles = profilesModule.loadModelProfiles(
229
+ profilesModule.resolveProfilesPath(getAgentDir()),
230
+ );
231
+ } catch (error) {
232
+ if (isModuleUnavailable(error)) {
233
+ ctx.ui.notify(
234
+ `Agent ${formatAgentDisplayName(agent)}: profile support unavailable; install it with pi install npm:@pi-kaush/pi-model-profiles.`,
235
+ "warning",
236
+ );
237
+ } else {
238
+ ctx.ui.notify(
239
+ `Agent ${formatAgentDisplayName(agent)}: ${error instanceof Error ? error.message : error}`,
240
+ "error",
241
+ );
242
+ }
243
+ return false;
244
+ }
245
+
246
+ const profileName = agent.profile!.trim();
247
+ const profile = profiles.profiles[profileName];
248
+ if (!profile) {
249
+ ctx.ui.notify(
250
+ `Agent ${formatAgentDisplayName(agent)}: unknown profile "${profileName}". Available: ${Object.keys(profiles.profiles).join(", ")}.`,
251
+ "error",
252
+ );
253
+ return false;
254
+ }
255
+
256
+ for (const candidate of profile.candidates) {
257
+ const model = findModel(candidate.model, ctx);
258
+ if (!model) continue;
259
+ if (await pi.setModel(model)) {
260
+ return {
261
+ ...(candidate.thinkingLevel !== undefined
262
+ ? { thinkingLevel: candidate.thinkingLevel }
263
+ : {}),
264
+ };
265
+ }
266
+ }
267
+
268
+ ctx.ui.notify(
269
+ `Agent ${formatAgentDisplayName(agent)}: profile "${profileName}" has no available candidate models. Configured: ${profile.candidates.map((candidate) => candidate.model).join(", ")}.`,
270
+ "error",
271
+ );
272
+ return false;
273
+ }
274
+
203
275
  async function activateAgent(
204
276
  agent: AgentConfig,
205
277
  ctx: ExtensionContext,
@@ -211,7 +283,18 @@ export function registerAgentMode(pi: ExtensionAPI) {
211
283
  thinkingLevel: baseline.thinkingLevel ?? pi.getThinkingLevel(),
212
284
  };
213
285
  let thinkingLevel = baseline.thinkingLevel;
214
- if (agent.model) {
286
+ if (agent.profile && agent.model) {
287
+ ctx.ui.notify(
288
+ `Agent ${formatAgentDisplayName(agent)}: declare either "profile" or "model" in frontmatter, not both.`,
289
+ "error",
290
+ );
291
+ return false;
292
+ }
293
+ if (agent.profile) {
294
+ const applied = await applyProfileModels(agent, ctx);
295
+ if (!applied) return false;
296
+ thinkingLevel = applied.thinkingLevel ?? baseline.thinkingLevel;
297
+ } else if (agent.model) {
215
298
  const modelSpec = parseAgentModelSpec(agent.model);
216
299
  const model = findModel(modelSpec.model, ctx);
217
300
  if (!model) {