@youngjurry/pi-agents 0.7.1 → 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
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.3 - 2026-09-05
4
+
5
+ - Fold the full `wait_agent` status tree by default and reuse Pi's `Ctrl+O` expansion state to reveal it on demand.
6
+ - Keep provider-facing wait results limited to newly queued mailbox notices while avoiding overwhelming TUI output.
7
+ - Add optional Role `skills` frontmatter that injects selected complete `SKILL.md` instructions into child system prompts.
8
+ - Report configured Role skills through on-demand Role discovery and fail clearly when a selected Skill is unavailable.
9
+
10
+ ## 0.7.2 - 2026-09-02
11
+
12
+ - Keep execution slots reserved until an `AgentSession` is fully settled and safe to evict.
13
+ - Treat temporary resident saturation as queue backpressure instead of permanently failing waiting agents.
14
+ - Resume blocked FIFO scheduling on settlement without busy-looping or losing wake-up signals.
15
+ - Add regression coverage for the batch overflow race that previously produced `agent residency limit reached` errors.
16
+
3
17
  ## 0.7.1 - 2026-09-01
4
18
 
5
19
  - Order the `/agents` picker by latest task assignment, newest first, instead of alphabetically.
package/README.md CHANGED
@@ -125,6 +125,7 @@ Role format:
125
125
  name: reviewer
126
126
  description: Review code without editing
127
127
  tools: read, grep, find, ls, bash
128
+ skills: [document]
128
129
  model: openai/gpt-5.4
129
130
  thinking: high
130
131
  nickname_candidates: [Ada, Grace]
@@ -133,6 +134,8 @@ nickname_candidates: [Ada, Grace]
133
134
  Review carefully and return findings with exact paths.
