@tintinweb/pi-subagents 0.7.2 → 0.7.3

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
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.3] - 2026-05-14
11
+
12
+ ### Added
13
+ - **`<active_agent name="…"/>` tag prepended to every child system prompt** ([#73](https://github.com/tintinweb/pi-subagents/pull/73) — thanks [@chris-lasher](https://github.com/chris-lasher)). `buildAgentPrompt` now emits `<active_agent name="${config.name}"/>` as the first line of the assembled prompt in both `replace` and `append` modes, before the env block. Downstream extensions (e.g. permission/policy systems) can parse it from inside the child session to resolve per-agent policy. The tag uses the agent's `config.name` verbatim — no escaping or normalization — and does not couple this extension to any specific downstream consumer; ignoring it is harmless.
14
+
15
+ ### Changed
16
+ - **Subagent sessions now get a stable, type-derived name with an id suffix for parallel spawns** ([#51](https://github.com/tintinweb/pi-subagents/pull/51) — thanks [@forcepushdev](https://github.com/forcepushdev)). `runAgent` calls `session.setSessionName(agentConfig?.name ?? type)`, and when the manager assigns an `agentId` (always, in production), the name is suffixed with an 8-char slice — e.g. `Explore#a1b2c3d4` — so concurrent spawns of the same agent type are distinguishable in the overlay instead of all collapsing onto the same bare name. Direct `runAgent` callers without an `agentId` (e.g. tests) get the bare name.
17
+
18
+ ### Fixed
19
+ - **Cross-extension spawn RPC now accepts a string `options.model`** ([#59](https://github.com/tintinweb/pi-subagents/pull/59), fixes [#60](https://github.com/tintinweb/pi-subagents/issues/60)). Cross-extension callers (e.g. `@tintinweb/pi-tasks@>=0.4.3`'s `TaskExecute`) naturally forward `model` as a serializable `"provider/modelId"` string. Previously the spawn handler passed strings straight through to `runAgent()`, which expects a `Model` object — the spawned agent then crashed with `No API key found for undefined`. The handler now resolves strings via the same `resolveModel(ctx.modelRegistry)` path the scheduler uses; `Model` objects pass through unchanged. Unresolved strings surface the human-readable `Model not found: "…"` error instead of the auth-lookup crash. Thanks @any-victor.
20
+
10
21
  ## [0.7.2] - 2026-05-12
11
22
 
12
23
  > **Heads-up — behavior changes in skill preloading:**
package/README.md CHANGED
@@ -406,6 +406,8 @@ pi.events.emit("subagents:rpc:spawn", {
406
406
  });
407
407
  ```
408
408
 
409
+ `options.model` accepts either a `Model` object (e.g. `ctx.model`) or a `"provider/modelId"` string — strings are resolved against `ctx.modelRegistry` at the RPC boundary, so cross-extension callers can forward serializable values without losing auth context.
410
+
409
411
  ### Stop
410
412
 
411
413
  Stop a running agent by ID:
@@ -107,6 +107,7 @@ export class AgentManager {
107
107
  const detach = () => { detachParentSignal?.(); detachParentSignal = undefined; };
108
108
  const promise = runAgent(ctx, type, prompt, {
109
109
  pi,
110
+ agentId: id,
110
111
  model: options.model,
111
112
  maxTurns: options.maxTurns,
112
113
  isolated: options.isolated,
@@ -23,6 +23,8 @@ export interface ToolActivity {
23
23
  export interface RunOptions {
24
24
  /** ExtensionAPI instance — used for pi.exec() instead of execSync. */
25
25
  pi: ExtensionAPI;
26
+ /** Manager-assigned id; suffixes session name to disambiguate parallel spawns (e.g. `Explore#a1b2c3d4`). */
27
+ agentId?: string;
26
28
  model?: Model<any>;
27
29
  maxTurns?: number;
28
30
  signal?: AbortSignal;
@@ -187,6 +187,8 @@ export async function runAgent(ctx, type, prompt, options) {
187
187
  sessionOpts.thinkingLevel = thinkingLevel;
188
188
  }
189
189
  const { session } = await createAgentSession(sessionOpts);
190
+ const baseSessionName = agentConfig?.name ?? type;
191
+ session.setSessionName(options.agentId ? `${baseSessionName}#${options.agentId.slice(0, 8)}` : baseSessionName);
190
192
  // Build disallowed tools set from agent config
191
193
  const disallowedSet = agentConfig?.disallowedTools
192
194
  ? new Set(agentConfig.disallowedTools)
@@ -8,6 +8,7 @@
8
8
  * success → { success: true, data?: T }
9
9
  * error → { success: false, error: string }
10
10
  */
11
+ import { resolveModel } from "./model-resolver.js";
11
12
  /** RPC protocol version — bumped when the envelope or method contracts change. */
12
13
  export const PROTOCOL_VERSION = 2;
13
14
  /**
@@ -44,7 +45,28 @@ export function registerRpcHandlers(deps) {
44
45
  const ctx = getCtx();
45
46
  if (!ctx)
46
47
  throw new Error("No active session");
47
- return { id: manager.spawn(pi, ctx, type, prompt, options ?? {}) };
48
+ // Cross-extension RPC callers (e.g. pi-tasks TaskExecute) naturally
49
+ // forward serializable values, so options.model can be a string like
50
+ // "openai-codex/gpt-5.5". Resolve it to a real Model instance here
51
+ // — same pattern the scheduler path already uses — so the spawned
52
+ // agent's auth lookup doesn't crash with "No API key found for
53
+ // undefined".
54
+ let normalizedOptions = options ?? {};
55
+ if (typeof normalizedOptions.model === "string") {
56
+ const registry = ctx.modelRegistry;
57
+ if (!registry) {
58
+ throw new Error(`Model override "${normalizedOptions.model}" provided but ctx.modelRegistry is unavailable`);
59
+ }
60
+ const resolved = resolveModel(normalizedOptions.model, registry);
61
+ if (typeof resolved === "string") {
62
+ // resolveModel returns a human-readable error string when the
63
+ // input doesn't match any available model. Surface it instead of
64
+ // silently falling back so the caller sees the auth/typo issue.
65
+ throw new Error(resolved);
66
+ }
67
+ normalizedOptions = { ...normalizedOptions, model: resolved };
68
+ }
69
+ return { id: manager.spawn(pi, ctx, type, prompt, normalizedOptions) };
48
70
  });
49
71
  const unsubStop = handleRpc(events, "subagents:rpc:stop", ({ agentId }) => {
50
72
  if (!manager.abort(agentId))
package/dist/prompts.d.ts CHANGED
@@ -19,6 +19,10 @@ export interface PromptExtras {
19
19
  * - "append" mode: env header + parent system prompt + sub-agent context + config.systemPrompt
20
20
  * - "append" with empty systemPrompt: pure parent clone
21
21
  *
22
+ * Both modes prepend an `<active_agent name="${config.name}"/>` tag so downstream
23
+ * extensions (e.g. permission/policy systems) can resolve per-agent policy
24
+ * inside the child session by parsing the system prompt.
25
+ *
22
26
  * @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
23
27
  * @param extras Optional extra sections to inject (memory, preloaded skills).
24
28
  */
package/dist/prompts.js CHANGED
@@ -8,10 +8,15 @@
8
8
  * - "append" mode: env header + parent system prompt + sub-agent context + config.systemPrompt
9
9
  * - "append" with empty systemPrompt: pure parent clone
10
10
  *
11
+ * Both modes prepend an `<active_agent name="${config.name}"/>` tag so downstream
12
+ * extensions (e.g. permission/policy systems) can resolve per-agent policy
13
+ * inside the child session by parsing the system prompt.
14
+ *
11
15
  * @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
12
16
  * @param extras Optional extra sections to inject (memory, preloaded skills).
13
17
  */
14
18
  export function buildAgentPrompt(config, cwd, env, parentSystemPrompt, extras) {
19
+ const activeAgentTag = `<active_agent name="${config.name}"/>\n\n`;
15
20
  const envBlock = `# Environment
16
21
  Working directory: ${cwd}
17
22
  ${env.isGitRepo ? `Git repository: yes\nBranch: ${env.branch}` : "Not a git repository"}
@@ -44,14 +49,14 @@ You are operating as a sub-agent invoked to handle a specific task.
44
49
  const customSection = config.systemPrompt?.trim()
45
50
  ? `\n\n<agent_instructions>\n${config.systemPrompt}\n</agent_instructions>`
46
51
  : "";
47
- return envBlock + "\n\n<inherited_system_prompt>\n" + identity + "\n</inherited_system_prompt>\n\n" + bridge + customSection + extrasSuffix;
52
+ return activeAgentTag + envBlock + "\n\n<inherited_system_prompt>\n" + identity + "\n</inherited_system_prompt>\n\n" + bridge + customSection + extrasSuffix;
48
53
  }
49
54
  // "replace" mode — env header + the config's full system prompt
50
55
  const replaceHeader = `You are a pi coding agent sub-agent.
51
56
  You have been invoked to handle a specific task autonomously.
52
57
 
53
58
  ${envBlock}`;
54
- return replaceHeader + "\n\n" + config.systemPrompt + extrasSuffix;
59
+ return activeAgentTag + replaceHeader + "\n\n" + config.systemPrompt + extrasSuffix;
55
60
  }
56
61
  /** Fallback base prompt when parent system prompt is unavailable in append mode. */
57
62
  const genericBase = `# Role
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tintinweb/pi-subagents",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
5
5
  "author": "tintinweb",
6
6
  "license": "MIT",
@@ -183,6 +183,7 @@ export class AgentManager {
183
183
 
184
184
  const promise = runAgent(ctx, type, prompt, {
185
185
  pi,
186
+ agentId: id,
186
187
  model: options.model,
187
188
  maxTurns: options.maxTurns,
188
189
  isolated: options.isolated,
@@ -88,6 +88,8 @@ export interface ToolActivity {
88
88
  export interface RunOptions {
89
89
  /** ExtensionAPI instance — used for pi.exec() instead of execSync. */
90
90
  pi: ExtensionAPI;
91
+ /** Manager-assigned id; suffixes session name to disambiguate parallel spawns (e.g. `Explore#a1b2c3d4`). */
92
+ agentId?: string;
91
93
  model?: Model<any>;
92
94
  maxTurns?: number;
93
95
  signal?: AbortSignal;
@@ -280,6 +282,11 @@ export async function runAgent(
280
282
 
281
283
  const { session } = await createAgentSession(sessionOpts);
282
284
 
285
+ const baseSessionName = agentConfig?.name ?? type;
286
+ session.setSessionName(
287
+ options.agentId ? `${baseSessionName}#${options.agentId.slice(0, 8)}` : baseSessionName,
288
+ );
289
+
283
290
  // Build disallowed tools set from agent config
284
291
  const disallowedSet = agentConfig?.disallowedTools
285
292
  ? new Set(agentConfig.disallowedTools)
@@ -9,6 +9,8 @@
9
9
  * error → { success: false, error: string }
10
10
  */
11
11
 
12
+ import { type ModelRegistry, resolveModel } from "./model-resolver.js";
13
+
12
14
  /** Minimal event bus interface needed by the RPC handlers. */
13
15
  export interface EventBus {
14
16
  on(event: string, handler: (data: unknown) => void): () => void;
@@ -81,7 +83,32 @@ export function registerRpcHandlers(deps: RpcDeps): RpcHandle {
81
83
  events, "subagents:rpc:spawn", ({ type, prompt, options }) => {
82
84
  const ctx = getCtx();
83
85
  if (!ctx) throw new Error("No active session");
84
- return { id: manager.spawn(pi, ctx, type, prompt, options ?? {}) };
86
+
87
+ // Cross-extension RPC callers (e.g. pi-tasks TaskExecute) naturally
88
+ // forward serializable values, so options.model can be a string like
89
+ // "openai-codex/gpt-5.5". Resolve it to a real Model instance here
90
+ // — same pattern the scheduler path already uses — so the spawned
91
+ // agent's auth lookup doesn't crash with "No API key found for
92
+ // undefined".
93
+ let normalizedOptions = options ?? {};
94
+ if (typeof normalizedOptions.model === "string") {
95
+ const registry = (ctx as { modelRegistry?: ModelRegistry }).modelRegistry;
96
+ if (!registry) {
97
+ throw new Error(
98
+ `Model override "${normalizedOptions.model}" provided but ctx.modelRegistry is unavailable`,
99
+ );
100
+ }
101
+ const resolved = resolveModel(normalizedOptions.model, registry);
102
+ if (typeof resolved === "string") {
103
+ // resolveModel returns a human-readable error string when the
104
+ // input doesn't match any available model. Surface it instead of
105
+ // silently falling back so the caller sees the auth/typo issue.
106
+ throw new Error(resolved);
107
+ }
108
+ normalizedOptions = { ...normalizedOptions, model: resolved };
109
+ }
110
+
111
+ return { id: manager.spawn(pi, ctx, type, prompt, normalizedOptions) };
85
112
  },
86
113
  );
87
114
 
package/src/prompts.ts CHANGED
@@ -19,6 +19,10 @@ export interface PromptExtras {
19
19
  * - "append" mode: env header + parent system prompt + sub-agent context + config.systemPrompt
20
20
  * - "append" with empty systemPrompt: pure parent clone
21
21
  *
22
+ * Both modes prepend an `<active_agent name="${config.name}"/>` tag so downstream
23
+ * extensions (e.g. permission/policy systems) can resolve per-agent policy
24
+ * inside the child session by parsing the system prompt.
25
+ *
22
26
  * @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
23
27
  * @param extras Optional extra sections to inject (memory, preloaded skills).
24
28
  */
@@ -29,6 +33,8 @@ export function buildAgentPrompt(
29
33
  parentSystemPrompt?: string,
30
34
  extras?: PromptExtras,
31
35
  ): string {
36
+ const activeAgentTag = `<active_agent name="${config.name}"/>\n\n`;
37
+
32
38
  const envBlock = `# Environment
33
39
  Working directory: ${cwd}
34
40
  ${env.isGitRepo ? `Git repository: yes\nBranch: ${env.branch}` : "Not a git repository"}
@@ -66,7 +72,7 @@ You are operating as a sub-agent invoked to handle a specific task.
66
72
  ? `\n\n<agent_instructions>\n${config.systemPrompt}\n</agent_instructions>`
67
73
  : "";
68
74
 
69
- return envBlock + "\n\n<inherited_system_prompt>\n" + identity + "\n</inherited_system_prompt>\n\n" + bridge + customSection + extrasSuffix;
75
+ return activeAgentTag + envBlock + "\n\n<inherited_system_prompt>\n" + identity + "\n</inherited_system_prompt>\n\n" + bridge + customSection + extrasSuffix;
70
76
  }
71
77
 
72
78
  // "replace" mode — env header + the config's full system prompt
@@ -75,7 +81,7 @@ You have been invoked to handle a specific task autonomously.
75
81
 
76
82
  ${envBlock}`;
77
83
 
78
- return replaceHeader + "\n\n" + config.systemPrompt + extrasSuffix;
84
+ return activeAgentTag + replaceHeader + "\n\n" + config.systemPrompt + extrasSuffix;
79
85
  }
80
86
 
81
87
  /** Fallback base prompt when parent system prompt is unavailable in append mode. */