@gotgenes/pi-subagents 18.0.2 → 18.0.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
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [18.0.3](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v18.0.2...pi-subagents-v18.0.3) (2026-07-15)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **pi-subagents:** omit empty Default agents section header ([a29c324](https://github.com/gotgenes/pi-packages/commit/a29c32498caa4e29f0cc3292d5a6be4f4126c5f8)), closes [#594](https://github.com/gotgenes/pi-packages/issues/594)
14
+ * **pi-subagents:** source subagent guideline copy from agent config ([a2b41a6](https://github.com/gotgenes/pi-packages/commit/a2b41a665586a271b6453b89dabec0bd94be394c)), closes [#594](https://github.com/gotgenes/pi-packages/issues/594)
15
+
8
16
  ## [18.0.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v18.0.1...pi-subagents-v18.0.2) (2026-07-14)
9
17
 
10
18
 
@@ -120,7 +120,7 @@ classDiagram
120
120
  +run()
121
121
  +resume(prompt, signal)
122
122
  +abort(): boolean
123
- +steer(message): Promise~boolean~
123
+ +steer(message): Promise~SteerOutcome~
124
124
  +isSessionReady(): boolean
125
125
  +getConversation(): string | undefined
126
126
  +getContextPercent(): number | null
@@ -977,7 +977,7 @@ Landed: `src/tools/get-result-report.ts` added — `AgentReport` value object pl
977
977
 
978
978
  `Release: batch "result-delivery"`
979
979
 
980
- #### Step 3 — `Subagent.steer` returns an outcome ([#537])
980
+ #### Step 3 — `Subagent.steer` returns an outcome ([#537])
981
981
 
982
982
  Smell: Category C (ask-then-tell) — coordinators pre-check status before telling.
983
983
  Target files:
@@ -987,9 +987,13 @@ Target files:
987
987
 
988
988
  Outcome: zero steer status pre-checks outside `Subagent.steer`; `steer-tool.execute` cyclomatic drops below 10.
989
989
 
990
+ Landed: `Subagent.steer` returns a discriminated `SteerOutcome` (`delivered` / `buffered` / `rejected` with the observed status) and owns the non-running rejection as its first guard; `SteerOutcome` is exported from `subagent.ts` and re-exported via `types.ts`.
991
+ `SteerTool.execute` and `SubagentsServiceAdapter.steer` dropped their `status !== "running"` pre-checks and switch on the outcome — the adapter maps `outcome.kind !== "rejected"` to the unchanged public boolean, and the tool's delivered-path stats moved into a private `renderDelivered` helper.
992
+ Zero steer status pre-checks remain outside `Subagent.steer`.
993
+
990
994
  `Release: independent`
991
995
 
992
- #### Step 4 — Type the model boundary ([#538])
996
+ #### Step 4 — Type the model boundary ([#538])
993
997
 
994
998
  Smell: Category C (platform type threading) — `ModelRegistry.find/getAll/getAvailable` return `any`, forcing `any`/`unknown` model threading through `model-resolver`, `spawn-config`, `service-adapter`, and `parent-snapshot`.
995
999
  Target files:
@@ -1000,6 +1004,11 @@ Target files:
1000
1004
 
1001
1005
  Outcome: `model-resolver.ts` file-level eslint-disable removed; `service-adapter.spawn` off the HIGH CRAP list; `any` model returns eliminated from the resolver.
1002
1006
 
1007
+ Landed: `ModelRegistry.find/getAll/getAvailable` and `resolveModel`'s return are typed against `Model<any>` from `@earendil-works/pi-ai`; the file-level eslint-disable headers on `model-resolver.ts` (2 rules) and `spawn-config.ts` (4 rules) are both removed — running disable-header tally 5 → 3 (`agent-tool` 6, `agent-widget` 4, `index` 1 remain, all Step 5 scope).
1008
+ `resolveModel`'s fuzzy-scoring loop was extracted to a private `findBestFuzzyMatch` helper, dropping `resolveModel` off the complexity list entirely.
1009
+ `service-adapter.spawn`'s model-resolution branch was extracted to a private `resolveModelOption`, dropping `spawn` from 16 cyclomatic / CRAP 71.3 (HIGH) to 13 cyclomatic / CRAP 49.5 (moderate) — off the HIGH-CRAP list; running HIGH-CRAP tally 2 → 1 remaining (the notification renderer arrow, untouched — Step 7 scope).
1010
+ `resolveInvocationModel` gained a `registry: ModelRegistry | undefined` guard (typed error instead of a crash when a model override is requested with no registry present); the residual `unknown` thread through `ParentSnapshot.model` / `SessionContext.model` is a separate SDK-boundary gap, deferred.
1011
+
1003
1012
  `Release: independent`
1004
1013
 
1005
1014
  #### Step 5 — Narrow `tui`/`theme` render interfaces ([#539])
@@ -1072,8 +1081,8 @@ Outcome: in-package clone groups 9 → ≤ 5; duplicated lines 81 → ≤ 40.
1072
1081
  flowchart LR
1073
1082
  S1["✅ Step 1 (#535)<br/>Result delivery off Subagent"] --> S2["✅ Step 2 (#536)<br/>Decompose get-result-tool"]
1074
1083
  S1 -.soft.-> S7["Step 7 (#541)<br/>Decompose notification renderer"]
1075
- S3["Step 3 (#537)<br/>Steer returns an outcome"]
1076
- S4["Step 4 (#538)<br/>Type the model boundary"]
1084
+ S3["Step 3 (#537)<br/>Steer returns an outcome"]
1085
+ S4["Step 4 (#538)<br/>Type the model boundary"]
1077
1086
  S5["Step 5 (#539)<br/>Narrow tui/theme interfaces"]
1078
1087
  S6["Step 6 (#540)<br/>Table-driven settings handler"]
1079
1088
  S8["Step 8 (#542)<br/>Full-value SubagentStateInit"] --> S9["Step 9 (#543)<br/>Consolidate test clones"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "18.0.2",
3
+ "version": "18.0.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -66,7 +66,7 @@
66
66
  "@earendil-works/pi-coding-agent": "0.79.1",
67
67
  "@earendil-works/pi-tui": "0.79.1",
68
68
  "@types/node": "^22.15.3",
69
- "rollup": "^4.61.1",
69
+ "rollup": "^4.62.2",
70
70
  "rollup-plugin-dts": "^6.4.1",
71
71
  "rumdl": "^0.2.10",
72
72
  "typescript": "^6.0.3",
@@ -15,6 +15,7 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
15
15
  name: "general-purpose",
16
16
  displayName: "Agent",
17
17
  description: "General-purpose agent for complex, multi-step tasks",
18
+ toolGuideline: "- Use general-purpose for complex tasks that need file editing.",
18
19
  // builtinToolNames omitted — means "all available tools" (resolved at lookup time)
19
20
  // inheritContext / runInBackground omitted — strategy fields, callers decide per-call.
20
21
  // Setting them to false would lock callsite intent (see resolveAgentInvocationConfig in invocation-config.ts).
@@ -29,6 +30,7 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
29
30
  name: "Explore",
30
31
  displayName: "Explore",
31
32
  description: "Fast codebase exploration agent (read-only)",
33
+ toolGuideline: "- Use Explore for codebase searches and code understanding.",
32
34
  builtinToolNames: READ_ONLY_TOOLS,
33
35
  model: "anthropic/claude-haiku-4-5-20251001",
34
36
  systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
@@ -69,6 +71,7 @@ Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find,
69
71
  name: "Plan",
70
72
  displayName: "Plan",
71
73
  description: "Software architect for implementation planning (read-only)",
74
+ toolGuideline: "- Use Plan for architecture and implementation planning.",
72
75
  builtinToolNames: READ_ONLY_TOOLS,
73
76
  systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
74
77
  You are a software architect and planning specialist.
@@ -35,6 +35,16 @@ export interface SubagentLifecycleObserver {
35
35
 
36
36
  export type { SubagentStatus } from "#src/lifecycle/subagent-state";
37
37
 
38
+ /**
39
+ * The result of a steer attempt. `Subagent.steer` owns the non-running
40
+ * rejection rule and reports it here, so coordinators switch on the outcome
41
+ * instead of pre-checking status (tell by id, with outcomes).
42
+ */
43
+ export type SteerOutcome =
44
+ | { kind: "delivered" }
45
+ | { kind: "buffered" }
46
+ | { kind: "rejected"; status: SubagentStatus };
47
+
38
48
  /**
39
49
  * The execution machinery a Subagent needs to run. A single mandatory
40
50
  * collaborator: production (SubagentManager.spawn) always supplies it, so run()
@@ -129,16 +139,21 @@ export class Subagent {
129
139
  }
130
140
 
131
141
  /**
132
- * Deliver or buffer a steer message.
133
- * Returns true when delivered immediately; false when buffered for later delivery.
142
+ * Steer a running agent, owning the non-running rejection rule.
143
+ * Returns a `rejected` outcome (with the observed status) when the agent is
144
+ * not running, a `buffered` outcome when the session is not yet ready, or a
145
+ * `delivered` outcome once the message reaches the session.
134
146
  */
135
- async steer(message: string): Promise<boolean> {
147
+ async steer(message: string): Promise<SteerOutcome> {
148
+ if (this.status !== "running") {
149
+ return { kind: "rejected", status: this.status };
150
+ }
136
151
  if (!this.subagentSession) {
137
152
  this.queueSteer(message);
138
- return false;
153
+ return { kind: "buffered" };
139
154
  }
140
155
  await this.subagentSession.steer(message);
141
- return true;
156
+ return { kind: "delivered" };
142
157
  }
143
158
 
144
159
  /** Return the session conversation as formatted text, or undefined if no session. */
package/src/runtime.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * Follows the same pattern as pi-permission-system's ExtensionRuntime.
7
7
  */
8
8
 
9
+ import type { Model } from "@earendil-works/pi-ai";
9
10
  import { buildParentSnapshot, type ParentSnapshot } from "#src/lifecycle/parent-snapshot";
10
11
  import type { ModelInfo } from "#src/tools/spawn-config";
11
12
  import type { SessionContext } from "#src/types";
@@ -54,7 +55,7 @@ export class SubagentRuntime {
54
55
  /** Extract model info from the current session context. */
55
56
  getModelInfo(): ModelInfo {
56
57
  return {
57
- parentModel: this.currentCtx?.model as ModelInfo["parentModel"],
58
+ parentModel: this.currentCtx?.model as Model<any> | undefined,
58
59
  modelRegistry: this.currentCtx?.modelRegistry,
59
60
  };
60
61
  }
@@ -5,6 +5,7 @@
5
5
  * (stripping non-serializable fields), and session gating.
6
6
  */
7
7
 
8
+ import type { Model } from "@earendil-works/pi-ai";
8
9
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
9
10
  import type { WorkspaceProvider } from "#src/lifecycle/workspace";
10
11
  import type { SpawnOptions, SubagentRecord, SubagentsService } from "#src/service/service";
@@ -35,7 +36,7 @@ export interface ServiceRuntimeLike {
35
36
  export class SubagentsServiceAdapter implements SubagentsService {
36
37
  constructor(
37
38
  private readonly manager: SubagentManagerLike,
38
- private readonly resolveModel: (input: string, registry: ModelRegistry) => unknown,
39
+ private readonly resolveModel: (input: string, registry: ModelRegistry) => Model<any> | string,
39
40
  private readonly runtime: ServiceRuntimeLike,
40
41
  ) {}
41
42
 
@@ -44,19 +45,7 @@ export class SubagentsServiceAdapter implements SubagentsService {
44
45
  throw new Error("No active session — cannot spawn agents outside a session.");
45
46
  }
46
47
 
47
- let model: unknown;
48
- if (options?.model) {
49
- const registry = this.runtime.currentCtx.modelRegistry;
50
- if (!registry) {
51
- throw new Error("No model registry available.");
52
- }
53
- const resolved = this.resolveModel(options.model, registry);
54
- if (typeof resolved === "string") {
55
- throw new Error(resolved);
56
- }
57
- model = resolved;
58
- }
59
-
48
+ const model = this.resolveModelOption(options?.model);
60
49
  const description = options?.description ?? prompt.slice(0, 80);
61
50
  const isBackground = !(options?.foreground ?? false);
62
51
 
@@ -87,11 +76,11 @@ export class SubagentsServiceAdapter implements SubagentsService {
87
76
 
88
77
  async steer(id: string, message: string): Promise<boolean> {
89
78
  const record = this.manager.getRecord(id);
90
- if (record?.status !== "running") {
79
+ if (!record) {
91
80
  return false;
92
81
  }
93
- await record.steer(message);
94
- return true;
82
+ const outcome = await record.steer(message);
83
+ return outcome.kind !== "rejected";
95
84
  }
96
85
 
97
86
  async waitForAll(): Promise<void> {
@@ -105,6 +94,20 @@ export class SubagentsServiceAdapter implements SubagentsService {
105
94
  registerWorkspaceProvider(provider: WorkspaceProvider): () => void {
106
95
  return this.manager.registerWorkspaceProvider(provider);
107
96
  }
97
+
98
+ /** Resolve an optional model-string override against the current session's registry. */
99
+ private resolveModelOption(modelInput: string | undefined): Model<any> | undefined {
100
+ if (!modelInput) return undefined;
101
+ const registry = this.runtime.currentCtx?.modelRegistry;
102
+ if (!registry) {
103
+ throw new Error("No model registry available.");
104
+ }
105
+ const resolved = this.resolveModel(modelInput, registry);
106
+ if (typeof resolved === "string") {
107
+ throw new Error(resolved);
108
+ }
109
+ return resolved;
110
+ }
108
111
  }
109
112
 
110
113
  /**
@@ -1,24 +1,17 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-redundant-type-constituents -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
2
1
  /**
3
2
  * Model resolution: exact match ("provider/modelId") with fuzzy fallback.
4
3
  */
5
-
6
- export interface ModelEntry {
7
- id: string;
8
- name: string;
9
- provider: string;
10
- }
4
+ import type { Model } from "@earendil-works/pi-ai";
11
5
 
12
6
  export interface ModelRegistry {
13
- find(provider: string, modelId: string): any;
14
- getAll(): any[];
15
- getAvailable?(): any[];
7
+ find(provider: string, modelId: string): Model<any> | undefined;
8
+ getAll(): Model<any>[];
9
+ getAvailable?(): Model<any>[];
16
10
  }
17
11
 
18
12
  /** Successful model resolution — `model` is the resolved or inherited model instance. */
19
13
  export interface ModelResolutionResult {
20
-
21
- model: any;
14
+ model: Model<any> | undefined;
22
15
  error?: undefined;
23
16
  }
24
17
 
@@ -42,12 +35,13 @@ export type ModelResolution = ModelResolutionResult | ModelResolutionError;
42
35
  * - `modelFromParams` false → silent fallback to `parentModel`.
43
36
  */
44
37
  export function resolveInvocationModel(
45
- parentModel: unknown,
38
+ parentModel: Model<any> | undefined,
46
39
  modelInput: string | undefined,
47
40
  modelFromParams: boolean,
48
- registry: ModelRegistry,
41
+ registry: ModelRegistry | undefined,
49
42
  ): ModelResolution {
50
43
  if (!modelInput) return { model: parentModel };
44
+ if (!registry) return { error: "No model registry available." };
51
45
  const resolved = resolveModel(modelInput, registry);
52
46
  if (typeof resolved !== "string") return { model: resolved };
53
47
  if (modelFromParams) return { error: resolved };
@@ -62,9 +56,9 @@ export function resolveInvocationModel(
62
56
  export function resolveModel(
63
57
  input: string,
64
58
  registry: ModelRegistry,
65
- ): any | string {
59
+ ): Model<any> | string {
66
60
  // Available models (those with auth configured)
67
- const all = (registry.getAvailable?.() ?? registry.getAll()) as ModelEntry[];
61
+ const all = registry.getAvailable?.() ?? registry.getAll();
68
62
  const availableSet = new Set(all.map(m => `${m.provider}/${m.id}`.toLowerCase()));
69
63
 
70
64
  // 1. Exact match: "provider/modelId" — only if available (has auth)
@@ -79,10 +73,27 @@ export function resolveModel(
79
73
  }
80
74
 
81
75
  // 2. Fuzzy match against available models
82
- const query = input.toLowerCase();
76
+ const bestMatch = findBestFuzzyMatch(all, input.toLowerCase());
77
+ if (bestMatch) {
78
+ const found = registry.find(bestMatch.provider, bestMatch.id);
79
+ if (found) return found;
80
+ }
83
81
 
84
- // Score each model: prefer exact id match > id contains > name contains > provider+id contains
85
- let bestMatch: ModelEntry | undefined;
82
+ // 3. No match list available models
83
+ const modelList = all
84
+ .map(m => ` ${m.provider}/${m.id}`)
85
+ .sort()
86
+ .join("\n");
87
+ return `Model not found: "${input}".\n\nAvailable models:\n${modelList}`;
88
+ }
89
+
90
+ /**
91
+ * Score each candidate model — prefer exact id match > id contains > name
92
+ * contains > provider+id contains — and return the best match at or above
93
+ * the acceptance threshold (20), or undefined if nothing scores high enough.
94
+ */
95
+ function findBestFuzzyMatch(all: Model<any>[], query: string): Model<any> | undefined {
96
+ let bestMatch: Model<any> | undefined;
86
97
  let bestScore = 0;
87
98
 
88
99
  for (const m of all) {
@@ -107,15 +118,5 @@ export function resolveModel(
107
118
  }
108
119
  }
109
120
 
110
- if (bestMatch && bestScore >= 20) {
111
- const found = registry.find(bestMatch.provider, bestMatch.id);
112
- if (found) return found;
113
- }
114
-
115
- // 3. No match — list available models
116
- const modelList = all
117
- .map(m => ` ${m.provider}/${m.id}`)
118
- .sort()
119
- .join("\n");
120
- return `Model not found: "${input}".\n\nAvailable models:\n${modelList}`;
121
+ return bestScore >= 20 ? bestMatch : undefined;
121
122
  }
@@ -8,7 +8,7 @@ import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
8
8
  import type { AgentSpawnConfig } from "#src/lifecycle/subagent-manager";
9
9
  import { spawnBackground } from "#src/tools/background-spawner";
10
10
  import { runForeground } from "#src/tools/foreground-runner";
11
- import { buildDetails, buildTypeListText, textResult } from "#src/tools/helpers";
11
+ import { buildAgentGuidelines, buildDetails, buildTypeListText, textResult } from "#src/tools/helpers";
12
12
  import { renderAgentResult } from "#src/tools/result-renderer";
13
13
  import { type ModelInfo, resolveSpawnConfig } from "#src/tools/spawn-config";
14
14
  import type { ParentSessionInfo, Subagent } from "#src/types";
@@ -42,6 +42,7 @@ export type AgentToolSettings = {
42
42
  export class AgentTool {
43
43
  private readonly typeListText: string;
44
44
  private readonly availableTypesText: string;
45
+ private readonly agentGuidelines: string[];
45
46
 
46
47
  constructor(
47
48
  private readonly manager: AgentToolManager,
@@ -52,6 +53,7 @@ export class AgentTool {
52
53
  ) {
53
54
  this.typeListText = buildTypeListText(registry, agentDir);
54
55
  this.availableTypesText = registry.getAvailableTypes().join(", ");
56
+ this.agentGuidelines = buildAgentGuidelines(registry);
55
57
  }
56
58
 
57
59
  async execute(
@@ -128,6 +130,19 @@ export class AgentTool {
128
130
  const agentDir = this.agentDir;
129
131
  const registry = this.registry;
130
132
 
133
+ const guidelines = [
134
+ "- For parallel work, use run_in_background: true on each agent. Foreground calls run sequentially — only one executes at a time.",
135
+ ...this.agentGuidelines,
136
+ "- Provide clear, detailed prompts so the agent can work autonomously.",
137
+ "- Subagent results are returned as text — summarize them for the user.",
138
+ "- Use run_in_background for work you don't need immediately. You will be notified when it completes.",
139
+ "- Use resume with an agent ID to continue a previous agent's work.",
140
+ "- Use steer_subagent to send mid-run messages to a running background agent.",
141
+ '- Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").',
142
+ "- Use thinking to control extended thinking level.",
143
+ "- Use inherit_context if the agent needs the parent conversation history.",
144
+ ].join("\n");
145
+
131
146
  return defineTool({
132
147
  name: "subagent" as const,
133
148
  label: "Subagent",
@@ -140,18 +155,7 @@ Available agent types:
140
155
  ${typeListText}
141
156
 
142
157
  Guidelines:
143
- - For parallel work, use run_in_background: true on each agent. Foreground calls run sequentially — only one executes at a time.
144
- - Use Explore for codebase searches and code understanding.
145
- - Use Plan for architecture and implementation planning.
146
- - Use general-purpose for complex tasks that need file editing.
147
- - Provide clear, detailed prompts so the agent can work autonomously.
148
- - Subagent results are returned as text — summarize them for the user.
149
- - Use run_in_background for work you don't need immediately. You will be notified when it completes.
150
- - Use resume with an agent ID to continue a previous agent's work.
151
- - Use steer_subagent to send mid-run messages to a running background agent.
152
- - Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
153
- - Use thinking to control extended thinking level.
154
- - Use inherit_context if the agent needs the parent conversation history.
158
+ ${guidelines}
155
159
  `,
156
160
  parameters: Type.Object({
157
161
  prompt: Type.String({
@@ -72,9 +72,8 @@ export interface TypeListRegistry extends AgentConfigLookup {
72
72
  * Extracted from index.ts so it can be called inside createAgentTool.
73
73
  */
74
74
  export function buildTypeListText(registry: TypeListRegistry, agentDir: string): string {
75
- const isEnabled = (name: string) => registry.resolveAgentConfig(name).enabled !== false;
76
- const defaultNames = registry.getDefaultAgentNames().filter(isEnabled);
77
- const userNames = registry.getUserAgentNames().filter(isEnabled);
75
+ const defaultNames = registry.getDefaultAgentNames().filter((name) => isEnabledAgent(registry, name));
76
+ const userNames = registry.getUserAgentNames().filter((name) => isEnabledAgent(registry, name));
78
77
 
79
78
  const defaultDescs = defaultNames.map((name) => {
80
79
  const cfg = registry.resolveAgentConfig(name);
@@ -88,14 +87,31 @@ export function buildTypeListText(registry: TypeListRegistry, agentDir: string):
88
87
  });
89
88
 
90
89
  return [
91
- "Default agents:",
92
- ...defaultDescs,
90
+ ...(defaultDescs.length > 0 ? ["Default agents:", ...defaultDescs] : []),
93
91
  ...(customDescs.length > 0 ? ["", "Custom agents:", ...customDescs] : []),
94
92
  "",
95
93
  `Custom agents can be defined in .pi/agents/<name>.md (project) or ${agentDir}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.`,
96
94
  ].join("\n");
97
95
  }
98
96
 
97
+ /** True when an agent config is present and not explicitly disabled. */
98
+ function isEnabledAgent(registry: AgentConfigLookup, name: string): boolean {
99
+ return registry.resolveAgentConfig(name).enabled !== false;
100
+ }
101
+
102
+ /**
103
+ * Collect the per-agent usage guidelines for the subagent tool's Guidelines: block.
104
+ * Sourced from each enabled default agent's `toolGuideline`, in registry order,
105
+ * so a disabled built-in drops its guideline automatically.
106
+ */
107
+ export function buildAgentGuidelines(registry: TypeListRegistry): string[] {
108
+ return registry
109
+ .getDefaultAgentNames()
110
+ .filter((name) => isEnabledAgent(registry, name))
111
+ .map((name) => registry.resolveAgentConfig(name).toolGuideline)
112
+ .filter((line): line is string => line !== undefined);
113
+ }
114
+
99
115
  /** Derive a short model label from a model string. */
100
116
  export function getModelLabelFromConfig(model: string): string {
101
117
  // Strip provider prefix (e.g. "anthropic/claude-sonnet-4-6" → "claude-sonnet-4-6")
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
2
1
  /**
3
2
  * spawn-config.ts — Pure config resolution for the Agent tool.
4
3
  *
@@ -11,6 +10,7 @@ import type { Model } from "@earendil-works/pi-ai";
11
10
  import type { AgentTypeRegistry } from "#src/config/agent-types";
12
11
  import { resolveAgentInvocationConfig } from "#src/config/invocation-config";
13
12
  import { normalizeMaxTurns } from "#src/lifecycle/turn-limits";
13
+ import type { ModelRegistry } from "#src/session/model-resolver";
14
14
  import { resolveInvocationModel } from "#src/session/model-resolver";
15
15
  import type { AgentInvocation, SubagentType, ThinkingLevel } from "#src/types";
16
16
  import {
@@ -22,8 +22,8 @@ import {
22
22
 
23
23
  /** Model info extracted from the parent session context. */
24
24
  export interface ModelInfo {
25
- parentModel: { id: string; name?: string } | undefined;
26
- modelRegistry: unknown;
25
+ parentModel: Model<any> | undefined;
26
+ modelRegistry: ModelRegistry | undefined;
27
27
  }
28
28
 
29
29
  /** Identity: who is being spawned. */
@@ -99,7 +99,7 @@ export function resolveSpawnConfig(
99
99
  modelInfo.parentModel,
100
100
  resolvedConfig.modelInput,
101
101
  resolvedConfig.modelFromParams,
102
- modelInfo.modelRegistry as any,
102
+ modelInfo.modelRegistry,
103
103
  );
104
104
  if (resolution.error) return { error: resolution.error };
105
105
  const model = resolution.model;
@@ -113,7 +113,7 @@ export function resolveSpawnConfig(
113
113
  const effectiveModelId = model?.id;
114
114
  const modelName =
115
115
  effectiveModelId && effectiveModelId !== parentModelId
116
- ? (model?.name ?? effectiveModelId).replace(/^Claude\s+/i, "").toLowerCase()
116
+ ? model.name.replace(/^Claude\s+/i, "").toLowerCase()
117
117
  : undefined;
118
118
 
119
119
  const effectiveMaxTurns = normalizeMaxTurns(
@@ -1,7 +1,7 @@
1
1
  import { defineTool } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import { formatLifetimeTokens, textResult } from "#src/tools/helpers";
4
- import type { Subagent } from "#src/types";
4
+ import type { SteerOutcome, Subagent } from "#src/types";
5
5
 
6
6
  // ---- Deps interfaces ----
7
7
 
@@ -34,41 +34,51 @@ export class SteerTool {
34
34
  `Agent not found: "${params.agent_id}". It may have been cleaned up.`,
35
35
  );
36
36
  }
37
- if (record.status !== "running") {
37
+
38
+ let outcome: SteerOutcome;
39
+ try {
40
+ outcome = await record.steer(params.message);
41
+ } catch (err) {
38
42
  return textResult(
39
- `Agent "${params.agent_id}" is not running (status: ${record.status}). Cannot steer a non-running agent.`,
43
+ `Failed to steer agent: ${err instanceof Error ? err.message : String(err)}`,
40
44
  );
41
45
  }
42
- try {
43
- const delivered = await record.steer(params.message);
44
- this.events.emit("subagents:steered", { id: record.id, message: params.message });
45
- if (!delivered) {
46
+
47
+ switch (outcome.kind) {
48
+ case "rejected":
46
49
  return textResult(
47
- `Steering message queued for agent ${record.id}. It will be delivered once the session initializes.`,
50
+ `Agent "${params.agent_id}" is not running (status: ${outcome.status}). Cannot steer a non-running agent.`,
48
51
  );
49
- }
50
- const tokens = formatLifetimeTokens(record);
51
- const contextPercent = record.getContextPercent();
52
- const stateParts: string[] = [];
53
- if (tokens) stateParts.push(tokens);
54
- stateParts.push(`${record.toolUses} tool ${record.toolUses === 1 ? "use" : "uses"}`);
55
- if (contextPercent !== null)
56
- stateParts.push(`context ${Math.round(contextPercent)}% full`);
57
- if (record.compactionCount)
58
- stateParts.push(
59
- `${record.compactionCount} compaction${record.compactionCount === 1 ? "" : "s"}`,
52
+ case "buffered":
53
+ this.events.emit("subagents:steered", { id: record.id, message: params.message });
54
+ return textResult(
55
+ `Steering message queued for agent ${record.id}. It will be delivered once the session initializes.`,
60
56
  );
61
- return textResult(
62
- `Steering message sent to agent ${record.id}. The agent will process it after its current tool execution.\n` +
63
- `Current state: ${stateParts.join(" · ")}`,
64
- );
65
- } catch (err) {
66
- return textResult(
67
- `Failed to steer agent: ${err instanceof Error ? err.message : String(err)}`,
68
- );
57
+ case "delivered":
58
+ this.events.emit("subagents:steered", { id: record.id, message: params.message });
59
+ return this.renderDelivered(record);
69
60
  }
70
61
  }
71
62
 
63
+ /** Render the success message with live state for a delivered steer. */
64
+ private renderDelivered(record: Subagent) {
65
+ const tokens = formatLifetimeTokens(record);
66
+ const contextPercent = record.getContextPercent();
67
+ const stateParts: string[] = [];
68
+ if (tokens) stateParts.push(tokens);
69
+ stateParts.push(`${record.toolUses} tool ${record.toolUses === 1 ? "use" : "uses"}`);
70
+ if (contextPercent !== null)
71
+ stateParts.push(`context ${Math.round(contextPercent)}% full`);
72
+ if (record.compactionCount)
73
+ stateParts.push(
74
+ `${record.compactionCount} compaction${record.compactionCount === 1 ? "" : "s"}`,
75
+ );
76
+ return textResult(
77
+ `Steering message sent to agent ${record.id}. The agent will process it after its current tool execution.\n` +
78
+ `Current state: ${stateParts.join(" · ")}`,
79
+ );
80
+ }
81
+
72
82
  toToolDefinition() {
73
83
  return defineTool({
74
84
  name: "steer_subagent" as const,
package/src/types.ts CHANGED
@@ -7,6 +7,7 @@ import type { AgentSessionEvent, SessionContext as SdkSessionContext } from "@ea
7
7
  import type { ModelRegistry } from "#src/session/model-resolver";
8
8
 
9
9
 
10
+ export type { SteerOutcome } from "#src/lifecycle/subagent";
10
11
  export { Subagent } from "#src/lifecycle/subagent";
11
12
  export type { AgentSessionEvent, ThinkingLevel };
12
13
 
@@ -55,6 +56,8 @@ export interface AgentConfig extends AgentIdentity, AgentPromptConfig {
55
56
  inheritContext?: boolean;
56
57
  /** Default for spawn: run in background. undefined = caller decides. */
57
58
  runInBackground?: boolean;
59
+ /** One-line usage guideline for the subagent tool's Guidelines: block. Omitted — no guideline line. */
60
+ toolGuideline?: string;
58
61
  /** true = this is an embedded default agent (informational) */
59
62
  isDefault?: boolean;
60
63
  /** false = agent is hidden from the registry */