@herbertgao/pi-subagents 0.18.5 → 0.22.0-local.1

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/src/schedule.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * of dispatching a user message — schedule fires bypass maxConcurrent so
13
13
  * a 5-minute interval can't be deferred behind 4 long-running agents.
14
14
  * - Result delivery is implicit: spawn → background completion → existing
15
- * `subagent-notification` followUp path. No new delivery code.
15
+ * `subagent-notification` path. No new delivery code.
16
16
  */
17
17
 
18
18
  import type {
package/src/settings.ts CHANGED
@@ -294,6 +294,41 @@ export interface SubagentsSettings {
294
294
  * narrow terminal.
295
295
  */
296
296
  showModel?: boolean
297
+ /**
298
+ * Whether agents declared by installed pi packages load, and from which
299
+ * packages. Defaults to `true`.
300
+ *
301
+ * - `true` — every package whose `package.json` declares `pi.subagents`
302
+ * (or the top-level `pi-subagents` key) contributes its agents.
303
+ * - `false` — none do.
304
+ * - list — only packages matching an entry, compared case-insensitively
305
+ * against the unscoped short name (`@scope/foo` matches `foo`),
306
+ * the full package name, or the settings source string.
307
+ *
308
+ * On by default because installing is already the trust decision: pi executes
309
+ * an installed package's `extensions/` and injects its `skills/` into the
310
+ * system prompt with no further prompt, so a declared `.md` agent is strictly
311
+ * less privileged than what the same package already runs. The gate exists for
312
+ * users who want to narrow that anyway, not as a second install ritual.
313
+ *
314
+ * Package agents always load at the *lowest* precedence, below global and
315
+ * project files, so a same-named local agent silently takes the name back.
316
+ */
317
+ packageAgents?: boolean | string[]
318
+ /**
319
+ * The same gate for workflow scripts declared by installed pi packages
320
+ * (`pi.subagents.workflows`). Defaults to `true`.
321
+ *
322
+ * Deliberately separate from `packageAgents` rather than folded into it: an
323
+ * agent is markdown we hand to a model, a workflow is `.js` we execute. Both
324
+ * still sit below the package's own `extensions/`, which pi runs unsandboxed,
325
+ * so the default matches — but a user who wants package agents without package
326
+ * workflow scripts can have that without giving up either wholesale.
327
+ *
328
+ * No-op when `workflowsEnabled` is off: the tool is never registered, so there
329
+ * is nothing to resolve a workflow name against.
330
+ */
331
+ packageWorkflows?: boolean | string[]
297
332
  /**
298
333
  * How much of the conversation viewer's transcript renders as Markdown.
299
334
  * Defaults to `assistant`. Applied live — the viewer's `m` key cycles this
@@ -337,6 +372,8 @@ export interface SettingsAppliers {
337
372
  setShowCost: (b: boolean) => void
338
373
  setShowModel: (b: boolean) => void
339
374
  setViewerMarkdown: (mode: ViewerMarkdownMode) => void
375
+ setPackageAgents: (gate: boolean | string[]) => void
376
+ setPackageWorkflows: (gate: boolean | string[]) => void
340
377
  }
341
378
 
342
379
  /** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
@@ -367,6 +404,28 @@ const MAX_TURNS_CEILING = 10_000
367
404
  const GRACE_TURNS_CEILING = 1_000
368
405
  const SUBAGENT_DEPTH_CEILING = 16
369
406
 
407
+ /**
408
+ * Normalize a package gate: a boolean passes through, an array is trimmed to its
409
+ * non-empty string entries, a bare string becomes a one-entry allowlist, and
410
+ * anything else is dropped.
411
+ *
412
+ * The two coercions exist for the same reason: this setting's default is `true`,
413
+ * so *dropping* a malformed value silently admits every package — the opposite
414
+ * of what someone narrowing the gate meant. So an array that trims to empty is
415
+ * kept as `[]` (matching nothing, like `false`) rather than dropped, and
416
+ * `"my-package"` — the obvious typo for a one-entry list — is read as
417
+ * `["my-package"]` rather than widened to everything.
418
+ */
419
+ function sanitizePackageGate(val: unknown): boolean | string[] | undefined {
420
+ if (typeof val === "boolean") return val
421
+ if (typeof val === "string") return val.trim() ? [val.trim()] : []
422
+ if (!Array.isArray(val)) return undefined
423
+ return val
424
+ .filter((e): e is string => typeof e === "string")
425
+ .map((e) => e.trim())
426
+ .filter(Boolean)
427
+ }
428
+
370
429
  /** Drop fields that don't match the expected shape. Silent — garbage becomes absent. */
371
430
  function sanitize(raw: unknown): SubagentsSettings {
372
431
  if (!raw || typeof raw !== "object") return {}
@@ -482,6 +541,10 @@ function sanitize(raw: unknown): SubagentsSettings {
482
541
  if (typeof r.workflowsEnabled === "boolean") {
483
542
  out.workflowsEnabled = r.workflowsEnabled
484
543
  }
544
+ const packageAgents = sanitizePackageGate(r.packageAgents)
545
+ if (packageAgents !== undefined) out.packageAgents = packageAgents
546
+ const packageWorkflows = sanitizePackageGate(r.packageWorkflows)
547
+ if (packageWorkflows !== undefined) out.packageWorkflows = packageWorkflows
485
548
  if (r.fallbackSubagent === false) {
486
549
  // The only non-string spelling worth accepting: a boolean would otherwise be
487
550
  // dropped, silently leaving the PERMISSIVE default in place. Every string is
@@ -595,6 +658,9 @@ export function applySettings(
595
658
  if (s.viewerMarkdown) appliers.setViewerMarkdown(s.viewerMarkdown)
596
659
  if (typeof s.workflowsEnabled === "boolean")
597
660
  appliers.setWorkflowsEnabled(s.workflowsEnabled)
661
+ if (s.packageAgents !== undefined) appliers.setPackageAgents(s.packageAgents)
662
+ if (s.packageWorkflows !== undefined)
663
+ appliers.setPackageWorkflows(s.packageWorkflows)
598
664
  }
599
665
 
600
666
  /**
package/src/types.ts CHANGED
@@ -88,8 +88,12 @@ export interface AgentConfig {
88
88
  isDefault?: boolean
89
89
  /** false = agent is hidden from the registry */
90
90
  enabled?: boolean
91
- /** Where this agent was loaded from */
92
- source?: "default" | "project" | "global"
91
+ /**
92
+ * Where this agent was loaded from. `"package"` means an installed pi package
93
+ * declared it (see package-resources.ts) — those files live under pi's install
94
+ * root, so `/agents` treats them as read-only and offers Eject instead of Edit.
95
+ */
96
+ source?: "default" | "project" | "global" | "package"
93
97
  /** Path of the .md it was loaded from. Unset for embedded defaults. */
94
98
  sourcePath?: string
95
99
  }
@@ -157,6 +161,9 @@ export type MentionResolution =
157
161
  | { kind: "live"; record: AgentRecord }
158
162
  | { kind: "tombstone"; entry: AgentTombstone }
159
163
 
164
+ /** Which concurrency pool a run is charged to. */
165
+ export type Pool = "background" | "foreground"
166
+
160
167
  export interface AgentRecord {
161
168
  id: string
162
169
  type: SubagentType
@@ -197,8 +204,22 @@ export interface AgentRecord {
197
204
  * which says only that the agent has an inline result surface: a detached
198
205
  * cross-extension RPC spawn is foreground by that measure and yet blocks
199
206
  * nobody, so it takes no slot.
207
+ *
208
+ * Cleared when the inline caller stops waiting: by `sendToBackground`, or
209
+ * when the run settles.
200
210
  */
201
211
  blocking?: boolean
212
+ /**
213
+ * The concurrency pool this run's slot is charged to, set once when it
214
+ * starts. `settleRun` releases exactly this; `sendToBackground` is the only
215
+ * thing that moves it, together with the counters.
216
+ */
217
+ chargedPool?: Pool
218
+ /**
219
+ * Removes the listener that stops this run when its caller's signal aborts.
220
+ * At most one is armed at a time — queueing arms one, starting replaces it.
221
+ */
222
+ detachParentAbort?: () => void
202
223
  /**
203
224
  * Present only while the record is "queued": resolves when it leaves the
204
225
  * queue, started or aborted. `spawnAndWait` waits on this because a queued
@@ -244,8 +265,9 @@ export interface AgentRecord {
244
265
  /** Number of times this agent's session has compacted. Initialized to 0 at spawn. */
245
266
  compactionCount: number
246
267
  /**
247
- * Whether this agent was spawned to run in the background. Tri-state, set at
248
- * spawn from `SpawnOptions.isBackground`: `true` = background, `false` =
268
+ * Whether this agent runs in the background. Tri-state, initialized at spawn
269
+ * from `SpawnOptions.isBackground` and flipped `false` → `true` (once, never
270
+ * back) by `sendToBackground`: `true` = background, `false` =
249
271
  * foreground (has an inline Agent tool-result surface), `undefined` = the
250
272
  * caller never declared it (e.g. a cross-extension RPC spawn, which is detached
251
273
  * and has no inline surface). The widget's background-only filter keys off this
@@ -99,6 +99,8 @@ export interface AgentDetails {
99
99
  activity?: string
100
100
  /** Current spinner frame index (for animated running indicator). */
101
101
  spinnerFrame?: number
102
+ /** Show the ctrl+b hint on this running foreground call's row — one row per batch. */
103
+ backgroundHint?: boolean
102
104
  /** Short label for the model the run used, e.g. "haiku 4.5". */
103
105
  modelName?: string
104
106
  /** Notable config tags (e.g. ["thinking: high", "isolated"]). */