134
135
  ```
135
136
 
137
+ `skills` is optional. Each named Skill must be discoverable by Pi. Its complete `SKILL.md` is loaded into that Role's child system prompt, including for tightly restricted Roles that do not expose `read` or `bash`. Relative references remain rooted at the Skill's base directory. Unknown Skill names fail explicitly instead of silently weakening the Role. Roles without `skills` retain Pi's normal progressive disclosure: the Skill catalog appears only when `read` or `bash` is active, and the child decides whether to load a matching Skill.
138
+
136
139
  ## Model configuration
137
140
 
138
141
  Global sub-agent settings live outside the installed package so updates cannot overwrite them:
@@ -177,6 +180,7 @@ The settings file is optional, but spawning requires a model from either the tas
177
180
  - Referenced legacy flat child files are migrated when their main session is resumed
178
181
  - Parents receive a compact completion notice instead of the full answer; use `list_agents(view="results")` or read the result file on demand
179
182
  - Notices to a busy agent are queued safely: `wait_agent` returns them in its own result, and any leftovers are delivered right after a successful recipient turn
183
+ - `wait_agent` sends only newly queued mailbox notices to the model; its full status tree is folded in the TUI by default and can be toggled with `Ctrl+O`
180
184
  - Failed notice delivery is re-queued instead of silently discarded
181
185
  - Notices pending when a turn is aborted or errors are deferred to the next explicit turn without restarting the interrupted agent
182
186
  - The extension never inserts messages between an assistant tool call and its tool result, keeping session history protocol-valid for strict gateways
package/control.ts CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  rootAgentInstructions,
24
24
  sanitizeForkMessages,
25
25
  } from "./context.ts";
26
- import { discoverRoles, resolveRole } from "./roles.ts";
26
+ import { discoverRoles, formatAssignedSkills, resolveRole } from "./roles.ts";
27
27
  import {
28
28
  DEFAULT_CHILD_THINKING_LEVEL,
29
29
  DEFAULT_MAX_CONCURRENT_SUBAGENTS,
@@ -163,6 +163,7 @@ export class AgentControl {
163
163
  private modelRuntimePromise?: Promise<ModelRuntime>;
164
164
  private activeExecutionSlots = 0;
165
165
  private schedulerPromise?: Promise<void>;
166
+ private schedulerRerunRequested = false;
166
167
  private spawnOperationTail: Promise<void> = Promise.resolve();
167
168
  private disposed = false;
168
169
  private shuttingDown = false;
@@ -586,9 +587,15 @@ export class AgentControl {
586
587
  await this.transcriptToolDefinitionsPromise;
587
588
  }
588
589
 
589
- private async createLoader(cwd: string, settingsManager: SettingsManager, instructions: string): Promise<DefaultResourceLoader> {
590
+ private async createLoader(
591
+ cwd: string,
592
+ settingsManager: SettingsManager,
593
+ instructions: string,
594
+ assignedSkillNames?: readonly string[],
595
+ ): Promise<DefaultResourceLoader> {
590
596
  const selfPath = path.resolve(this.selfExtensionPath);
591
- const loader = new DefaultResourceLoader({
597
+ let loader: DefaultResourceLoader;
598
+ loader = new DefaultResourceLoader({
592
599
  cwd,
593
600
  agentDir: getAgentDir(),
594
601
  settingsManager,
@@ -596,22 +603,33 @@ export class AgentControl {
596
603
  ...base,
597
604
  extensions: base.extensions.filter((extension) => path.resolve(extension.resolvedPath) !== selfPath),
598
605
  }),
599
- systemPromptOverride: (base) => `${base || "You are a coding agent."}\n\n${instructions}`,
606
+ systemPromptOverride: (base) => [
607
+ base || "You are a coding agent.",
608
+ instructions,
609
+ formatAssignedSkills(assignedSkillNames, loader.getSkills().skills),
610
+ ].filter((part) => part.trim()).join("\n\n"),
600
611
  });
601
612
  await loader.reload();
602
613
  this.captureTranscriptToolDefinitions(loader);
603
614
  return loader;
604
615
  }
605
616
 
606
- private async evictForResidency(protectedPath?: string): Promise<void> {
617
+ private tryEvictForResidency(protectedPath?: string): boolean {
607
618
  const allResidents = [...this.agentsByPath.values()].filter((record) => record.loaded);
608
- if (allResidents.length < this.maxResidentSubagents) return;
619
+ if (allResidents.length < this.maxResidentSubagents) return true;
609
620
  const candidate = allResidents
610
621
  .filter((record) => record.path !== protectedPath)
611
622
  .filter((record) => record.status !== "running" && !record.holdsExecutionSlot && record.session?.isIdle !== false)
612
623
  .sort((left, right) => left.lastUsedAt - right.lastUsedAt)[0];
613
- if (!candidate) throw new Error(`agent residency limit reached (${this.maxResidentSubagents}); all resident agents are busy`);
624
+ if (!candidate) return false;
614
625
  this.unload(candidate);
626
+ return true;
627
+ }
628
+
629
+ private async evictForResidency(protectedPath?: string): Promise<void> {
630
+ if (!this.tryEvictForResidency(protectedPath)) {
631
+ throw new Error(`agent residency limit reached (${this.maxResidentSubagents}); all resident agents are busy`);
632
+ }
615
633
  }
616
634
 
617
635
  private unload(record: AgentRecord): void {
@@ -656,6 +674,13 @@ export class AgentControl {
656
674
  this.noteTurnEnd(record.path, lastAssistant?.role === "assistant" ? lastAssistant.stopReason : undefined);
657
675
  }
658
676
  }
677
+ if (event.type === "agent_settled") {
678
+ this.releaseExecutionSlot(record);
679
+ this.persistState();
680
+ // Let AgentSession finish emitting its settled event before an LRU eviction
681
+ // can dispose this now-idle session.
682
+ queueMicrotask(() => void this.scheduleQueued());
683
+ }
659
684
  this.changed();
660
685
  });
661
686
  }
@@ -686,10 +711,8 @@ export class AgentControl {
686
711
  if (answer.aborted) {
687
712
  record.status = "interrupted";
688
713
  record.statusMessage = "interrupted";
689
- this.releaseExecutionSlot(record);
690
714
  this.persistState();
691
715
  this.changed();
692
- void this.scheduleQueued();
693
716
  return;
694
717
  }
695
718
  if (answer.error) {
@@ -702,10 +725,8 @@ export class AgentControl {
702
725
  record.statusMessage = undefined;
703
726
  }
704
727
  this.writeAgentResult(record);
705
- this.releaseExecutionSlot(record);
706
728
  this.persistState();
707
729
  this.changed();
708
- void this.scheduleQueued();
709
730
  void this.deliver(record.path, record.parentPath, this.completionNotice(record), false, "AGENT_STATUS").catch(() => {});
710
731
  }
711
732
 
@@ -862,8 +883,11 @@ export class AgentControl {
862
883
  return records;
863
884
  }
864
885
 
865
- private async startQueued(record: AgentRecord): Promise<void> {
866
- if (record.status !== "queued") return;
886
+ private async startQueued(record: AgentRecord): Promise<boolean> {
887
+ if (record.status !== "queued") return true;
888
+ // A completed AgentSession emits agent_end just before it becomes idle. Treat a
889
+ // temporarily full resident set as backpressure, not as a permanent task error.
890
+ if (!record.session && !this.tryEvictForResidency(record.path)) return false;
867
891
  this.reserveExecutionSlot();
868
892
  record.holdsExecutionSlot = true;
869
893
  record.status = "pending_init";
@@ -877,7 +901,7 @@ export class AgentControl {
877
901
  record.statusMessage = "waiting for an execution slot";
878
902
  this.releaseExecutionSlot(record);
879
903
  this.unload(record);
880
- return;
904
+ return true;
881
905
  }
882
906
  const content = [...(record.queuedMail ?? []), record.queuedMessage].filter((item): item is string => Boolean(item)).join("\n\n");
883
907
  if ((record.queuedMail?.length ?? 0) > 0) this.mailboxPending.set(record.path, 0);
@@ -885,6 +909,7 @@ export class AgentControl {
885
909
  record.queuedMail = undefined;
886
910
  this.launch(record, content);
887
911
  this.persistState();
912
+ return true;
888
913
  } catch (error) {
889
914
  record.status = "errored";
890
915
  record.statusMessage = error instanceof Error ? error.message : String(error);
@@ -896,6 +921,7 @@ export class AgentControl {
896
921
  this.writeAgentResult(record);
897
922
  this.persistState();
898
923
  void this.deliver(record.path, record.parentPath, this.completionNotice(record), false, "AGENT_STATUS").catch(() => {});
924
+ return true;
899
925
  }
900
926
  }
901
927
 
@@ -903,20 +929,26 @@ export class AgentControl {
903
929
  while (!this.disposed && !this.shuttingDown && this.activeExecutionSlots < this.maxConcurrentSubagents) {
904
930
  const next = this.queuedRecords()[0];
905
931
  if (!next) break;
906
- await this.startQueued(next);
932
+ if (!(await this.startQueued(next))) break;
907
933
  }
908
934
  }
909
935
 
910
936
  private async scheduleQueued(): Promise<void> {
911
937
  if (this.disposed || this.shuttingDown) return;
912
- if (this.schedulerPromise) return this.schedulerPromise;
938
+ if (this.schedulerPromise) {
939
+ this.schedulerRerunRequested = true;
940
+ return this.schedulerPromise;
941
+ }
942
+ this.schedulerRerunRequested = false;
913
943
  const operation = this.runQueuedScheduler();
914
944
  this.schedulerPromise = operation;
915
945
  try {
916
946
  await operation;
917
947
  } finally {
948
+ const rerun = this.schedulerRerunRequested;
949
+ this.schedulerRerunRequested = false;
918
950
  if (this.schedulerPromise === operation) this.schedulerPromise = undefined;
919
- if (!this.disposed && !this.shuttingDown && this.activeExecutionSlots < this.maxConcurrentSubagents && this.queuedRecords().length > 0) {
951
+ if (rerun && !this.disposed && !this.shuttingDown) {
920
952
  queueMicrotask(() => void this.scheduleQueued());
921
953
  }
922
954
  }
@@ -1119,6 +1151,7 @@ export class AgentControl {
1119
1151
  model: role.model || settings.defaultModel,
1120
1152
  thinkingLevel: role.thinkingLevel ?? settings.defaultThinkingLevel ?? DEFAULT_CHILD_THINKING_LEVEL,
1121
1153
  tools: role.tools,
1154
+ skills: role.skills,
1122
1155
  source: role.source,
1123
1156
  }));
1124
1157
  }
@@ -1253,7 +1286,12 @@ export class AgentControl {
1253
1286
  const forkContext = this.forkContextFromSessionManager(sessionManager);
1254
1287
  const role = resolveRole(this.root.cwd, this.root.ctx.isProjectTrusted(), record.role);
1255
1288
  const settingsManager = SettingsManager.create(this.root.cwd, getAgentDir());
1256
- const loader = await this.createLoader(this.root.cwd, settingsManager, this.childInstructions(record, role.systemPrompt));
1289
+ const loader = await this.createLoader(
1290
+ this.root.cwd,
1291
+ settingsManager,
1292
+ this.childInstructions(record, role.systemPrompt),
1293
+ role.skills,
1294
+ );
1257
1295
  const runtime = await this.getModelRuntime(this.root.ctx);
1258
1296
  const model = runtime.getModel(record.modelProvider, record.modelId) || this.root.model;
1259
1297
  if (!model) throw new Error(`model ${record.modelProvider}/${record.modelId} is unavailable`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youngjurry/pi-agents",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Persistent in-process Codex-style multi-agent collaboration for Pi",
5
5
  "author": "youngjurry",
6
6
  "type": "module",
package/roles.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
4
- import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
4
+ import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter, type Skill } from "@earendil-works/pi-coding-agent";
5
5
  import type { AgentRole } from "./types.ts";
6
6
 
7
7
  const BUILTIN_ROLES: AgentRole[] = [
@@ -31,6 +31,7 @@ type RoleFrontmatter = {
31
31
  name?: unknown;
32
32
  description?: unknown;
33
33
  tools?: unknown;
34
+ skills?: unknown;
34
35
  model?: unknown;
35
36
  thinking?: unknown;
36
37
  nickname_candidates?: unknown;
@@ -67,6 +68,7 @@ function loadDirectory(directory: string, source: "user" | "project"): AgentRole
67
68
  description: frontmatter.description.trim(),
68
69
  systemPrompt: body.trim(),
69
70
  tools: stringList(frontmatter.tools),
71
+ skills: stringList(frontmatter.skills),
70
72
  model: typeof frontmatter.model === "string" ? frontmatter.model.trim() : undefined,
71
73
  thinkingLevel: thinking,
72
74
  nicknameCandidates: stringList(frontmatter.nickname_candidates),
@@ -114,3 +116,19 @@ export function resolveRole(cwd: string, projectTrusted: boolean, name?: string)
114
116
  }
115
117
  return role;
116
118
  }
119
+
120
+ export function formatAssignedSkills(requestedNames: readonly string[] | undefined, discoveredSkills: readonly Skill[]): string {
121
+ if (!requestedNames?.length) return "";
122
+ const skillsByName = new Map(discoveredSkills.map((skill) => [skill.name, skill]));
123
+ const missing = requestedNames.filter((name) => !skillsByName.has(name));
124
+ if (missing.length > 0) {
125
+ const available = [...skillsByName.keys()].sort().join(", ") || "none";
126
+ throw new Error(`Role references unknown skill(s): ${missing.join(", ")}. Available skills: ${available}`);
127
+ }
128
+ const sections = requestedNames.map((name) => {
129
+ const skill = skillsByName.get(name)!;
130
+ const instructions = fs.readFileSync(skill.filePath, "utf8").trim();
131
+ return `<skill>\nName: ${skill.name}\nLocation: ${skill.filePath}\nBase directory: ${skill.baseDir}\n\n${instructions}\n</skill>`;
132
+ });
133
+ return `<agent_skills>\nThe following Role-selected skills are fully loaded. Follow them when completing the task. Resolve relative paths against each skill's base directory.\n\n${sections.join("\n\n")}\n</agent_skills>`;
134
+ }
package/tools.ts CHANGED
@@ -71,7 +71,14 @@ function renderCollaborationResult(
71
71
  }
