@narumitw/pi-subagents 0.53.0 → 1.0.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.
- package/README.md +85 -15
- package/package.json +1 -1
- package/src/agents/built-ins.ts +124 -0
- package/src/agents/catalog.ts +224 -0
- package/src/agents/discovery.ts +249 -0
- package/src/agents/types.ts +98 -0
- package/src/agents.ts +47 -670
- package/src/auto-transport.ts +2 -1
- package/src/automation.ts +7 -2
- package/src/capability-router.ts +1 -1
- package/src/completion-delivery.ts +82 -11
- package/src/config-status.ts +2 -2
- package/src/config-ui.ts +8 -8
- package/src/consult-resources.ts +1 -1
- package/src/consult.ts +9 -7
- package/src/create-stateful-transport.ts +2 -1
- package/src/cwd-policy.ts +1 -1
- package/src/execution/budget.ts +56 -0
- package/src/execution/runtime-policy.ts +19 -0
- package/src/execution-plan.ts +1 -1
- package/src/execution-profiles.ts +1 -1
- package/src/execution-ui.ts +1 -1
- package/src/execution.ts +269 -100
- package/src/in-process-transport.ts +3 -2
- package/src/inspect.ts +39 -8
- package/src/limits.ts +1 -0
- package/src/orchestration-metrics.ts +12 -5
- package/src/panel-execution.ts +1 -1
- package/src/panel-planning.ts +1 -1
- package/src/params.ts +3 -1
- package/src/persistence.ts +106 -7
- package/src/registry-types.ts +22 -5
- package/src/registry.ts +205 -37
- package/src/render.ts +1 -1
- package/src/retained-semantic-state.ts +1 -1
- package/src/rpc-transport-metadata.ts +1 -1
- package/src/rpc-transport.ts +2 -1
- package/src/runner.ts +6 -1
- package/src/settings/inspection.ts +275 -0
- package/src/settings/schema.ts +186 -0
- package/src/settings.ts +72 -420
- package/src/spawn-idempotency.ts +1 -1
- package/src/stateful-agent-view.ts +87 -0
- package/src/stateful-config.ts +1 -1
- package/src/stateful-guidance.ts +2 -2
- package/src/stateful-limits.ts +1 -1
- package/src/stateful-prompt.ts +2 -2
- package/src/stateful-render.ts +0 -1
- package/src/stateful-safety.ts +2 -1
- package/src/stateful-tool-params.ts +13 -20
- package/src/stateful.ts +36 -117
- package/src/subagents.ts +8 -9
- package/src/subprocess-transport.ts +2 -6
- package/src/transport-types.ts +1 -1
- package/src/transport-ui.ts +1 -1
- package/src/verification-harness.ts +516 -0
- package/src/verification-receipt.ts +275 -0
- package/src/verified-execution-benchmark.ts +86 -0
- package/src/verified-execution-contract.ts +219 -0
- package/src/work-item-ledger.ts +510 -37
- package/src/work-item-persistence.ts +31 -0
- package/src/workflow-completion-controller.ts +397 -0
- package/src/workflow-plan-compiler.ts +1 -1
- package/src/workflow-plan-patch.ts +1 -1
- package/src/workflow-planning.ts +11 -1
- package/src/workflow-ui.ts +1 -1
package/README.md
CHANGED
|
@@ -299,8 +299,8 @@ No benchmark result in this release changes the default delegation policy or mak
|
|
|
299
299
|
| --- | --- | --- |
|
|
300
300
|
| `list_agents` | Optional `agentScope` (default `user`) and `limit` (default 32, maximum 100) | Bounded agent metadata and omission counts |
|
|
301
301
|
| `get_agent` | Required `agent`; optional `agentScope` | One resolved definition, safe source path, configured tools, and consultation-effective tools; never the system prompt |
|
|
302
|
-
| `list_runs` | Optional `includeClosed` and `limit` (default 50, maximum 100) | Metadata-only retained-run summaries and unread counts |
|
|
303
|
-
| `get_run` | Required `agentId` | Safe `cwd`, current-task/error summaries, thinking level, context footprint, protocol, effective transport, bounded timing/usage telemetry, structured result when valid, policy, history count, and unread count |
|
|
302
|
+
| `list_runs` | Optional `includeClosed` and `limit` (default 50, maximum 100) | Metadata-only retained-run summaries, turn generation, pending-completion count, and unread counts |
|
|
303
|
+
| `get_run` | Required `agentId` | Safe `cwd`, current run and turn generation, current-task/error summaries, thinking level, context footprint, protocol, effective transport, bounded timing/usage telemetry, structured result when valid, policy, history count, pending-completion count, and unread count |
|
|
304
304
|
| `list_workflows` | Optional `limit` (default 50, maximum 100) | Metadata-only persisted blocking-workflow summaries for the current session |
|
|
305
305
|
| `get_workflow` | Required `workflowId` | Bounded task states, generations, dependencies, plan identities, artifact metadata, verification state, and outcome reasons without artifact contents |
|
|
306
306
|
| `list_models` | Optional `limit` (default 50, maximum 100) | Session-scoped models, or the already-loaded available snapshot |
|
|
@@ -310,7 +310,7 @@ No benchmark result in this release changes the default delegation policy or mak
|
|
|
310
310
|
|
|
311
311
|
The schema rejects fields that do not belong to the selected action. Explicit `project` or `both` scope fails before project-agent discovery unless Pi already trusts the project. Run inspection never returns history output, stored context, or mailbox content; unread counts come from a metadata-only snapshot and do not acknowledge messages. Workflow inspection reads validated, redacted snapshots without quarantining or rewriting invalid files. Paths beneath the Pi agent directory use `~`, project paths are workspace-relative, model objects are projected through an allow-list, and model-facing text is bounded to 50 KiB or 2,000 lines.
|
|
312
312
|
|
|
313
|
-
|
|
313
|
+
`subagent_manage` no longer accepts its former compatibility `list` action. Use `subagent_inspect({ "action": "list_runs", "includeClosed": true })` for metadata-only discovery and `get_run` for detail.
|
|
314
314
|
|
|
315
315
|
## 📖 Read-only consultation
|
|
316
316
|
|
|
@@ -480,7 +480,55 @@ Run an explicit dependency workflow:
|
|
|
480
480
|
}
|
|
481
481
|
```
|
|
482
482
|
|
|
483
|
-
|
|
483
|
+
Managed verified execution is an explicit per-workflow contract.
|
|
484
|
+
The executor infers the final mutating integration owner when none is declared, synthesizes one distinct read-only verifier, runs declared deterministic checks in a disposable Git worktree overlaid with the submitted state, and accepts only the exact unchanged submitted state.
|
|
485
|
+
Every deterministic check has a stable evidence ID, a direct executable with argument-array invocation, and an optional relative `cwd` and timeout.
|
|
486
|
+
Only `git`, `node`, `npm`, and `npx` are accepted; shell command strings fail before child allocation.
|
|
487
|
+
The integration owner must request `structured-v2`, declare a non-empty `writePaths` scope, and name current required evidence through its delegation contract.
|
|
488
|
+
Every required evidence ID must match a currently passed executor-owned check; worker-authored artifact metadata never satisfies that binding.
|
|
489
|
+
|
|
490
|
+
```json
|
|
491
|
+
{
|
|
492
|
+
"workflow": {
|
|
493
|
+
"verifiedExecution": {
|
|
494
|
+
"verifierAgent": "reviewer",
|
|
495
|
+
"maxReworkCycles": 1,
|
|
496
|
+
"checks": [
|
|
497
|
+
{
|
|
498
|
+
"id": "focused-test",
|
|
499
|
+
"command": "npm",
|
|
500
|
+
"args": ["test", "--", "feature"],
|
|
501
|
+
"timeoutMs": 120000
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
},
|
|
505
|
+
"tasks": [
|
|
506
|
+
{
|
|
507
|
+
"id": "implementation",
|
|
508
|
+
"agent": "worker",
|
|
509
|
+
"task": "Implement the contracted change.",
|
|
510
|
+
"writePaths": ["src", "test"],
|
|
511
|
+
"acceptanceCriteria": ["The focused regression test passes"],
|
|
512
|
+
"resultFormat": "structured-v2",
|
|
513
|
+
"contract": {
|
|
514
|
+
"version": "pi-subagents:delegation:v2",
|
|
515
|
+
"level": "full",
|
|
516
|
+
"taskId": "implementation",
|
|
517
|
+
"objective": "Implement the contracted change",
|
|
518
|
+
"requiredEvidence": ["focused-test"],
|
|
519
|
+
"sideEffectPolicy": "mutating"
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
]
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
An advanced caller may provide the verifier task instead of letting the executor synthesize it.
|
|
528
|
+
That task must directly and only depend on the integration owner, use `structured-v2`, select the configured distinct verifier agent, and declare an enforced read-only contract without shell or custom tools.
|
|
529
|
+
The executor narrows accepted verifier authority to `read` even when the selected agent normally has broader tools, and disables verifier extensions, skills, prompt templates, and inherited context files.
|
|
530
|
+
|
|
531
|
+
The older explicit verifier contract remains available as a compatibility gate without managed integration:
|
|
484
532
|
|
|
485
533
|
```json
|
|
486
534
|
{
|
|
@@ -528,16 +576,32 @@ A task that explicitly requires independent verification must have exactly one d
|
|
|
528
576
|
The producer stops in `awaiting-verification`, its own passing verification claims remain untrusted, and ordinary downstream tasks stay blocked until the executor records an accepted verifier receipt.
|
|
529
577
|
The verifier runs alone in a fresh subprocess context against one bounded Git-visible tree identity and must encode `verification-accepted`, `verification-rework`, or `verification-rejected` through the documented `structured-v2` status and reason fields.
|
|
530
578
|
Dirty-tree identity covers at most 1 MiB across separately framed staged and unstaged binary diffs plus bounded non-ignored untracked paths and bytes; submodules, unsupported states, and changing trees fail closed.
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
579
|
+
The compatibility gate preserves bounded rework or rejection evidence but does not replay the producer automatically.
|
|
580
|
+
|
|
581
|
+
With `verifiedExecution`, execution completion and acceptance are separate `pi-subagents:work-acceptance:v1` states.
|
|
582
|
+
A worker's own verification, confidence, prose, consensus, or exit status cannot move `pending` acceptance to `accepted`.
|
|
583
|
+
The executor-owned `pi-subagents:verification-receipt:v1` binds both tree captures, patch digest, changed paths, accepted scope, target and verifier generations and `ExecutionPlan` IDs, verifier identity, acceptance criteria, required current evidence, and bounded deterministic check receipts.
|
|
584
|
+
Each receipt is capped at 12 KiB, each stored check stream at 2 KiB, and oversized acceptance evidence fails closed rather than expanding tool details.
|
|
585
|
+
The verifier receives the original objective, current artifact metadata, immutable tree identity, and executor-owned check output rather than the worker narrative.
|
|
586
|
+
Verifier mutation, a stale or replaced generation, a failed or unsafe check, missing evidence, wrong scope, patch, plan, tree, or identity, cancellation, timeout, and unsupported Git state all produce non-success.
|
|
587
|
+
One verifier `rework` decision may rotate the worker and verifier generations when `maxReworkCycles` is `1`; prior grants are revoked, prior evidence remains history, only current requirements and findings are added, and a second rejection is terminal.
|
|
588
|
+
Crashes, timeouts, cancellation, ambiguous settlement, and drift are never replayed.
|
|
589
|
+
The disposable check worktree includes bounded tracked and non-ignored untracked files and is removed after checks.
|
|
590
|
+
When the repository has a local `node_modules` directory, the worktree is nested beneath it so normal Node and npm resolution can read the installed dependency tree without copying it.
|
|
591
|
+
The worktree isolates repository build output, but it does not make the installed dependency tree read-only and is not an operating-system sandbox for processes, network, secrets, absolute paths, or host credentials.
|
|
592
|
+
The accepted state remains the selected shared workspace; no general patch merge or conflict resolver is added.
|
|
593
|
+
|
|
594
|
+
Omitting `verifiedExecution` preserves prior workflow behavior, including the older explicit verifier gate above.
|
|
595
|
+
To downgrade, finish active workflows, remove `verifiedExecution`, and either use the explicit `verifierFor` compatibility form or perform verification in the parent.
|
|
596
|
+
Older package versions reject the unknown managed contract rather than silently providing its guarantees.
|
|
597
|
+
Explicit workflow transitions are atomically persisted as mode-0600, private-text-redacted snapshots for current-session `list_workflows` and `get_workflow` inspection; in-flight execution or acceptance restores as interrupted non-success, and no prior side effect is automatically resumed.
|
|
598
|
+
Legacy v1 and v2 records without acceptance fields retain their prior completed terminal meaning, while v1 self-reported verification flags and artifact trust remain untrusted.
|
|
535
599
|
|
|
536
600
|
## 🔁 Stateful agents
|
|
537
601
|
|
|
538
|
-
Stateful lifecycle tools are available by default. `subagent_spawn` is detached: it schedules work, returns immediately with an opaque `agentId`, and later injects a bounded `pi-subagent-completion` custom message.
|
|
602
|
+
Stateful lifecycle tools are available by default. `subagent_spawn` is detached: it schedules work, returns immediately with an opaque `agentId`, and later injects a bounded `pi-subagent-completion` custom message. Every turn receives an executor-owned `runId`, monotonically increasing agent-local generation, and unique `completionId`. The terminal completion is persisted before delivery, simultaneous completions are batched, and the broker allows at most one in-flight root wake until that parent turn starts.
|
|
539
603
|
|
|
540
|
-
Detached work follows a non-polling policy. With default `next-turn` delivery, prefer one bounded `subagent_spawn` for related asynchronous research or review only when the current response does not depend on its result. If it does, use blocking `subagent` when registered; in **Async only**, complete required work directly, opt into `auto-resume` when a later synthesis turn is appropriate, or switch workflows. With opt-in `auto-resume`, detached broad work may be final-answer-dependent because completion requests a synthesis turn after the root settles. In either mode, do useful non-overlapping main-agent work immediately, do not poll `
|
|
604
|
+
Detached work follows a non-polling policy. With default `next-turn` delivery, prefer one bounded `subagent_spawn` for related asynchronous research or review only when the current response does not depend on its result. If it does, use blocking `subagent` when registered; in **Async only**, complete required work directly, opt into `auto-resume` when a later synthesis turn is appropriate, or switch workflows. With opt-in `auto-resume`, detached broad work may be final-answer-dependent because completion requests a synthesis turn after the root settles. In either mode, do useful non-overlapping main-agent work immediately, do not poll `subagent_inspect` or `subagent_mailbox` with `action: "read"`, and do not duplicate delegated work. Add another detached agent only for truly independent work with safe workspace concurrency. Detached lifecycle work intentionally has no `subagent_wait` tool.
|
|
541
605
|
|
|
542
606
|
A detached agent additionally needs a concrete isolation or specialization benefit such as independent review, bounded context/output, a distinct model/tool profile, or workspace isolation. Simple work that the main agent can perform directly should not be delegated.
|
|
543
607
|
|
|
@@ -546,7 +610,7 @@ A detached agent additionally needs a concrete isolation or specialization benef
|
|
|
546
610
|
- `"next-turn"` (default) preserves the previous behavior: use `deliverAs: "steer"` with `triggerTurn: false`. An active root can consume completion naturally; an idle root is not awakened.
|
|
547
611
|
- `"auto-resume"` holds completion while the root is active, then requests one synthesis turn after the parent settles when no user or extension messages are already pending. Simultaneous completions share that turn, active work is not interrupted, and pending input suppresses the autonomous wake.
|
|
548
612
|
|
|
549
|
-
Auto-resume
|
|
613
|
+
The bounded persisted completion outbox provides ordered at-least-once delivery across process restart without replaying the child turn. When state must be reduced to its storage bound, persistence drops roots without pending completions first and trims old history rather than discarding an outbox-owned root. A completion is acknowledged only after parent context assembly observes its exact `completionId`; an injection that returns synchronously but never reaches context remains pending for retry. If the process exits after context assembly but before acknowledgement is persisted, the same ID can be delivered again and consumers must deduplicate it. Auto-resume wake admission remains best-effort because Pi's custom-message API is fire-and-forget, but an unacknowledged terminal completion itself remains available for redelivery on the next start of the owning session. Transient terminal-persistence failures retry with bounded exponential backoff and keep the run pending; shutdown cancels retry waits and reports a final persistence failure instead of silently resolving unsaved work.
|
|
550
614
|
|
|
551
615
|
The default `subprocess` transport preserves compatibility: each turn starts a fresh isolated `pi --mode json -p --no-session` child and receives sanitized, bounded history.
|
|
552
616
|
Set `transport` to `in-process` to retain one public Pi SDK `AgentSession` per stateful `agentId`, avoiding repeated process startup while preserving native child history in memory.
|
|
@@ -623,7 +687,7 @@ This avoids lifecycle-driven tool-schema churn and preserves a stable provider p
|
|
|
623
687
|
| --- | --- |
|
|
624
688
|
| `subagent_spawn` | Start detached work with optional task-selected thinking and retained timeout, exact-retry `idempotencyKey`, and `text`, `structured-v1`, or `structured-v2` result format; return an opaque `agentId` immediately and deliver completion asynchronously. |
|
|
625
689
|
| `subagent_send` | Send follow-up work with an optional one-turn timeout override and trigger a new turn on a reusable agent; semantic skew requires explicit `revalidate: true`, and shared-workspace write conflicts are guarded unless explicitly overridden. |
|
|
626
|
-
| `subagent_manage` | Use `
|
|
690
|
+
| `subagent_manage` | Use `"interrupt"` to retain an agent after aborting active work or `"close"` to release it; both actions accept optional `subtree`. Use `subagent_inspect` for all list and detail operations. |
|
|
627
691
|
| `subagent_mailbox` | Use `action: "send"` for queue-only messages that do not start a turn, or `"read"` to read and optionally acknowledge unread messages. |
|
|
628
692
|
|
|
629
693
|
The action schemas are flat for provider compatibility and reject parameters that belong to another action. For example:
|
|
@@ -657,13 +721,14 @@ The five replaced names are intentionally not registered as aliases. Update expl
|
|
|
657
721
|
|
|
658
722
|
| Previous call | Fixed-surface call |
|
|
659
723
|
| --- | --- |
|
|
660
|
-
| `subagent_list({ includeClosed })` | `
|
|
724
|
+
| `subagent_list({ includeClosed })` | `subagent_inspect({ action: "list_runs", includeClosed })` |
|
|
725
|
+
| `subagent_manage({ action: "list", includeClosed })` | `subagent_inspect({ action: "list_runs", includeClosed })` |
|
|
661
726
|
| `subagent_interrupt({ agentId, subtree })` | `subagent_manage({ action: "interrupt", agentId, subtree })` |
|
|
662
727
|
| `subagent_close({ agentId, subtree })` | `subagent_manage({ action: "close", agentId, subtree })` |
|
|
663
728
|
| `subagent_message({ agentId, message, ... })` | `subagent_mailbox({ action: "send", agentId, message, ... })` |
|
|
664
729
|
| `subagent_messages({ agentId, acknowledge, limit })` | `subagent_mailbox({ action: "read", agentId, acknowledge, limit })` |
|
|
665
730
|
|
|
666
|
-
Persisted agent and mailbox records require no migration. If an explicit prompt in a resumed conversation keeps requesting an old name, update it with the mapping above or start a fresh conversation. To roll back after an upgrade, pin the package version used before the upgrade; for this migration, use `pi install npm:@narumitw/pi-subagents@0.26.0`. The previous release can read the same state directory.
|
|
731
|
+
Persisted agent and mailbox records require no manual migration; older records load with an empty completion outbox and generation zero. If an explicit prompt in a resumed conversation keeps requesting an old name, update it with the mapping above or start a fresh conversation. To roll back after an upgrade, pin the package version used before the upgrade; for this migration, use `pi install npm:@narumitw/pi-subagents@0.26.0`. The previous release can read the same state directory.
|
|
667
732
|
|
|
668
733
|
A spawn can request a thinking level explicitly:
|
|
669
734
|
|
|
@@ -1010,7 +1075,12 @@ packages/pi-subagents/
|
|
|
1010
1075
|
│ ├── execution-plan.ts # Executor-owned authority and resource resolution
|
|
1011
1076
|
│ ├── work-item-ledger.ts # Persistent dependency and artifact state machine
|
|
1012
1077
|
│ ├── work-item-persistence.ts # Atomic redacted workflow state and inspection
|
|
1013
|
-
│ ├── workflow-verification.ts #
|
|
1078
|
+
│ ├── workflow-verification.ts # Compatibility independent-verifier receipts
|
|
1079
|
+
│ ├── verified-execution-contract.ts # Explicit managed-verification request boundary
|
|
1080
|
+
│ ├── workflow-completion-controller.ts # Sole opted-in terminal acceptance owner
|
|
1081
|
+
│ ├── verification-harness.ts # Disposable deterministic check execution
|
|
1082
|
+
│ ├── verification-receipt.ts # Strict executor-owned managed receipts
|
|
1083
|
+
│ ├── verified-execution-benchmark.ts # Matched offline acceptance/cost fixture
|
|
1014
1084
|
│ ├── workflow-tree-identity.ts # Bounded exact Git-visible tree identities
|
|
1015
1085
|
│ ├── integration-controller.ts # Fail-closed canonical integration admission
|
|
1016
1086
|
│ ├── adaptive-scheduler.ts # Dependency, capacity, budget, and conflict scheduling
|
package/package.json
CHANGED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in agent definitions and prompt construction.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type AgentCapabilityManifest, CAPABILITY_MANIFEST_VERSION } from "../capabilities.js";
|
|
6
|
+
import type { AgentConfig } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export const BUILT_IN_AGENTS: AgentConfig[] = [
|
|
9
|
+
{
|
|
10
|
+
name: "scout",
|
|
11
|
+
description:
|
|
12
|
+
"Read-only codebase reconnaissance; returns concise findings with paths and evidence.",
|
|
13
|
+
tools: ["read", "grep", "find", "ls", "bash"],
|
|
14
|
+
capabilityManifest: builtInManifest(["repository-search", "code-evidence"], "read", [
|
|
15
|
+
"evidence-gathering",
|
|
16
|
+
]),
|
|
17
|
+
source: "built-in",
|
|
18
|
+
filePath: "built-in:scout",
|
|
19
|
+
systemPrompt: [
|
|
20
|
+
"You are a scout subagent. Explore the codebase quickly and report grounded findings.",
|
|
21
|
+
"Do not edit files. Prefer read, grep, find, ls, and safe bash inspection commands.",
|
|
22
|
+
"Return concise bullets with exact file paths, symbols, and open questions.",
|
|
23
|
+
].join("\n"),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "planner",
|
|
27
|
+
description: "Turns reconnaissance into a lean implementation or migration plan.",
|
|
28
|
+
tools: ["read", "grep", "find", "ls"],
|
|
29
|
+
capabilityManifest: builtInManifest(
|
|
30
|
+
["task-decomposition", "implementation-planning", "migration-planning"],
|
|
31
|
+
"read",
|
|
32
|
+
),
|
|
33
|
+
source: "built-in",
|
|
34
|
+
filePath: "built-in:planner",
|
|
35
|
+
systemPrompt: [
|
|
36
|
+
"You are a planner subagent. Produce executable, verifiable plans only.",
|
|
37
|
+
"Do not modify files. Ground the plan in the repository's actual structure.",
|
|
38
|
+
"Call out assumptions, risks, sequencing, and verification commands.",
|
|
39
|
+
].join("\n"),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "reviewer",
|
|
43
|
+
description: "Independent code review agent that inspects existing verification evidence.",
|
|
44
|
+
tools: ["read", "grep", "find", "ls", "bash"],
|
|
45
|
+
capabilityManifest: builtInManifest(
|
|
46
|
+
["code-review", "evidence-review", "security-baseline"],
|
|
47
|
+
"read",
|
|
48
|
+
["independent-review"],
|
|
49
|
+
),
|
|
50
|
+
source: "built-in",
|
|
51
|
+
filePath: "built-in:reviewer",
|
|
52
|
+
systemPrompt: [
|
|
53
|
+
"You are a reviewer subagent. Review changes adversarially and assess claims against the code and existing evidence.",
|
|
54
|
+
"Do not edit files or run tests, builds, benchmarks, formatters, or other long-running verification commands.",
|
|
55
|
+
"Inspect code, diffs, test definitions, and existing verification evidence. Recommend any additional commands for the main agent to run.",
|
|
56
|
+
"Report PASS, FAIL, or PARTIAL with evidence, commands inspected, and specific follow-ups.",
|
|
57
|
+
].join("\n"),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "worker",
|
|
61
|
+
description: "General-purpose implementation worker with the default Pi tool set.",
|
|
62
|
+
capabilityManifest: builtInManifest(
|
|
63
|
+
["implementation", "command-execution", "repository-modification"],
|
|
64
|
+
"write",
|
|
65
|
+
),
|
|
66
|
+
source: "built-in",
|
|
67
|
+
filePath: "built-in:worker",
|
|
68
|
+
systemPrompt: workerSystemPrompt(),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "general",
|
|
72
|
+
description: "Alias for worker; kept for model-generated subagent names.",
|
|
73
|
+
capabilityManifest: builtInManifest(
|
|
74
|
+
["implementation", "command-execution", "repository-modification"],
|
|
75
|
+
"write",
|
|
76
|
+
),
|
|
77
|
+
source: "built-in",
|
|
78
|
+
filePath: "built-in:general",
|
|
79
|
+
systemPrompt: workerSystemPrompt(),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "general-purpose",
|
|
83
|
+
description: "Alias for worker; compatible with common subagent naming conventions.",
|
|
84
|
+
capabilityManifest: builtInManifest(
|
|
85
|
+
["implementation", "command-execution", "repository-modification"],
|
|
86
|
+
"write",
|
|
87
|
+
),
|
|
88
|
+
source: "built-in",
|
|
89
|
+
filePath: "built-in:general-purpose",
|
|
90
|
+
systemPrompt: workerSystemPrompt(),
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
export function getBuiltInAgent(name: string): AgentConfig | undefined {
|
|
95
|
+
const agent = BUILT_IN_AGENTS.find((candidate) => candidate.name === name);
|
|
96
|
+
return agent ? structuredClone(agent) : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function builtInManifest(
|
|
100
|
+
capabilities: string[],
|
|
101
|
+
filesystem: "read" | "write",
|
|
102
|
+
verificationRoles: string[] = [],
|
|
103
|
+
): AgentCapabilityManifest {
|
|
104
|
+
return {
|
|
105
|
+
version: CAPABILITY_MANIFEST_VERSION,
|
|
106
|
+
capabilities,
|
|
107
|
+
modalities: ["text"],
|
|
108
|
+
resultFormats: ["text", "structured-v1", "structured-v2"],
|
|
109
|
+
authority: { filesystem },
|
|
110
|
+
verificationRoles,
|
|
111
|
+
contextStrengths: ["repository"],
|
|
112
|
+
costHint: filesystem === "read" ? "low" : "medium",
|
|
113
|
+
latencyHint: filesystem === "read" ? "low" : "medium",
|
|
114
|
+
limitations: [],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function workerSystemPrompt(): string {
|
|
119
|
+
return [
|
|
120
|
+
"You are a focused worker subagent running in an isolated Pi process.",
|
|
121
|
+
"Complete the delegated task directly. Keep scope tight and avoid unrelated changes.",
|
|
122
|
+
"When done, summarize files changed, commands run, and any remaining risks.",
|
|
123
|
+
].join("\n");
|
|
124
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, sanitized model-facing agent catalog formatting.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { BUILT_IN_AGENTS } from "./built-ins.js";
|
|
6
|
+
import {
|
|
7
|
+
type AgentDiscoveryOptions,
|
|
8
|
+
type AgentDiscoveryResult,
|
|
9
|
+
discoverAgents,
|
|
10
|
+
} from "./discovery.js";
|
|
11
|
+
import type { AgentConfig, SubagentSettings } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export function formatAgentList(
|
|
14
|
+
agents: AgentConfig[],
|
|
15
|
+
maxItems: number,
|
|
16
|
+
): { text: string; remaining: number } {
|
|
17
|
+
if (agents.length === 0) return { text: "none", remaining: 0 };
|
|
18
|
+
const listed = agents.slice(0, maxItems);
|
|
19
|
+
const remaining = agents.length - listed.length;
|
|
20
|
+
return {
|
|
21
|
+
text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join("; "),
|
|
22
|
+
remaining,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AgentCatalog {
|
|
27
|
+
/** The effective catalog for the default invocation scope. */
|
|
28
|
+
user: AgentDiscoveryResult;
|
|
29
|
+
/** The project-scope catalog; custom project definitions are loaded only after project trust. */
|
|
30
|
+
project?: AgentDiscoveryResult;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AgentCatalogFormatOptions {
|
|
34
|
+
maxItems?: number;
|
|
35
|
+
maxDescriptionLength?: number;
|
|
36
|
+
maxCharacters?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AgentCatalogFormatResult {
|
|
40
|
+
text: string;
|
|
41
|
+
omitted: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const DEFAULT_AGENT_CATALOG_MAX_ITEMS = 32;
|
|
45
|
+
export const DEFAULT_AGENT_CATALOG_MAX_DESCRIPTION_LENGTH = 240;
|
|
46
|
+
export const DEFAULT_AGENT_CATALOG_MAX_CHARACTERS = 6_000;
|
|
47
|
+
export const DEFAULT_AGENT_CATALOG_MAX_FILES_PER_SCOPE = 128;
|
|
48
|
+
export const DEFAULT_AGENT_CATALOG_MAX_FILE_BYTES = 64 * 1024;
|
|
49
|
+
export const DEFAULT_AGENT_CATALOG_MAX_TOTAL_BYTES_PER_SCOPE = 2 * 1024 * 1024;
|
|
50
|
+
|
|
51
|
+
const BUILT_IN_AGENT_ORDER = new Map(BUILT_IN_AGENTS.map((agent, index) => [agent.name, index]));
|
|
52
|
+
|
|
53
|
+
function compareCatalogAgents(left: AgentConfig, right: AgentConfig): number {
|
|
54
|
+
const leftBuiltInOrder = BUILT_IN_AGENT_ORDER.get(left.name);
|
|
55
|
+
const rightBuiltInOrder = BUILT_IN_AGENT_ORDER.get(right.name);
|
|
56
|
+
if (leftBuiltInOrder !== undefined || rightBuiltInOrder !== undefined) {
|
|
57
|
+
if (leftBuiltInOrder === undefined) return 1;
|
|
58
|
+
if (rightBuiltInOrder === undefined) return -1;
|
|
59
|
+
return leftBuiltInOrder - rightBuiltInOrder;
|
|
60
|
+
}
|
|
61
|
+
return left.name.localeCompare(right.name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeCatalogDescription(description: string, maxLength: number): string {
|
|
65
|
+
const normalized = description.replace(/\s+/gu, " ").trim();
|
|
66
|
+
if (normalized.length <= maxLength) return normalized;
|
|
67
|
+
const suffix = "…";
|
|
68
|
+
return `${normalized.slice(0, Math.max(0, maxLength - suffix.length)).trimEnd()}${suffix}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type CatalogScope = "user" | "project" | "project-fallback";
|
|
72
|
+
|
|
73
|
+
function catalogAgentLine(
|
|
74
|
+
agent: AgentConfig,
|
|
75
|
+
scope: CatalogScope,
|
|
76
|
+
userNames: ReadonlySet<string>,
|
|
77
|
+
maxDescriptionLength: number,
|
|
78
|
+
): string {
|
|
79
|
+
const scopeLabel =
|
|
80
|
+
scope === "user"
|
|
81
|
+
? 'agentScope: "user"'
|
|
82
|
+
: scope === "project"
|
|
83
|
+
? 'requires agentScope: "project" or "both"'
|
|
84
|
+
: 'requires agentScope: "project" ("both" selects the user definition)';
|
|
85
|
+
const collision =
|
|
86
|
+
scope !== "user" && userNames.has(agent.name)
|
|
87
|
+
? scope === "project"
|
|
88
|
+
? "; overrides the default user definition for project/both"
|
|
89
|
+
: "; scope-specific fallback for the default user override"
|
|
90
|
+
: "";
|
|
91
|
+
return `- ${agent.name} [source: ${agent.source}; ${scopeLabel}${collision}] — ${normalizeCatalogDescription(agent.description, maxDescriptionLength)}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Format the effective agent variants that the parent model can invoke.
|
|
96
|
+
*
|
|
97
|
+
* User-authored descriptions are prompt text, so this formatter deliberately normalizes and bounds
|
|
98
|
+
* them. Project definitions are supplied separately by the caller so an untrusted project is never
|
|
99
|
+
* read merely to build model-facing metadata.
|
|
100
|
+
*/
|
|
101
|
+
export function formatAgentCatalog(
|
|
102
|
+
catalog: AgentCatalog,
|
|
103
|
+
options: AgentCatalogFormatOptions = {},
|
|
104
|
+
): AgentCatalogFormatResult {
|
|
105
|
+
const maxItems = Math.max(0, options.maxItems ?? DEFAULT_AGENT_CATALOG_MAX_ITEMS);
|
|
106
|
+
const maxDescriptionLength = Math.max(
|
|
107
|
+
1,
|
|
108
|
+
options.maxDescriptionLength ?? DEFAULT_AGENT_CATALOG_MAX_DESCRIPTION_LENGTH,
|
|
109
|
+
);
|
|
110
|
+
const maxCharacters = Math.max(1, options.maxCharacters ?? DEFAULT_AGENT_CATALOG_MAX_CHARACTERS);
|
|
111
|
+
const userDiscoveryIncomplete =
|
|
112
|
+
(catalog.user.omittedAgentDefinitions ?? 0) > 0 ||
|
|
113
|
+
catalog.user.metadataDiscoveryIncomplete === true;
|
|
114
|
+
const projectDiscoveryIncomplete =
|
|
115
|
+
(catalog.project?.omittedAgentDefinitions ?? 0) > 0 ||
|
|
116
|
+
catalog.project?.metadataDiscoveryIncomplete === true;
|
|
117
|
+
const discoveredUserAgents = [...catalog.user.agents].sort(compareCatalogAgents);
|
|
118
|
+
const discoveredProjectScopeAgents = [...(catalog.project?.agents ?? [])].sort(
|
|
119
|
+
compareCatalogAgents,
|
|
120
|
+
);
|
|
121
|
+
const userAgents = userDiscoveryIncomplete ? [] : discoveredUserAgents;
|
|
122
|
+
const projectScopeAgents = projectDiscoveryIncomplete ? [] : discoveredProjectScopeAgents;
|
|
123
|
+
const projectAgents = projectScopeAgents.filter((agent) => agent.source === "project");
|
|
124
|
+
const discoveredUserByName = new Map(discoveredUserAgents.map((agent) => [agent.name, agent]));
|
|
125
|
+
const userByName = new Map(userAgents.map((agent) => [agent.name, agent]));
|
|
126
|
+
const userNames = new Set(userByName.keys());
|
|
127
|
+
const potentialProjectFallbackAgents = discoveredProjectScopeAgents.filter(
|
|
128
|
+
(agent) =>
|
|
129
|
+
agent.source === "built-in" && discoveredUserByName.get(agent.name)?.source === "user",
|
|
130
|
+
);
|
|
131
|
+
const projectFallbackAgents =
|
|
132
|
+
userDiscoveryIncomplete || projectDiscoveryIncomplete ? [] : potentialProjectFallbackAgents;
|
|
133
|
+
const allEntries = [
|
|
134
|
+
...userAgents.map((agent) => ({ agent, scope: "user" as const })),
|
|
135
|
+
...projectAgents.map((agent) => ({ agent, scope: "project" as const })),
|
|
136
|
+
...projectFallbackAgents.map((agent) => ({ agent, scope: "project-fallback" as const })),
|
|
137
|
+
];
|
|
138
|
+
const boundedEntries = allEntries.slice(0, maxItems);
|
|
139
|
+
const suppressedMetadataEntries =
|
|
140
|
+
(userDiscoveryIncomplete ? discoveredUserAgents.length : 0) +
|
|
141
|
+
(projectDiscoveryIncomplete
|
|
142
|
+
? discoveredProjectScopeAgents.filter((agent) => agent.source === "project").length +
|
|
143
|
+
potentialProjectFallbackAgents.length
|
|
144
|
+
: 0);
|
|
145
|
+
const discoveryOmissions =
|
|
146
|
+
(catalog.user.omittedAgentDefinitions ?? 0) +
|
|
147
|
+
(catalog.project?.omittedAgentDefinitions ?? 0) +
|
|
148
|
+
suppressedMetadataEntries;
|
|
149
|
+
const discoveryIncomplete =
|
|
150
|
+
catalog.user.metadataDiscoveryIncomplete === true ||
|
|
151
|
+
catalog.project?.metadataDiscoveryIncomplete === true;
|
|
152
|
+
|
|
153
|
+
const render = (entries: typeof allEntries, omitted: number): string => {
|
|
154
|
+
const lines = [
|
|
155
|
+
"Available agent definitions (metadata only; runtime validation and trust remain authoritative).",
|
|
156
|
+
];
|
|
157
|
+
const userLines = entries
|
|
158
|
+
.filter((entry) => entry.scope === "user")
|
|
159
|
+
.map((entry) => catalogAgentLine(entry.agent, entry.scope, userNames, maxDescriptionLength));
|
|
160
|
+
if (userLines.length > 0) {
|
|
161
|
+
lines.push('Default scope (agentScope: "user"):');
|
|
162
|
+
lines.push(...userLines);
|
|
163
|
+
}
|
|
164
|
+
const projectLines = entries
|
|
165
|
+
.filter((entry) => entry.scope !== "user")
|
|
166
|
+
.map((entry) => catalogAgentLine(entry.agent, entry.scope, userNames, maxDescriptionLength));
|
|
167
|
+
if (projectLines.length > 0) {
|
|
168
|
+
lines.push("Trusted project/scope variants (use the required agentScope shown):");
|
|
169
|
+
lines.push(...projectLines);
|
|
170
|
+
}
|
|
171
|
+
const collisionNames = entries
|
|
172
|
+
.filter((entry) => entry.scope !== "user" && userNames.has(entry.agent.name))
|
|
173
|
+
.map((entry) => entry.agent.name);
|
|
174
|
+
if (collisionNames.length > 0 && projectLines.length > 0) {
|
|
175
|
+
const precedence = entries
|
|
176
|
+
.filter((entry) => entry.scope !== "user" && userNames.has(entry.agent.name))
|
|
177
|
+
.map((entry) =>
|
|
178
|
+
entry.scope === "project"
|
|
179
|
+
? `${entry.agent.name}: user with "user", project with "project"/"both"`
|
|
180
|
+
: `${entry.agent.name}: user with "user"/"both", built-in with "project"`,
|
|
181
|
+
);
|
|
182
|
+
lines.push(`Same-name precedence: ${precedence.join("; ")}.`);
|
|
183
|
+
}
|
|
184
|
+
if (omitted > 0) {
|
|
185
|
+
lines.push(
|
|
186
|
+
`[${omitted} additional agent definition${omitted === 1 ? "" : "s"} omitted due to metadata bounds or incomplete discovery.]`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (discoveryIncomplete) {
|
|
190
|
+
lines.push("[Agent metadata discovery was incomplete; some definitions may be unavailable.]");
|
|
191
|
+
}
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
let listedCount = boundedEntries.length;
|
|
196
|
+
let text = render(
|
|
197
|
+
boundedEntries.slice(0, listedCount),
|
|
198
|
+
allEntries.length - listedCount + discoveryOmissions,
|
|
199
|
+
);
|
|
200
|
+
while (text.length > maxCharacters && listedCount > 0) {
|
|
201
|
+
listedCount -= 1;
|
|
202
|
+
text = render(
|
|
203
|
+
boundedEntries.slice(0, listedCount),
|
|
204
|
+
allEntries.length - listedCount + discoveryOmissions,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return { text, omitted: allEntries.length - listedCount + discoveryOmissions };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function discoverAgentCatalog(
|
|
211
|
+
cwd: string,
|
|
212
|
+
projectTrusted: boolean,
|
|
213
|
+
config?: SubagentSettings,
|
|
214
|
+
): AgentCatalog {
|
|
215
|
+
const options: AgentDiscoveryOptions = {
|
|
216
|
+
maxFiles: DEFAULT_AGENT_CATALOG_MAX_FILES_PER_SCOPE,
|
|
217
|
+
maxFileBytes: DEFAULT_AGENT_CATALOG_MAX_FILE_BYTES,
|
|
218
|
+
maxTotalBytes: DEFAULT_AGENT_CATALOG_MAX_TOTAL_BYTES_PER_SCOPE,
|
|
219
|
+
};
|
|
220
|
+
return {
|
|
221
|
+
user: discoverAgents(cwd, "user", config, options),
|
|
222
|
+
project: projectTrusted ? discoverAgents(cwd, "project", config, options) : undefined,
|
|
223
|
+
};
|
|
224
|
+
}
|