@gotgenes/pi-subagents 5.2.0 → 5.4.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.
@@ -48,12 +48,6 @@ export function resolveType(name: string): string | undefined {
48
48
  return resolveKey(name);
49
49
  }
50
50
 
51
- /** Get the agent config for a type (case-insensitive). */
52
- export function getAgentConfig(name: string): AgentConfig | undefined {
53
- const key = resolveKey(name);
54
- return key ? agents.get(key) : undefined;
55
- }
56
-
57
51
  /** Get all enabled type names (for spawning and tool descriptions). */
58
52
  export function getAvailableTypes(): string[] {
59
53
  return [...agents.entries()]
@@ -116,49 +110,29 @@ export function getToolNamesForType(type: string): string[] {
116
110
  return names;
117
111
  }
118
112
 
119
- /** Get config for a type (case-insensitive, returns a SubagentTypeConfig-compatible object). Falls back to general-purpose. */
120
- export function getConfig(type: string): {
121
- displayName: string;
122
- description: string;
123
- builtinToolNames: string[];
124
- extensions: true | string[] | false;
125
- skills: true | string[] | false;
126
- promptMode: "replace" | "append";
127
- } {
113
+ /** Resolve agent config with guaranteed non-null return. Falls back: unknown general-purpose → absolute fallback. */
114
+ export function resolveAgentConfig(type: string): AgentConfig {
128
115
  const key = resolveKey(type);
129
116
  const config = key ? agents.get(key) : undefined;
130
- if (config && config.enabled !== false) {
131
- return {
132
- displayName: config.displayName ?? config.name,
133
- description: config.description,
134
- builtinToolNames: config.builtinToolNames ?? BUILTIN_TOOL_NAMES,
135
- extensions: config.extensions,
136
- skills: config.skills,
137
- promptMode: config.promptMode,
138
- };
117
+ if (config) {
118
+ return config;
139
119
  }
140
120
 
141
- // Fallback for unknown/disabled types — general-purpose config
121
+ // Fallback to general-purpose for unknown types
142
122
  const gp = agents.get("general-purpose");
143
- if (gp && gp.enabled !== false) {
144
- return {
145
- displayName: gp.displayName ?? gp.name,
146
- description: gp.description,
147
- builtinToolNames: gp.builtinToolNames ?? BUILTIN_TOOL_NAMES,
148
- extensions: gp.extensions,
149
- skills: gp.skills,
150
- promptMode: gp.promptMode,
151
- };
123
+ if (gp) {
124
+ return gp;
152
125
  }
153
126
 
154
- // Absolute fallback (should never happen)
127
+ // Absolute fallback (should never happen in practice)
155
128
  return {
129
+ name: type,
156
130
  displayName: "Agent",
157
131
  description: "General-purpose agent for complex, multi-step tasks",
158
132
  builtinToolNames: BUILTIN_TOOL_NAMES,
159
133
  extensions: true,
160
134
  skills: true,
135
+ systemPrompt: "",
161
136
  promptMode: "append",
162
137
  };
163
138
  }
164
-
package/src/index.ts CHANGED
@@ -14,7 +14,7 @@ import { join } from "node:path";
14
14
  import { defineTool, type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
15
15
  import { AgentManager } from "./agent-manager.js";
16
16
  import { getAgentConversation, normalizeMaxTurns, steerAgent } from "./agent-runner.js";
17
- import { getAgentConfig, getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, } from "./agent-types.js";
17
+ import { getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveAgentConfig, } from "./agent-types.js";
18
18
  import { loadCustomAgents } from "./custom-agents.js";
19
19
  import { type ModelRegistry, resolveModel } from "./model-resolver.js";
20
20
  import { buildEventData, createNotificationSystem } from "./notification.js";
@@ -149,14 +149,14 @@ export default function (pi: ExtensionAPI) {
149
149
  const userNames = getUserAgentNames();
150
150
 
151
151
  const defaultDescs = defaultNames.map((name) => {
152
- const cfg = getAgentConfig(name);
153
- const modelSuffix = cfg?.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
154
- return `- ${name}: ${cfg?.description ?? name}${modelSuffix}`;
152
+ const cfg = resolveAgentConfig(name);
153
+ const modelSuffix = cfg.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
154
+ return `- ${name}: ${cfg.description}${modelSuffix}`;
155
155
  });
156
156
 
157
157
  const customDescs = userNames.map((name) => {
158
- const cfg = getAgentConfig(name);
159
- return `- ${name}: ${cfg?.description ?? name}`;
158
+ const cfg = resolveAgentConfig(name);
159
+ return `- ${name}: ${cfg.description}`;
160
160
  });
161
161
 
162
162
  return [
@@ -237,8 +237,8 @@ export default function (pi: ExtensionAPI) {
237
237
  reloadCustomAgents,
238
238
  agentActivity: runtime.agentActivity,
239
239
  getModelLabel: (type, registry) => {
240
- const cfg = getAgentConfig(type);
241
- if (!cfg?.model) return 'inherit';
240
+ const cfg = resolveAgentConfig(type);
241
+ if (!cfg.model) return 'inherit';
242
242
  if (registry) {
243
243
  const resolved = resolveModel(cfg.model, registry as any);
244
244
  if (typeof resolved === 'string') return 'inherit';
@@ -0,0 +1,243 @@
1
+ /**
2
+ * session-config.ts — Pure configuration assembler for agent sessions.
3
+ *
4
+ * `assembleSessionConfig()` is the pure core extracted from `runAgent()`.
5
+ * It accepts resolved inputs (agent type, narrow context, run options, env info)
6
+ * and returns everything `runAgent()` needs to create the SDK session — without
7
+ * importing or constructing any Pi SDK types.
8
+ *
9
+ * The only async IO in the assembly phase (`detectEnv`) is handled by the caller
10
+ * before invoking this function, keeping the assembler synchronous.
11
+ */
12
+
13
+ import {
14
+ getMemoryToolNames,
15
+ getReadOnlyMemoryToolNames,
16
+ getToolNamesForType,
17
+ resolveAgentConfig,
18
+ } from "./agent-types.js";
19
+ import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
20
+ import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
21
+ import { preloadSkills } from "./skill-loader.js";
22
+ import type { EnvInfo, SubagentType, ThinkingLevel } from "./types.js";
23
+
24
+ // ── Public interfaces ────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Narrow context the assembler reads from the parent session.
28
+ * Tests construct plain objects satisfying this interface — no SDK mocking needed.
29
+ *
30
+ * Models are treated as opaque handles: the assembler never inspects their
31
+ * internals, only passes them through. `getAvailable` returns just enough
32
+ * structural information ({ provider, id }) for the availability check in
33
+ * `resolveDefaultModel`.
34
+ */
35
+ export interface AssemblerContext {
36
+ /** Parent working directory (overridable via options.cwd). */
37
+ cwd: string;
38
+ /** Parent's effective system prompt (for append-mode agents). */
39
+ parentSystemPrompt: string;
40
+ /** Parent's current model instance (fallback when agent config has no model). */
41
+ parentModel?: unknown;
42
+ /** Model registry for resolving config.model strings. */
43
+ modelRegistry: {
44
+ find(provider: string, modelId: string): unknown;
45
+ getAvailable?(): Array<{ provider: string; id: string }>;
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Narrow slice of RunOptions consumed by the assembler.
51
+ * All fields are optional — callers pass only what they have.
52
+ */
53
+ export interface AssemblerOptions {
54
+ /** Override working directory (e.g. for worktree isolation). */
55
+ cwd?: string;
56
+ /** When true, forces extensions and skills to false. */
57
+ isolated?: boolean;
58
+ /** Explicit model override — wins over agentConfig.model and parent model. */
59
+ model?: unknown;
60
+ /** Explicit thinking level — wins over agentConfig.thinking. */
61
+ thinkingLevel?: ThinkingLevel;
62
+ }
63
+
64
+ /**
65
+ * Assembled configuration returned to `runAgent()`.
66
+ * Contains everything needed to create the SDK session and filter tools —
67
+ * with no SDK object references.
68
+ */
69
+ export interface SessionConfig {
70
+ /** Resolved working directory (`options.cwd ?? ctx.cwd`). */
71
+ effectiveCwd: string;
72
+ /** Fully-assembled system prompt string (ready for `systemPromptOverride`). */
73
+ systemPrompt: string;
74
+ /** Built-in tool names for session creation, filtering, and memory augmentation. */
75
+ toolNames: string[];
76
+ /** Disallowed tool set from agentConfig (for `filterActiveTools`). undefined when empty. */
77
+ disallowedSet: Set<string> | undefined;
78
+ /** Resolved extensions setting for resource loader and tool filtering. */
79
+ extensions: boolean | string[];
80
+ /**
81
+ * Resolved model instance (undefined → use parent model as passed to SDK).
82
+ * Opaque handle — the assembler passes it through without inspection.
83
+ * Caller casts to the SDK’s Model<any> at the session-creation boundary.
84
+ */
85
+ model: unknown;
86
+ /** Resolved thinking level (undefined → inherit from session). */
87
+ thinkingLevel: ThinkingLevel | undefined;
88
+ /** Whether to skip skill loading in the resource loader (`noSkills` flag). */
89
+ noSkills: boolean;
90
+ /** Prompt extras (memory block, preloaded skill blocks) — for transparency. */
91
+ extras: PromptExtras;
92
+ /** Per-agent configured max turns (from agentConfig.maxTurns). */
93
+ agentMaxTurns: number | undefined;
94
+ }
95
+
96
+ // ── Internal helpers ─────────────────────────────────────────────────────────
97
+
98
+ /**
99
+ * Resolve the default model from the agent config's model string.
100
+ *
101
+ * Priority: parentModel is the fallback; if `configModel` is a "provider/modelId"
102
+ * string that resolves against the registry AND is in the available set, return
103
+ * that model instead.
104
+ */
105
+ function resolveDefaultModel(
106
+ parentModel: unknown,
107
+ registry: AssemblerContext["modelRegistry"],
108
+ configModel?: string,
109
+ ): unknown {
110
+ if (configModel) {
111
+ const slashIdx = configModel.indexOf("/");
112
+ if (slashIdx !== -1) {
113
+ const provider = configModel.slice(0, slashIdx);
114
+ const modelId = configModel.slice(slashIdx + 1);
115
+
116
+ const available = registry.getAvailable?.();
117
+ const availableKeys = available
118
+ ? new Set(available.map((m) => `${m.provider}/${m.id}`))
119
+ : undefined;
120
+ const isAvailable = (p: string, id: string) =>
121
+ !availableKeys || availableKeys.has(`${p}/${id}`);
122
+
123
+ const found = registry.find(provider, modelId);
124
+ if (found && isAvailable(provider, modelId)) return found;
125
+ }
126
+ }
127
+ return parentModel;
128
+ }
129
+
130
+ // ── Public function ──────────────────────────────────────────────────────────
131
+
132
+ /**
133
+ * Assemble all configuration needed to create an agent session.
134
+ *
135
+ * Synchronous and side-effect-free (beyond calling `preloadSkills` which reads
136
+ * the filesystem). The caller is responsible for resolving `EnvInfo` beforehand
137
+ * via `detectEnv()`.
138
+ *
139
+ * @param type The subagent type name (case-insensitive registry lookup).
140
+ * @param ctx Narrow context from the parent session.
141
+ * @param options Per-call overrides (cwd, isolated, model, thinkingLevel).
142
+ * @param env Pre-resolved environment info from `detectEnv()`.
143
+ */
144
+ export function assembleSessionConfig(
145
+ type: SubagentType,
146
+ ctx: AssemblerContext,
147
+ options: AssemblerOptions,
148
+ env: EnvInfo,
149
+ ): SessionConfig {
150
+ const agentConfig = resolveAgentConfig(type);
151
+
152
+ const effectiveCwd = options.cwd ?? ctx.cwd;
153
+
154
+ // Resolve extensions/skills: isolated overrides to false
155
+ const extensions = options.isolated ? false : agentConfig.extensions;
156
+ const skills = options.isolated ? false : agentConfig.skills;
157
+
158
+ // Build prompt extras (memory, preloaded skills)
159
+ const extras: PromptExtras = {};
160
+
161
+ // Skill preloading: when skills is string[], preload their content into the prompt
162
+ if (Array.isArray(skills)) {
163
+ const loaded = preloadSkills(skills, effectiveCwd);
164
+ if (loaded.length > 0) {
165
+ extras.skillBlocks = loaded;
166
+ }
167
+ }
168
+
169
+ let toolNames = getToolNamesForType(type);
170
+
171
+ // Persistent memory: detect write capability and branch accordingly.
172
+ // Account for disallowedTools — a tool in the base set but on the denylist
173
+ // is not truly available.
174
+ if (agentConfig.memory) {
175
+ const existingNames = new Set(toolNames);
176
+ const denied = agentConfig.disallowedTools
177
+ ? new Set(agentConfig.disallowedTools)
178
+ : undefined;
179
+ const effectivelyHas = (name: string) =>
180
+ existingNames.has(name) && !denied?.has(name);
181
+ const hasWriteTools = effectivelyHas("write") || effectivelyHas("edit");
182
+
183
+ if (hasWriteTools) {
184
+ const extraNames = getMemoryToolNames(existingNames);
185
+ if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
186
+ extras.memoryBlock = buildMemoryBlock(
187
+ agentConfig.name,
188
+ agentConfig.memory,
189
+ effectiveCwd,
190
+ );
191
+ } else {
192
+ const extraNames = getReadOnlyMemoryToolNames(existingNames);
193
+ if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
194
+ extras.memoryBlock = buildReadOnlyMemoryBlock(
195
+ agentConfig.name,
196
+ agentConfig.memory,
197
+ effectiveCwd,
198
+ );
199
+ }
200
+ }
201
+
202
+ // Build system prompt from the resolved agent config
203
+ const systemPrompt = buildAgentPrompt(
204
+ agentConfig,
205
+ effectiveCwd,
206
+ env,
207
+ ctx.parentSystemPrompt,
208
+ extras,
209
+ );
210
+
211
+ // noSkills: when we've already preloaded skills into the prompt, or skills = false,
212
+ // tell the resource loader not to load them again.
213
+ const noSkills = skills === false || Array.isArray(skills);
214
+
215
+ // Disallowed tools set (for filterActiveTools in runAgent)
216
+ const disallowedSet = agentConfig.disallowedTools
217
+ ? new Set(agentConfig.disallowedTools)
218
+ : undefined;
219
+
220
+ // Model resolution: explicit option > config model string > parent model
221
+ const model =
222
+ options.model ??
223
+ resolveDefaultModel(ctx.parentModel, ctx.modelRegistry, agentConfig.model);
224
+
225
+ // Thinking level: explicit option > agent config > undefined (inherit)
226
+ const thinkingLevel = options.thinkingLevel ?? agentConfig.thinking;
227
+
228
+ // Per-agent max turns (combined with options.maxTurns and defaultMaxTurns by runAgent)
229
+ const agentMaxTurns = agentConfig.maxTurns;
230
+
231
+ return {
232
+ effectiveCwd,
233
+ systemPrompt,
234
+ toolNames,
235
+ disallowedSet,
236
+ extensions,
237
+ model,
238
+ thinkingLevel,
239
+ noSkills,
240
+ extras,
241
+ agentMaxTurns,
242
+ };
243
+ }
@@ -1,7 +1,7 @@
1
1
  import { Text } from "@earendil-works/pi-tui";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import { normalizeMaxTurns } from "../agent-runner.js";
4
- import { getAgentConfig, resolveType } from "../agent-types.js";
4
+ import { resolveAgentConfig, resolveType } from "../agent-types.js";
5
5
  import { resolveAgentInvocationConfig } from "../invocation-config.js";
6
6
  import { resolveInvocationModel } from "../model-resolver.js";
7
7
  import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "../output-file.js";
@@ -370,8 +370,8 @@ Guidelines:
370
370
 
371
371
  const displayName = getDisplayName(subagentType);
372
372
 
373
- // Get agent config (if any)
374
- const customConfig = getAgentConfig(subagentType);
373
+ // Get agent config for invocation resolution
374
+ const customConfig = resolveAgentConfig(subagentType);
375
375
 
376
376
  const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
377
377
 
@@ -3,8 +3,9 @@ import { join } from "node:path";
3
3
 
4
4
  import {
5
5
  BUILTIN_TOOL_NAMES,
6
- getAgentConfig,
7
6
  getAllTypes,
7
+ resolveAgentConfig,
8
+ resolveType,
8
9
  } from "../agent-types.js";
9
10
  import type { AgentConfig, AgentRecord } from "../types.js";
10
11
  import type { AgentActivity } from "./agent-widget.js";
@@ -152,21 +153,21 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
152
153
  };
153
154
 
154
155
  const entries = allNames.map((name) => {
155
- const cfg = getAgentConfig(name);
156
- const disabled = cfg?.enabled === false;
156
+ const cfg = resolveAgentConfig(name);
157
+ const disabled = cfg.enabled === false;
157
158
  const model = deps.getModelLabel(name, ctx.modelRegistry);
158
159
  const indicator = sourceIndicator(cfg);
159
160
  const prefix = `${indicator}${name} · ${model}`;
160
- const desc = disabled ? "(disabled)" : (cfg?.description ?? name);
161
+ const desc = disabled ? "(disabled)" : cfg.description;
161
162
  return { name, prefix, desc };
162
163
  });
163
164
  const maxPrefix = Math.max(...entries.map((e) => e.prefix.length));
164
165
 
165
166
  const hasCustom = allNames.some((n) => {
166
- const c = getAgentConfig(n);
167
- return c && !c.isDefault && c.enabled !== false;
167
+ const c = resolveAgentConfig(n);
168
+ return !c.isDefault && c.enabled !== false;
168
169
  });
169
- const hasDisabled = allNames.some((n) => getAgentConfig(n)?.enabled === false);
170
+ const hasDisabled = allNames.some((n) => resolveAgentConfig(n).enabled === false);
170
171
  const legendParts: string[] = [];
171
172
  if (hasCustom) legendParts.push("• = project ◦ = global");
172
173
  if (hasDisabled) legendParts.push("✕ = disabled");
@@ -184,7 +185,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
184
185
  .split(" · ")[0]
185
186
  .replace(/^[•◦✕\s]+/, "")
186
187
  .trim();
187
- if (getAgentConfig(agentName)) {
188
+ if (resolveType(agentName) != null) {
188
189
  await showAgentDetail(ctx, agentName);
189
190
  await showAllAgentsList(ctx);
190
191
  }
@@ -245,11 +246,11 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
245
246
  }
246
247
 
247
248
  async function showAgentDetail(ctx: MenuContext, name: string) {
248
- const cfg = getAgentConfig(name);
249
- if (!cfg) {
249
+ if (resolveType(name) == null) {
250
250
  ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
251
251
  return;
252
252
  }
253
+ const cfg = resolveAgentConfig(name);
253
254
 
254
255
  const file = findAgentFile(name);
255
256
  const isDefault = cfg.isDefault === true;
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { truncateToWidth } from "@earendil-works/pi-tui";
9
9
  import type { AgentManager } from "../agent-manager.js";
10
- import { getConfig } from "../agent-types.js";
10
+ import { resolveAgentConfig } from "../agent-types.js";
11
11
  import type { AgentInvocation, SubagentType } from "../types.js";
12
12
  import { getLifetimeTotal, getSessionContextPercent, type LifetimeUsage, type SessionLike } from "../usage.js";
13
13
 
@@ -144,12 +144,13 @@ export function formatDuration(startedAt: number, completedAt?: number): string
144
144
 
145
145
  /** Get display name for any agent type (built-in or custom). */
146
146
  export function getDisplayName(type: SubagentType): string {
147
- return getConfig(type).displayName;
147
+ const config = resolveAgentConfig(type);
148
+ return config.displayName ?? config.name;
148
149
  }
149
150
 
150
151
  /** Short label for prompt mode: "twin" for append, nothing for replace (the default). */
151
152
  export function getPromptModeLabel(type: SubagentType): string | undefined {
152
- const config = getConfig(type);
153
+ const config = resolveAgentConfig(type);
153
154
  return config.promptMode === "append" ? "twin" : undefined;
154
155
  }
155
156