72
72
  const icon = data.timedOut ? theme.fg("warning", "◷") : theme.fg("success", "✓");
73
73
  const lines = [`${icon} ${theme.fg("toolTitle", data.tool)}`];
74
- for (const agent of data.targets) lines.push(` ${theme.fg("accent", compactStatus(agent))}`);
74
+ if (data.tool === "wait_agent" && data.targets.length > 0 && !options.expanded) {
75
+ const counts = new Map<string, number>();
76
+ for (const agent of data.targets) counts.set(agent.status, (counts.get(agent.status) ?? 0) + 1);
77
+ const summary = [...counts.entries()].map(([status, count]) => `${count} ${status}`).join(" · ");
78
+ lines.push(` ${theme.fg("muted", `${data.targets.length} agents hidden · ${summary} · Ctrl+O to expand`)}`);
79
+ } else {
80
+ for (const agent of data.targets) lines.push(` ${theme.fg("accent", compactStatus(agent))}`);
81
+ }
75
82
  if (data.roles?.length) {
76
83
  lines.push(` ${theme.fg("muted", "Roles:")} ${data.roles.map((role) => role.name).join(", ")}`);
77
84
  if (options.expanded) {
@@ -81,6 +88,7 @@ function renderCollaborationResult(
81
88
  role.model ? `model: ${role.model}` : undefined,
82
89
  role.thinkingLevel ? `thinking: ${role.thinkingLevel}` : undefined,
83
90
  `tools: ${role.tools?.join(", ") || "default set"}`,
91
+ role.skills?.length ? `skills: ${role.skills.join(", ")}` : undefined,
84
92
  ].filter(Boolean).join(" · ");
85
93
  lines.push(` ${theme.fg("accent", role.name)} — ${role.description}`);
86
94
  lines.push(` ${theme.fg("dim", configuration)}`);
package/types.ts CHANGED
@@ -146,6 +146,7 @@ export interface AgentRoleView {
146
146
  model?: string;
147
147
  thinkingLevel?: ThinkingLevel;
148
148
  tools?: string[];
149
+ skills?: string[];
149
150
  source: "builtin" | "user" | "project";
150
151
  }
151
152
 
@@ -154,6 +155,7 @@ export interface AgentRole {
154
155
  description: string;
155
156
  systemPrompt: string;
156
157
  tools?: string[];
158
+ skills?: string[];
157
159
  model?: string;
158
160
  thinkingLevel?: ThinkingLevel;
159
161
  nicknameCandidates?: string[];