@cr1ms0n/pi-subagent 0.9.0 → 0.11.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/CHANGELOG.md +23 -2
- package/README.md +40 -672
- package/README.zh-CN.md +42 -0
- package/docs/ARCHITECTURE.md +12 -24
- package/docs/COST-ACCOUNTING.md +6 -7
- package/docs/DEVELOPMENT.md +124 -0
- package/docs/PLAN.md +2 -0
- package/docs/REFERENCE.md +454 -0
- package/docs/RELEASING.md +151 -32
- package/docs/ROADMAP.md +2 -0
- package/docs/SECURITY.md +17 -18
- package/docs/UX.md +8 -12
- package/package.json +9 -1
- package/skills/subagent/SKILL.md +17 -12
- package/src/backend.ts +16 -1
- package/src/config.ts +1 -1
- package/src/extension.ts +33 -15
- package/src/format.ts +98 -3
- package/src/jev-router.ts +63 -27
- package/src/model-failover.ts +445 -0
- package/src/notifications.ts +2 -0
- package/src/orchestrator.ts +522 -303
- package/src/output.ts +9 -4
- package/src/persistence.ts +127 -5
- package/src/policy.ts +26 -3
- package/src/process-lock.ts +16 -0
- package/src/protocol.ts +208 -11
- package/src/registry.ts +33 -7
- package/src/routing-policy.ts +27 -19
- package/src/routing-types.ts +26 -4
- package/src/runner.ts +101 -15
- package/src/schema.ts +3 -3
- package/src/types.ts +88 -1
package/docs/RELEASING.md
CHANGED
|
@@ -1,32 +1,151 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
This is an independent community fork of `@parke.dev/pi-subagent
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
1
|
+
# Release and repository maintenance
|
|
2
|
+
|
|
3
|
+
This is an independent community fork of Luke Parke's `@parke.dev/pi-subagent`. Preserve the original [MIT license](../LICENSE), copyright and upstream attribution. Do not publish under the upstream scope. This standalone repository does not inherit the upstream monorepo's tag automation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
### Choose the scope first
|
|
8
|
+
|
|
9
|
+
A README or repository-presentation update does not require a new npm version, tag, GitHub Release, package installation or binary upload. An edit to a Release body should change only that body. A new package release is a separate operation with artifact verification and explicit publication approval.
|
|
10
|
+
|
|
11
|
+
Read [package.json](../package.json), [CHANGELOG.md](../CHANGELOG.md), the working tree and index before making changes. Record the original local HEAD, remote main and any remote metadata you intend to update. Preserve unrelated modifications and existing commit history by default. A separately approved single-root conversion must follow the history-replacement safeguards below; do not infer that permission from a documentation update or another project’s release rules.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
### Public source-tree boundary
|
|
16
|
+
|
|
17
|
+
Commit production source, the distributed [subagent skill](../skills/subagent/SKILL.md), public documentation and package metadata. Keep local maintainer tooling and private artifacts out of the public tree: `.trellis/`, `.agents/`, `.codex/`, project-local `.pi/` configuration, `AGENTS.md`, `LOCAL-PATCH.md`, Trellis-named support files, credentials, sessions, logs, dependencies, generated bundles and tarballs.
|
|
18
|
+
|
|
19
|
+
The product directory [skills/](../skills/) is not the local `.agents/skills/` directory. Never remove the product skill merely because local workflow skills are excluded. No Actions workflow or contributor guide is required for this manual release process.
|
|
20
|
+
|
|
21
|
+
Do not stage or discard local `.gitignore` edits. Where exclusion rules are maintained in `.git/info/exclude`, preserve its existing content and keep that file local. Ignore rules do not untrack files already in Git. When a reviewed cleanup needs to stop tracking local files, use `git rm --cached -- <explicit paths>` after backing them up and verifying their contents; never delete the working copies to clean the public tree. An ordinary commit does not erase those files from older history.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
### Documentation-only source updates
|
|
26
|
+
|
|
27
|
+
Keep [README.md](../README.md) and [README.zh-CN.md](../README.zh-CN.md) synchronized in the same change. Preserve the language switch, package identity, original copyright and license links. Each README ends with one Linux.do acknowledgement; do not expand it into a community/contact section. Do not add a documentation-index table, contribution guide or stale release-promotion block to the landing page.
|
|
28
|
+
|
|
29
|
+
Run the relevant [development checks](DEVELOPMENT.md), including relative-link and translation review, whitespace checks and the package dry run. A fresh checkout has no bundled test runner or typecheck script; do not claim upstream/private harness commands are available.
|
|
30
|
+
|
|
31
|
+
Stage only explicitly reviewed paths. For example, when both README files are the complete change:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git add -- README.md README.zh-CN.md
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Inspect the staged names:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git diff --cached --name-status
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Inspect the actual staged patch:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git diff --cached
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Create an ordinary commit after reviewing and approving its scope. Do not use broad `git add -A`, `--amend -a`, history squashing or a force push for documentation maintenance.
|
|
50
|
+
|
|
51
|
+
Before an authorized push, re-read remote main and compare it with the value saved at the start. Stop on an unexplained change rather than accepting a new baseline. The local branch can be named differently from remote main; target the reviewed commit deliberately:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git push origin HEAD:refs/heads/main
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Use a normal fast-forward push for ordinary maintenance. Afterward, confirm local HEAD and remote main match, read back affected files, and verify the intended current tree no longer exposes local-only paths. Update GitHub About fields only when that specific edit was approved, and re-read them afterward. Source push and About edits can succeed separately; report their actual status separately.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### Explicitly approved history replacement
|
|
62
|
+
|
|
63
|
+
Only replace the main history when the repository owner has explicitly requested that scope. Preserve the original graph in a local-only backup ref and an external Git bundle, verify the bundle in a separate repository, and retain working-file and index backups. Construct a parentless candidate from the reviewed public tree; verify its tree and that its reachable history contains exactly one commit before changing the local branch. Local maintenance files must remain on disk.
|
|
64
|
+
|
|
65
|
+
Before replacing remote main, obtain approval for the exact candidate and original remote SHA. Recheck remote state and use an explicit `--force-with-lease=refs/heads/main:<original-sha>` with only `<candidate-sha>:refs/heads/main`. Stop if the lease fails; never refresh the expected SHA to bypass it. Do not push backup refs, use an unqualified force or alter tags and other branches.
|
|
66
|
+
|
|
67
|
+
Read back the remote commit, parent list and tree after the update. A single-root main history does not erase old objects from GitHub caches, other clones or local backups. It also does not authorize npm publication, installation or About changes.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### Prepare an npm release
|
|
72
|
+
|
|
73
|
+
Only perform this section for an approved version release:
|
|
74
|
+
|
|
75
|
+
1. Choose an unpublished version and update package metadata and changelog together. Keep README install examples consistent with the intended release.
|
|
76
|
+
2. Run the checks available in the actual environment, as described in [development](DEVELOPMENT.md). A syntax transform is not a semantic typecheck. Record missing tooling and any separately configured typecheck/fixture results. Do not run real provider calls without permission.
|
|
77
|
+
3. Inspect the package dry-run list for unexpected files.
|
|
78
|
+
4. Pack the source, record the tarball's integrity, and test the packed source in an isolated installation where suitable offline verification is available. Do not imply a missing harness was run.
|
|
79
|
+
5. Review the final name, version, tarball, contents and verification results before requesting publication approval.
|
|
80
|
+
|
|
81
|
+
Dry-run contents:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm pack --dry-run --ignore-scripts --json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Create the release artifact only when preparing a release:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm pack --ignore-scripts
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Install the reviewed artifact into a separate temporary prefix using `--ignore-scripts --legacy-peer-deps` if installation checks are part of the approved release scope. Never replace the working Pi installation merely to inspect a package.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
### Publish with verified TLS
|
|
98
|
+
|
|
99
|
+
Use an existing npm login or a securely supplied environment-based credential. Never put tokens in command text, source files, a committed `.npmrc` or diagnostics.
|
|
100
|
+
|
|
101
|
+
If the environment contains `NODE_TLS_REJECT_UNAUTHORIZED=0`, remove that override before any credential-bearing registry operation. `--strict-ssl=true` does not undo a Node-level TLS override.
|
|
102
|
+
|
|
103
|
+
In Bash:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
unset NODE_TLS_REJECT_UNAUTHORIZED
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
In PowerShell:
|
|
110
|
+
|
|
111
|
+
```powershell
|
|
112
|
+
Remove-Item Env:NODE_TLS_REJECT_UNAUTHORIZED -ErrorAction SilentlyContinue
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Check the authenticated account without displaying credentials:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm whoami --strict-ssl=true --registry=https://registry.npmjs.org/
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
After explicit approval, publish the reviewed tarball. Replace `<version>` with the approved version:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npm publish "./cr1ms0n-pi-subagent-<version>.tgz" --access public --ignore-scripts --strict-ssl=true --registry=https://registry.npmjs.org/
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Read the exact version's registry metadata:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npm view "@cr1ms0n/pi-subagent@<version>" name version dist.integrity --strict-ssl=true --registry=https://registry.npmjs.org/
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Registry propagation can lag. A successful publish is not proof that the exact version is already readable. Verify name/version/integrity against the reviewed local artifact before replacing an installation; do not repeat a publication blindly after an uncertain response. All files in a published tarball become public.
|
|
134
|
+
|
|
135
|
+
GitHub tags, Releases and attachments are separate from npm publication. Do not create or modify them implicitly. If explicitly requested, inspect the existing remote objects, use a tool that actually supports the operation, update only the approved fields/assets, then read back and verify the result.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### Install and replace
|
|
140
|
+
|
|
141
|
+
Installation is a separate local change. After artifact verification and approval, replace `<version>` with the verified release:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
pi install "npm:@cr1ms0n/pi-subagent@<version>"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Back up the old package selection/source and keep it for rollback. Do not enable this fork and `@parke.dev/pi-subagent` simultaneously: both register the same tools. Verify the physical installed package version and files rather than relying only on a command's exit status or the settings entry. Reload or restart Pi after changing packages.
|
|
148
|
+
|
|
149
|
+
This fork uses the same configuration and persisted-state paths as upstream. Switching packages is not a data migration. Do not overwrite model choices or credentials during upgrades. Configure `jevRouting` in `~/.pi/subagent.json` before starting new tasks, as described in the [reference](REFERENCE.md#jev-routing).
|
|
150
|
+
|
|
151
|
+
Report preparation, publication, remote synchronization and local installation as distinct states, including checks that could not be performed.
|
package/docs/ROADMAP.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
+
> **Historical upstream roadmap.** Retained for rationale and deferred ideas, not as this fork's current release schedule. Phases 2-4 are recorded as shipped in [the historical execution plan](PLAN.md); their future-tense sketches below do not mean these features are missing. Jev routing supersedes the old model-selection assumptions. Use [the current reference](REFERENCE.md), [architecture contract](ARCHITECTURE.md) and [development checks](DEVELOPMENT.md) for current behavior and available tooling. Historical test claims are not verification results for this checkout.
|
|
4
|
+
|
|
3
5
|
> Execution details for the remaining phases (work breakdown, acceptance
|
|
4
6
|
> criteria, test plans, release gates) live in [PLAN.md](./PLAN.md). This
|
|
5
7
|
> document holds the rationale, design sketches, and deferral decisions.
|
package/docs/SECURITY.md
CHANGED
|
@@ -6,11 +6,11 @@ and can use tools according to their capability profile.
|
|
|
6
6
|
|
|
7
7
|
## What subagents can do
|
|
8
8
|
|
|
9
|
-
| Profile |
|
|
10
|
-
|
|
11
|
-
| `explore` |
|
|
12
|
-
| `review` |
|
|
13
|
-
| `general` | Jev-chosen subset of the full available locally permitted catalog
|
|
9
|
+
| Profile | Finalized tools | Writes? |
|
|
10
|
+
|---------|-----------------|---------|
|
|
11
|
+
| `explore` | Jev-chosen subset of locally permitted read-only candidates, plus available Pi context tools | No project-file writes |
|
|
12
|
+
| `review` | Same as explore | No project-file writes |
|
|
13
|
+
| `general` | Jev-chosen subset of the full available locally permitted catalog, plus available Pi context tools | Yes if write-capable tools are selected |
|
|
14
14
|
|
|
15
15
|
Parallel mode defaults to `explore` to avoid concurrent shared writes.
|
|
16
16
|
|
|
@@ -50,20 +50,20 @@ Parallel mode defaults to `explore` to avoid concurrent shared writes.
|
|
|
50
50
|
(RPC mode). Extension UI dialogs raised inside a child are auto-cancelled so
|
|
51
51
|
they can never hang a run — which also means a child can never obtain
|
|
52
52
|
interactive consent. Prefer restricted tools for async/background runs.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
7. **Steering messages** (`action: "steer"` and the overlay `s` key) inject text
|
|
54
|
+
into a running child's conversation with user-level authority. Anything that
|
|
55
|
+
can call the subagent tool can steer any live run in the same session.
|
|
56
|
+
8. **Process cleanup.** On POSIX, children run in their own process group so tree
|
|
57
57
|
kills work for ordinary descendants. Parent (re)start reaps orphans recorded
|
|
58
58
|
under `~/.pi/subagent-locks/runs/` so resume cannot race a still-alive writer.
|
|
59
59
|
Grandchildren that call `setsid()` can still escape a simple process-group kill.
|
|
60
|
-
|
|
60
|
+
9. **Resume exclusivity.** Direct resume takes a durable per-session file lock;
|
|
61
61
|
concurrent parents cannot append to the same child session.
|
|
62
|
-
|
|
62
|
+
10. **Profiles are tool-selection policy, not a sandbox.** Children inherit
|
|
63
63
|
`$HOME`, SSH/cloud credentials, network access, and the parent filesystem.
|
|
64
64
|
Git worktrees only isolate the checkout. For untrusted tasks, use an outer
|
|
65
65
|
container/cgroup/network policy.
|
|
66
|
-
|
|
66
|
+
11. **`max_cost` is accounting, not a hard provider gate.** Usage arrives after a
|
|
67
67
|
turn; orphans may spend money the ledger never sees. It caps provider-reported
|
|
68
68
|
execution cost only: TypeSafe reports routing tokens, not currency, so selector
|
|
69
69
|
cost is unreported and outside `max_cost`. Combine with provider account
|
|
@@ -81,12 +81,11 @@ and synthesis select from the new task instruction rather than the assembled
|
|
|
81
81
|
transcript. Task text and model descriptions are user content and can themselves
|
|
82
82
|
contain secrets; there is no guaranteed redaction.
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`Authorization` header to the fixed official HTTPS endpoint, with redirects
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
conversation.
|
|
84
|
+
Version `0.10.0` reads the TypeSafe credential from `jevRouting.apiKey` in the private user-level `~/.pi/subagent.json`. This is plaintext storage: restrict file access and protect editor backups and synchronized copies. Same-user processes, including children with filesystem access, may read it. Profiles and worktrees do not protect this file from those processes.
|
|
85
|
+
|
|
86
|
+
The transport sends the key only as an `Authorization` header to the fixed official HTTPS endpoint, with redirects disabled. It does not automatically copy the key into prompts, selector JSON bodies, argv, child manifests, logs, receipts or results. Never serialize or log the complete routing configuration. Rotate any credential pasted into a transcript or shared in conversation.
|
|
87
|
+
|
|
88
|
+
Published npm `0.9.0` uses the older environment-based mechanism. In `0.10.0`, `apiKeyEnv` is rejected with manual migration guidance and no environment fallback. Unrelated `PI_SUBAGENT_*` runtime settings remain supported.
|
|
90
89
|
|
|
91
90
|
New extension-managed dispatch is Pi-only. A `backend: "codex"` or
|
|
92
91
|
`backend: "claude"` new task is rejected before any selector or provider work,
|
package/docs/UX.md
CHANGED
|
@@ -41,14 +41,9 @@ The standalone pi-subagent provides rich TUI support for monitoring, inspecting,
|
|
|
41
41
|
per-task stats, and a one-line tail (live activity or first output line).
|
|
42
42
|
- Expanded (Ctrl+O / `app.tools.expand`): full task output capped with a dim
|
|
43
43
|
`… +N lines` trailer pointing at the artifact/child session.
|
|
44
|
-
- Expanded detail adds
|
|
45
|
-
execution model, selected tools (plus locally added control-plane tools),
|
|
46
|
-
selector version, confidence, outcome and selection latency. Legacy runs
|
|
47
|
-
simply have no route line.
|
|
44
|
+
- Expanded detail adds a bounded route summary for Jev-routed runs: original selection, ranked probabilities, shared selected tools (plus locally added control-plane tools), selector version, answer-level confidence, outcome and selection latency. The actual execution model stays separate from the original choice. Large lists show a preview and total count; old runs without new fields remain readable. Compact results and completion notifications show at most the last five attempt models and label a shortened chain with its total attempt count.
|
|
48
45
|
- Durations freeze at `endedAt`; running durations tick at render time.
|
|
49
|
-
- Reliability annotations render inline: `[attempt 2]` during a
|
|
50
|
-
retry, `[stalled 2m]` while the stall watchdog is flagging silence, and
|
|
51
|
-
`◐ wrapped up` on budget-stopped runs that concluded gracefully.
|
|
46
|
+
- Reliability annotations render inline: `[attempt 2]` during retry/failover, the actual attempt model and bounded attempt chain, `[stalled 2m]` while the stall watchdog is flagging silence, and `◐ wrapped up` on budget-stopped runs that concluded gracefully. Availability failures can switch models only before tools begin; a stalled indicator is not a promise of another attempt.
|
|
52
47
|
|
|
53
48
|
### Footer status
|
|
54
49
|
Terse and actionable only: `⚙ 2 running · 1 ready · /subagents`. Cleared when
|
|
@@ -80,6 +75,7 @@ text with run ids and a `wait { id }` pointer.
|
|
|
80
75
|
failures bypass batching and flush immediately, carrying held successes.
|
|
81
76
|
- A `wait` that already delivered the run suppresses the redundant
|
|
82
77
|
notification (delivered-state is re-checked at flush time).
|
|
78
|
+
- Model/attempt annotations use the actual execution history, not the immutable original Jev choice. Fallback creates no extra completion notification or delivery path.
|
|
83
79
|
|
|
84
80
|
### `/subagents` overlay
|
|
85
81
|
- Header: title + running/ready counters + full usage ledger + rule.
|
|
@@ -129,11 +125,11 @@ usage and a bounded `Optional synthesis blocked: …` diagnostic instead of
|
|
|
129
125
|
discarding or re-routing them.
|
|
130
126
|
|
|
131
127
|
### Plan results (tool output, not TUI)
|
|
132
|
-
`action:"plan"` returns the
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
and
|
|
128
|
+
`action:"plan"` returns the initial model, a bounded probability-ranked candidate preview, common tools, effective total attempt limit and selector usage. Probabilities are selector preferences, not uptime estimates, and `confidence` belongs to the original answer. A plan reports no actual attempts; a later invocation selects again. It starts no child and creates no run entry, so plan never adds an overlay row or ambient widget. A plan whose optional synthesis selection fails still returns the valid worker plan and labels only that stage blocked with its diagnostic.
|
|
129
|
+
|
|
130
|
+
### Failed-attempt output
|
|
131
|
+
|
|
132
|
+
Earlier attempts retain bounded, attributed output previews and child-session pointers. If all attempts fail, the final state, failure reason, model and session stay authoritative; earlier text does not become that model's answer or a successful structured result. A human-readable earlier preview names its source attempt. Successful final JSON is never concatenated with failed JSON. Full output remains in existing child sessions, which are retained while referenced on the active branch. Compact views remain within their existing output limits.
|
|
137
133
|
|
|
138
134
|
## States
|
|
139
135
|
- **Queued/Running**: spinner + live stats + activity tail from live text.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cr1ms0n/pi-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Community fork of Luke Parke's pi-subagent with Jev model/tool routing and verified Pi child capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
"cr1ms0n (fork maintainer)"
|
|
11
11
|
],
|
|
12
12
|
"homepage": "https://www.npmjs.com/package/@cr1ms0n/pi-subagent",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/awoaCrim/pi-smart-subagents.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/awoaCrim/pi-smart-subagents/issues"
|
|
19
|
+
},
|
|
13
20
|
"keywords": [
|
|
14
21
|
"pi-package",
|
|
15
22
|
"pi",
|
|
@@ -31,6 +38,7 @@
|
|
|
31
38
|
"skills",
|
|
32
39
|
"docs",
|
|
33
40
|
"README.md",
|
|
41
|
+
"README.zh-CN.md",
|
|
34
42
|
"CHANGELOG.md",
|
|
35
43
|
"LICENSE"
|
|
36
44
|
],
|
package/skills/subagent/SKILL.md
CHANGED
|
@@ -109,7 +109,7 @@ Pi path are **refused**, not silently degraded:
|
|
|
109
109
|
- Prefer `max_turns`, `max_cost`, and/or `timeout_ms` on long or write-capable runs.
|
|
110
110
|
`timeout_ms` is absolute: local preflight, Jev selection, setup, queue and
|
|
111
111
|
runtime all count against it.
|
|
112
|
-
- `output_schema` asks the child for a fenced `json:result` block
|
|
112
|
+
- `output_schema` asks the child for a fenced `json:result` block. An otherwise successful invalid answer gets one repair round; a failed provider attempt neither repairs nor publishes structured output.
|
|
113
113
|
- `context: "fork"` continues from a fork of the parent session.
|
|
114
114
|
- Do not poll `status` in a tight loop. Use `wait` / `subagent_wait`, or let the
|
|
115
115
|
completion notification arrive for `async: true` runs.
|
|
@@ -121,22 +121,27 @@ Pi path are **refused**, not silently degraded:
|
|
|
121
121
|
|
|
122
122
|
Omit `model` and `fallback_models` on every new call: both are legacy fields,
|
|
123
123
|
and an explicit value is rejected rather than bypassing selection. Jev chooses
|
|
124
|
-
|
|
125
|
-
include/exclude decision per eligible tool. The local policy then re-validates
|
|
124
|
+
an initial execution model and probabilities for the user's eligible candidates, plus one task-based include/exclude decision per eligible tool shared by all attempts. The local policy then re-validates
|
|
126
125
|
the answer: unknown or unsafe tools cannot launch, explore/review stay read-only,
|
|
127
126
|
and management actions need no routing config or credential.
|
|
128
127
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
A Jev timeout or invalid decision still stops new dispatch; there is no emergency model. A valid route retains every candidate probability and tries higher values first. Tied maxima keep the returned choice first; other ties follow configured order. Low confidence and zero probability are accepted, not thresholds. Per-model probability is a selector preference, not uptime or a separate confidence score.
|
|
129
|
+
|
|
130
|
+
Recognized settled model-unavailable, temporary rate-limit/service and transport errors can advance to the next candidate only before any tool execution begins in the current invocation. Once a tool starts, or protocol evidence is uncertain, do not restart the child on another or the same model. Auth/configuration, quota/billing, context, invalid requests, task/schema quality, cancellation and exhausted budgets never trigger model switching. Historical resume/fork messages are not new tool execution.
|
|
131
|
+
|
|
132
|
+
`max_retries` limits all extension-level extra attempts: 0 means one initial attempt; 2 means at most three attempts. The built-in default remains 1. Availability failure advances directly to the next candidate; candidate exhaustion never wraps. Conclusively pre-work infrastructure failures may retry the same model within that budget. Every attempt shares tools, absolute deadline and cumulative reported cost/turn budgets, with fresh exact-model/tool startup verification. Switching makes no extra Jev call. Pi's internal provider retries are separate, unchanged and may delay fallback.
|
|
133
|
+
|
|
134
|
+
Plan/status distinguish original choice, ranked alternatives and actual attempts. Earlier failed output is retained as attributed previews/session pointers, not mixed into a later structured answer. All-failed tasks keep their final failure. Existing runs remain manageable without selector configuration or a credential.
|
|
134
135
|
|
|
135
136
|
An optional candidate `thinking` value is an opaque Pi thinking-level string;
|
|
136
137
|
common values include `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and
|
|
137
138
|
`max`, but model-specific values are passed through unchanged. It is a default:
|
|
138
139
|
explicit task, agent, and profile `taskDefaults.thinking` values override it.
|
|
139
|
-
The extension re-reads `jevRouting` on each dispatch and injects
|
|
140
|
-
routing guidance into the parent prompt.
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
The extension re-reads `jevRouting` on each dispatch and injects non-secret
|
|
141
|
+
routing guidance into the parent prompt. The user stores the TypeSafe credential
|
|
142
|
+
in `jevRouting.apiKey` in the private `~/.pi/subagent.json`; do not read, display
|
|
143
|
+
or copy the key into task text, prompts or output. Legacy `apiKeyEnv` is rejected
|
|
144
|
+
with migration guidance; there is no environment fallback. If the config or key
|
|
145
|
+
is missing or invalid, management remains available but new spawns, `/btw`, plan,
|
|
146
|
+
resume, fork and synthesis are rejected. This config-file credential contract ships in
|
|
147
|
+
npm 0.10.0; published npm 0.9.0 uses the old environment mechanism.
|
package/src/backend.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import type { ProtocolUpdate } from "./protocol.js";
|
|
27
|
-
import type { TaskResult, TaskSpec } from "./types.js";
|
|
27
|
+
import type { TaskResult, TaskSpec, ToolActivity } from "./types.js";
|
|
28
28
|
|
|
29
29
|
/** Normalized event-stream parser contract, implemented per backend. */
|
|
30
30
|
export interface BackendParser {
|
|
@@ -40,6 +40,21 @@ export interface BackendParser {
|
|
|
40
40
|
getLiveText(): string;
|
|
41
41
|
/** Completed messages so far. */
|
|
42
42
|
getMessages(): import("@earendil-works/pi-ai").Message[];
|
|
43
|
+
/**
|
|
44
|
+
* Optional sticky current-invocation tool-activity observation used by the
|
|
45
|
+
* ranked failover gate. Parsers that do not implement it provide no
|
|
46
|
+
* conclusive evidence (callers must treat activity as unknown on the ranked
|
|
47
|
+
* path); the legacy unranked path is unaffected.
|
|
48
|
+
*/
|
|
49
|
+
getToolActivity?(): ToolActivity | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Optional stop reason of the latest completed assistant message, used to
|
|
52
|
+
* suppress the ranked structured-output repair prompt and final publication
|
|
53
|
+
* after a settled provider error/abort. Absent means "not observable".
|
|
54
|
+
*/
|
|
55
|
+
getAssistantStopReason?(): string | undefined;
|
|
56
|
+
/** Latest completed assistant text; empty must not fall back to earlier turns. */
|
|
57
|
+
getAssistantText?(): string | undefined;
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
export interface BackendInvocation {
|
package/src/config.ts
CHANGED
|
@@ -61,7 +61,7 @@ export interface SubagentConfig {
|
|
|
61
61
|
lockRetentionDays: number;
|
|
62
62
|
/** Legacy per-profile defaults. Model/fallback fields are retained for config compatibility but ignored by Jev routing. */
|
|
63
63
|
taskDefaults?: TaskDefaultsByProfile;
|
|
64
|
-
/**
|
|
64
|
+
/** Private Jev configuration including apiKey. Never log or serialize the whole config. */
|
|
65
65
|
jevRouting?: JevRoutingConfig;
|
|
66
66
|
/** Safe migration or parse failure; existing-run management remains available. */
|
|
67
67
|
jevRoutingError?: string;
|
package/src/extension.ts
CHANGED
|
@@ -8,10 +8,13 @@ import { Value } from "typebox/value";
|
|
|
8
8
|
import { defaultConfig, loadConfig, readConfigFile, type SubagentConfig } from "./config.js";
|
|
9
9
|
import {
|
|
10
10
|
formatDuration,
|
|
11
|
+
formatRankedPreview,
|
|
11
12
|
formatStatusPreview,
|
|
12
13
|
formatTokens,
|
|
13
14
|
isActiveState,
|
|
14
15
|
oneLine,
|
|
16
|
+
projectRoutingForDisplay,
|
|
17
|
+
projectAttemptsForDisplay,
|
|
15
18
|
renderCallLine,
|
|
16
19
|
renderRunLines,
|
|
17
20
|
SPINNERS,
|
|
@@ -25,7 +28,7 @@ import { runTasks } from "./orchestrator.js";
|
|
|
25
28
|
import { OutputManager } from "./output.js";
|
|
26
29
|
import { parseDepth, parseSpawnPolicy, SPAWNS_ENV_VAR, validateSubagentRequest, type PreparedTask, type ParentContext, type PreparationOptions, type ResolvedTask } from "./policy.js";
|
|
27
30
|
import type { ChildRunner } from "./runner.js";
|
|
28
|
-
import { ProcessLockManager } from "./process-lock.js";
|
|
31
|
+
import { ProcessLockManager, runRecordSessionIds } from "./process-lock.js";
|
|
29
32
|
import { SessionScopedRunRegistry, snapshotFromLiveRun } from "./registry.js";
|
|
30
33
|
import {
|
|
31
34
|
ProviderSubagentParamsSchema,
|
|
@@ -49,6 +52,7 @@ import { eligibleModelCandidates, formatJevRoutingPrompt, toToolCandidates } fro
|
|
|
49
52
|
import { JevRouter } from "./jev-router.js";
|
|
50
53
|
import { routePreparedTasks, type RoutingCatalog } from "./dispatch-routing.js";
|
|
51
54
|
import { runLocalPreflights } from "./dispatch-preflight.js";
|
|
55
|
+
import { rankedMaxAttempts } from "./model-failover.js";
|
|
52
56
|
import type { RoutingReceipt } from "./routing-types.js";
|
|
53
57
|
import { buildRoutingEvent, foldRoutingReceipts, MAX_ROUTING_DELIVERY_IDS, ROUTING_ENTRY_TYPE, type PersistedRoutingEvent } from "./persistence.js";
|
|
54
58
|
|
|
@@ -318,7 +322,7 @@ function compactDetails(
|
|
|
318
322
|
errorMessage: result.errorMessage?.slice(0, 1_000),
|
|
319
323
|
usage: result.usage ?? emptyUsage(),
|
|
320
324
|
model: result.model,
|
|
321
|
-
routing: result.routing,
|
|
325
|
+
routing: projectRoutingForDisplay(result.routing),
|
|
322
326
|
thinking: result.thinking,
|
|
323
327
|
profile: result.profile,
|
|
324
328
|
canWrite: result.canWrite,
|
|
@@ -332,7 +336,8 @@ function compactDetails(
|
|
|
332
336
|
wrappedUp: result.wrappedUp,
|
|
333
337
|
stalledSince: result.stalledSince,
|
|
334
338
|
attempts: result.attempts,
|
|
335
|
-
|
|
339
|
+
...projectAttemptsForDisplay(result, Math.min(4_096, perResultText)),
|
|
340
|
+
toolActivity: result.toolActivity,
|
|
336
341
|
structuredOutput: result.structuredOutput,
|
|
337
342
|
structuredError: result.structuredError,
|
|
338
343
|
})),
|
|
@@ -430,7 +435,7 @@ async function runPlanPreflights(
|
|
|
430
435
|
});
|
|
431
436
|
}
|
|
432
437
|
|
|
433
|
-
function formatPlanEntry(task: ResolvedTask, index: number) {
|
|
438
|
+
function formatPlanEntry(task: ResolvedTask, index: number, maxRetriesDefault: number) {
|
|
434
439
|
const agentNote = task.resolutionNotes.find((note) => note.startsWith("agent="));
|
|
435
440
|
const agent = agentNote?.slice("agent=".length);
|
|
436
441
|
return {
|
|
@@ -438,12 +443,16 @@ function formatPlanEntry(task: ResolvedTask, index: number) {
|
|
|
438
443
|
label: task.label,
|
|
439
444
|
agent,
|
|
440
445
|
model: task.model,
|
|
441
|
-
|
|
446
|
+
// The ranked route replaces legacy fallback semantics for this path: show
|
|
447
|
+
// the bounded ranked preview and the effective extension attempt budget.
|
|
448
|
+
rankedPreview: formatRankedPreview(task.routing?.rankedModels),
|
|
449
|
+
rankedTotal: task.routing?.rankedModels?.length,
|
|
450
|
+
maxAttempts: rankedMaxAttempts(task.maxRetries ?? maxRetriesDefault),
|
|
442
451
|
thinking: task.thinking,
|
|
443
452
|
profile: task.profile,
|
|
444
453
|
access: task.canWrite ? "RW" : "RO" as const,
|
|
445
454
|
tools: task.effectiveTools,
|
|
446
|
-
routing: task.routing,
|
|
455
|
+
routing: projectRoutingForDisplay(task.routing),
|
|
447
456
|
budgets: {
|
|
448
457
|
timeoutMs: task.timeoutMs,
|
|
449
458
|
maxTurns: task.maxTurns,
|
|
@@ -466,8 +475,10 @@ function formatPlanText(mode: "single" | "parallel", plan: ReturnType<typeof for
|
|
|
466
475
|
].filter(Boolean).join(" ");
|
|
467
476
|
return [
|
|
468
477
|
`${entry.index + 1}. ${entry.label}${entry.agent ? ` [agent:${entry.agent}]` : ""} (${entry.profile}/${entry.access})`,
|
|
469
|
-
` model=${entry.model ?? "(none)"}
|
|
470
|
-
`
|
|
478
|
+
` model=${entry.model ?? "(none)"} thinking=${entry.thinking ?? "(default)"} isolation=${entry.isolation}`,
|
|
479
|
+
` ranked_models=[${entry.rankedPreview ?? entry.model ?? "(none)"}]${entry.rankedTotal && entry.rankedTotal > 5 ? ` (total ${entry.rankedTotal})` : ""}`,
|
|
480
|
+
` attempt_budget=${entry.maxAttempts} (max_retries limits EXTRA extension-level attempts; pre-tool availability failure advances the ranking, never wraps)`,
|
|
481
|
+
` shared_tools=[${entry.tools.join(",")}]`,
|
|
471
482
|
` ${budgets}`,
|
|
472
483
|
` notes: ${entry.resolutionNotes.join(", ")}`,
|
|
473
484
|
].join("\n");
|
|
@@ -502,7 +513,7 @@ function guidelines(catalog?: Map<string, AgentDefinition>): string[] {
|
|
|
502
513
|
"Profiles: explore/review are strictly read-only (safe for fanout); general offers the full available locally permitted catalog to Jev and may write. Explicit tools are a ceiling; agent tool defaults do not narrow candidates. Single tasks default to general, parallel tasks to explore.",
|
|
503
514
|
"Parallel writers need isolation:'worktree' (each gets an isolated checkout; changed work lands on a branch). After a worktree run finishes, use action:'diff' to inspect, then 'apply' to bring changes into the main checkout or 'discard' to drop them.",
|
|
504
515
|
"Set budgets: at max_turns/max_cost the child is steered to wrap up and given grace turns for a final answer (grace_turns tunes this); results end as 'partial' with wrappedUp:true when the child concluded. timeout_ms includes Jev selection, setup, queue and retries; max_cost excludes unreported TypeSafe currency; timeout results report the phase.",
|
|
505
|
-
"Transient child failures may retry
|
|
516
|
+
"Transient child failures may retry within the same invocation: ranked Jev routes advance to the next probability-ranked candidate only for a recognized model-availability failure that settles before any tool execution, sharing one task-based tool set and the total max_retries attempt budget (0 = first attempt only; never wraps back). A tool that started, uncertain evidence, or auth/quota/context/schema failures stop without switching. No selector retries or emergency models are used. Task-quality failures never retry.",
|
|
506
517
|
"context:'fork' starts a single child from a branched copy of this conversation — use it when the task depends on discussion context instead of re-explaining. Single-task only.",
|
|
507
518
|
"Use async:true only when you have independent work meanwhile; then use action:'wait' with the run id (interruptible, does not cancel). action:'steer' injects mid-run guidance into a running child instead of cancel + retry.",
|
|
508
519
|
"For parallel research, add synthesis:'<instruction>' to have one read-only child fold all outputs into a single brief, delivered first.",
|
|
@@ -614,7 +625,8 @@ function buildCompletionDetails(runtime: SessionRuntime, runIds: string[]): Comp
|
|
|
614
625
|
tokens: (result.usage?.input ?? 0) + (result.usage?.output ?? 0),
|
|
615
626
|
cost: result.usage?.cost ?? 0,
|
|
616
627
|
model: result.model,
|
|
617
|
-
|
|
628
|
+
attempts: result.attempts,
|
|
629
|
+
attemptedModels: projectAttemptsForDisplay({ attemptedModels: result.attemptedModels }).attemptedModels,
|
|
618
630
|
pointers: taskPointers,
|
|
619
631
|
};
|
|
620
632
|
});
|
|
@@ -632,6 +644,7 @@ function buildCompletionDetails(runtime: SessionRuntime, runIds: string[]): Comp
|
|
|
632
644
|
// Preserve the old top-level fields only for single-task consumers. The
|
|
633
645
|
// complete per-task model/attempt data lives in tasks[].
|
|
634
646
|
model: tasks.length === 1 ? first?.model : undefined,
|
|
647
|
+
attempts: tasks.length === 1 ? first?.attempts : undefined,
|
|
635
648
|
attemptedModels: tasks.length === 1 ? first?.attemptedModels : undefined,
|
|
636
649
|
pointers,
|
|
637
650
|
tasks,
|
|
@@ -665,7 +678,7 @@ function scheduleMaintenance(runtime: SessionRuntime): void {
|
|
|
665
678
|
const keep = new Set(runtime.registry.planSessionRetention().keep);
|
|
666
679
|
const busy = new Set<string>();
|
|
667
680
|
for (const record of runtime.locks.listRunRecords()) {
|
|
668
|
-
if (record.state === "running"
|
|
681
|
+
if (record.state === "running") for (const id of runRecordSessionIds(record)) busy.add(id);
|
|
669
682
|
}
|
|
670
683
|
for (const run of runtime.registry.getLiveRuns(runtime.key)) {
|
|
671
684
|
for (const id of run.childSessionIds) busy.add(id);
|
|
@@ -912,6 +925,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
912
925
|
state: run.state,
|
|
913
926
|
preview: run.preview,
|
|
914
927
|
model: run.model,
|
|
928
|
+
attempts: run.attempts,
|
|
915
929
|
attemptedModels: run.attemptedModels,
|
|
916
930
|
pointers: run.pointers,
|
|
917
931
|
turns: run.turns,
|
|
@@ -920,7 +934,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
920
934
|
}];
|
|
921
935
|
return tasks.map((task) => {
|
|
922
936
|
const attemptText = task.attemptedModels && task.attemptedModels.length > 1
|
|
923
|
-
? `; attempts: ${task.attemptedModels.join(" → ")}`
|
|
937
|
+
? `; attempts${task.attempts ? ` (${task.attempts} total)` : ""}: ${task.attemptedModels.join(" → ")}`
|
|
924
938
|
: "";
|
|
925
939
|
const label = tasks.length > 1 ? `${run.label}/${task.label}` : task.label;
|
|
926
940
|
return `- [${run.id.slice(0, 8)}] ${label}: ${task.state}${task.model ? ` on ${task.model}` : ""}${attemptText}${task.preview ? ` — ${task.preview}` : ""}${task.pointers.length ? ` (${task.pointers.join(", ")})` : ""}`;
|
|
@@ -1025,6 +1039,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
1025
1039
|
tokens: run.tokens,
|
|
1026
1040
|
cost: run.cost,
|
|
1027
1041
|
model: run.model,
|
|
1042
|
+
attempts: run.attempts,
|
|
1028
1043
|
attemptedModels: run.attemptedModels,
|
|
1029
1044
|
pointers: run.pointers,
|
|
1030
1045
|
}];
|
|
@@ -1041,7 +1056,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
1041
1056
|
lines.push(truncateToWidth(`${glyph} ${theme.fg("dim", task.model ?? "model unknown")} · ${theme.bold(theme.fg("toolTitle", label))} ${theme.fg("dim", `[${run.id.slice(0, 8)}] ${stats}`)}`, width));
|
|
1042
1057
|
if (task.preview) lines.push(truncateToWidth(` ${theme.fg("dim", "⎿")} ${theme.fg("toolOutput", task.preview)}`, width));
|
|
1043
1058
|
if (task.attemptedModels && task.attemptedModels.length > 1) {
|
|
1044
|
-
lines.push(truncateToWidth(` ${theme.fg("warning", `models: ${task.attemptedModels.join(" → ")}`)}`, width));
|
|
1059
|
+
lines.push(truncateToWidth(` ${theme.fg("warning", `models: ${task.attemptedModels.join(" → ")}${task.attempts && task.attempts > task.attemptedModels.length ? ` (last ${task.attemptedModels.length} of ${task.attempts})` : ""}`)}`, width));
|
|
1045
1060
|
}
|
|
1046
1061
|
if ((expanded || tasks.length === 1) && task.pointers.length) {
|
|
1047
1062
|
lines.push(truncateToWidth(theme.fg("dim", ` ${task.pointers.join(" · ")}`), width));
|
|
@@ -1339,7 +1354,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
1339
1354
|
const planned = await routePreparedTasks(synthetic, catalog, router, {
|
|
1340
1355
|
purpose: "plan", signal: routingScope.controller.signal, assertOwner: routingScope.assertOwner,
|
|
1341
1356
|
});
|
|
1342
|
-
synthesis = { state: "resolved", plan: formatPlanEntry(planned[0]!, 0) };
|
|
1357
|
+
synthesis = { state: "resolved", plan: formatPlanEntry(planned[0]!, 0, runtime.config.maxRetries) };
|
|
1343
1358
|
} catch (error) {
|
|
1344
1359
|
routingScope.assertOwner();
|
|
1345
1360
|
synthesis = { state: "blocked", error: error instanceof Error ? error.message : "Optional synthesis routing failed." };
|
|
@@ -1348,7 +1363,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
1348
1363
|
routingScope.assertOwner();
|
|
1349
1364
|
await requireRoutingPersistence(runtime);
|
|
1350
1365
|
routingScope.assertOwner();
|
|
1351
|
-
const plan = resolved.map((task, index) => formatPlanEntry(task, index));
|
|
1366
|
+
const plan = resolved.map((task, index) => formatPlanEntry(task, index, runtime.config.maxRetries));
|
|
1352
1367
|
const mode = validated.mode as "single" | "parallel";
|
|
1353
1368
|
const receipts = [...routingScope.receipts.values()];
|
|
1354
1369
|
const selectorUsage = await claimRoutingUsage(runtime, { ids: new Set(receipts.map((receipt) => receipt.requestId)) });
|
|
@@ -1612,6 +1627,9 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
1612
1627
|
wrappedUp: task.wrappedUp,
|
|
1613
1628
|
stalledSince: task.stalledSince,
|
|
1614
1629
|
attempts: task.attempts,
|
|
1630
|
+
attemptedModels: task.attemptedModels,
|
|
1631
|
+
toolActivity: task.toolActivity,
|
|
1632
|
+
modelAttempts: task.modelAttempts,
|
|
1615
1633
|
structuredOutput: task.structuredOutput,
|
|
1616
1634
|
structuredError: task.structuredError,
|
|
1617
1635
|
})),
|