@mystilleef/pi-subagent 0.3.1 → 0.4.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 +169 -11
- package/package.json +30 -12
- package/src/cancel-command.ts +3 -1
- package/src/index.ts +5 -0
- package/src/instance-name.ts +164 -0
- package/src/jobs-command.ts +41 -0
- package/src/progress-state.ts +80 -0
- package/src/progress.ts +100 -111
- package/src/run-registry.ts +1 -0
- package/src/subagent-orchestrator.ts +74 -16
- package/src/termination.ts +7 -6
- package/src/types.ts +1 -0
- package/src/ui.ts +198 -15
package/README.md
CHANGED
|
@@ -1,38 +1,81 @@
|
|
|
1
1
|
# Subagent
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[
|
|
3
|
+
`pi-subagent` adds isolated subagent orchestration to
|
|
4
|
+
[Pi](https://github.com/earendil-works/pi). It provides a `subagent`
|
|
5
|
+
tool and `/run` command for delegating work to specialized agents in
|
|
6
|
+
separate child Pi processes. Designed especially for the `SPAE`
|
|
7
|
+
framework, but doesn't require it.
|
|
8
|
+
|
|
9
|
+
## Action
|
|
10
|
+
|
|
11
|
+
Agents in
|
|
12
|
+
[action](https://raw.githubusercontent.com/mystilleef/pi-subagent/main/assets/parallel-agents-demo.mp4).
|
|
5
13
|
|
|
6
14
|
## Installation
|
|
7
15
|
|
|
16
|
+
**Install from `npm`:**
|
|
17
|
+
|
|
8
18
|
```sh
|
|
9
19
|
pi install npm:@mystilleef/pi-subagent
|
|
10
20
|
```
|
|
11
21
|
|
|
22
|
+
**Try temporarily without installing:**
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pi -e npm:@mystilleef/pi-subagent
|
|
26
|
+
```
|
|
27
|
+
|
|
12
28
|
## Features
|
|
13
29
|
|
|
14
|
-
- **Asynchronous:** Agents
|
|
15
|
-
- **Parallel:** Run
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
30
|
+
- **Asynchronous:** Agents run in the background.
|
|
31
|
+
- **Parallel:** Run many agents simultaneously.
|
|
32
|
+
- **Isolated:** Each delegated task receives a separate context window.
|
|
33
|
+
- **Simple:** No complex orchestration workflow required.
|
|
34
|
+
- **Bloat-free:** No bundled agents.
|
|
18
35
|
|
|
19
36
|
## Usage
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
**Run an agent with an optional task:**
|
|
22
39
|
|
|
23
40
|
```text
|
|
24
41
|
/run agent [optional task]
|
|
25
42
|
```
|
|
26
43
|
|
|
27
|
-
|
|
44
|
+
**Examples:**
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
/run spec implement google login screen
|
|
48
|
+
/run plan
|
|
49
|
+
/run inspect
|
|
50
|
+
/run build
|
|
51
|
+
/run verify
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Use natural language to launch agents in parallel:**
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
use the work agent to write a poem about linux; use the commit agent to make
|
|
58
|
+
commits; use the query agent to summarize the project.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Show active and completed jobs:**
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
/jobs
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Cancel running `subagents`:**
|
|
28
68
|
|
|
29
69
|
```text
|
|
30
70
|
/cancel-subagent
|
|
31
71
|
```
|
|
32
72
|
|
|
33
|
-
## Workflow
|
|
73
|
+
## _SPAE_ Workflow
|
|
34
74
|
|
|
35
|
-
|
|
75
|
+
`pi-subagent` supports the
|
|
76
|
+
[`SPAE` Framework](https://github.com/mystilleef/spae-framework), but
|
|
77
|
+
doesn't require it. `SPAE` provides pre-built agents and skills for a
|
|
78
|
+
structured workflow.
|
|
36
79
|
|
|
37
80
|
| Phase | Agent | Purpose |
|
|
38
81
|
| ----- | ------------------------- | --------------------------------------------- |
|
|
@@ -42,4 +85,119 @@ The [SPAE Framework](https://github.com/mystilleef/spae-framework) emphasizes a
|
|
|
42
85
|
| 4 | `/run build` | Carry out tasks from `PLAN.md` |
|
|
43
86
|
| 5 | `/run verify` | Verify implementation against `SPEC.md` |
|
|
44
87
|
|
|
45
|
-
|
|
88
|
+
## Agent definitions
|
|
89
|
+
|
|
90
|
+
This package ships no agents. Define agents as Markdown files with YAML
|
|
91
|
+
`frontmatter` and a Markdown system prompt body.
|
|
92
|
+
|
|
93
|
+
**Discovery locations:**
|
|
94
|
+
|
|
95
|
+
- User-global agents: `~/.pi/agents/*.md`
|
|
96
|
+
- Project-local agents: nearest `.pi/agents/*.md`
|
|
97
|
+
|
|
98
|
+
**Required `frontmatter`:**
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
name: review
|
|
102
|
+
description: Review code for correctness and maintainability.
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Optional `frontmatter`:**
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
tools: read, bash, edit
|
|
109
|
+
skills: code-review
|
|
110
|
+
thinking: medium
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Accepted `thinking` values:**
|
|
114
|
+
|
|
115
|
+
- `off`
|
|
116
|
+
- `minimal`
|
|
117
|
+
- `low`
|
|
118
|
+
- `medium`
|
|
119
|
+
- `high`
|
|
120
|
+
- `xhigh`
|
|
121
|
+
|
|
122
|
+
## Tool
|
|
123
|
+
|
|
124
|
+
The extension also registers a `subagent` tool for model-driven
|
|
125
|
+
delegation.
|
|
126
|
+
|
|
127
|
+
**Inputs:**
|
|
128
|
+
|
|
129
|
+
- `agent`: agent name.
|
|
130
|
+
- `task`: task prompt for the child agent.
|
|
131
|
+
- `agentScope`: optional lookup scope, one of `user`, `project`, or
|
|
132
|
+
`both`.
|
|
133
|
+
- `debug`: optional flag that includes full child messages in result
|
|
134
|
+
details.
|
|
135
|
+
|
|
136
|
+
## Security
|
|
137
|
+
|
|
138
|
+
`Subagents` launch child `pi --json` processes. Agents, tools, and
|
|
139
|
+
extensions run with user permissions, so treat agent definitions like
|
|
140
|
+
executable automation.
|
|
141
|
+
|
|
142
|
+
**Trust guidance:**
|
|
143
|
+
|
|
144
|
+
- Review project-local agents before running them.
|
|
145
|
+
- Avoid delegating secrets unless the agent and tools need them.
|
|
146
|
+
- Prefer trusted repositories for shared agent definitions.
|
|
147
|
+
- Remember that child agents can call their configured tools.
|
|
148
|
+
|
|
149
|
+
## Configuration and limits
|
|
150
|
+
|
|
151
|
+
**Environment variables:**
|
|
152
|
+
|
|
153
|
+
- `PI_SUBAGENT_DEPTH`: nested subagent depth guard. Nested calls stop at
|
|
154
|
+
depth `1`.
|
|
155
|
+
- `PI_SUBAGENT_MAX_OUTPUT_BYTES`: max returned output bytes. Default:
|
|
156
|
+
`50000`.
|
|
157
|
+
- `PI_SUBAGENT_MAX_OUTPUT_LINES`: max returned output lines. Default:
|
|
158
|
+
`500`.
|
|
159
|
+
|
|
160
|
+
## Troubleshooting
|
|
161
|
+
|
|
162
|
+
**Missing agent:**
|
|
163
|
+
|
|
164
|
+
- Confirm the file lives under `~/.pi/agents/` or the nearest
|
|
165
|
+
`.pi/agents/`.
|
|
166
|
+
- Confirm `frontmatter` includes `name` and `description`.
|
|
167
|
+
- Confirm `/run` uses the `name` value, not the filename.
|
|
168
|
+
|
|
169
|
+
**Project-local agent prompt:**
|
|
170
|
+
|
|
171
|
+
- Pi may request confirmation before loading project-local agents when
|
|
172
|
+
UI context exists.
|
|
173
|
+
|
|
174
|
+
**Nested subagent blocked:**
|
|
175
|
+
|
|
176
|
+
- Nested delegation hits the `PI_SUBAGENT_DEPTH` safety limit.
|
|
177
|
+
- Run the child task directly from the parent session instead.
|
|
178
|
+
|
|
179
|
+
**Truncated output:**
|
|
180
|
+
|
|
181
|
+
- Raise `PI_SUBAGENT_MAX_OUTPUT_BYTES` or
|
|
182
|
+
`PI_SUBAGENT_MAX_OUTPUT_LINES`.
|
|
183
|
+
- Ask the child agent for a shorter summary.
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
**Install dependencies:**
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
bun install
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Run full verification:**
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
bun verify
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Check npm package contents:**
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
bun pack:smoke
|
|
203
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mystilleef/pi-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Pi subagent for the SPAE Framework",
|
|
5
5
|
"author": "Lateef Alabi-Oki <mystilleef@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/mystilleef/pi-subagent.git"
|
|
10
10
|
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/mystilleef/pi-subagent/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/mystilleef/pi-subagent#readme",
|
|
11
15
|
"keywords": [
|
|
16
|
+
"pi-package",
|
|
17
|
+
"pi-extension",
|
|
12
18
|
"pi",
|
|
13
19
|
"agent",
|
|
14
20
|
"subagent",
|
|
@@ -23,7 +29,8 @@
|
|
|
23
29
|
},
|
|
24
30
|
"type": "module",
|
|
25
31
|
"engines": {
|
|
26
|
-
"bun": ">=1.3.13"
|
|
32
|
+
"bun": ">=1.3.13",
|
|
33
|
+
"node": ">=18"
|
|
27
34
|
},
|
|
28
35
|
"files": [
|
|
29
36
|
"src",
|
|
@@ -33,26 +40,37 @@
|
|
|
33
40
|
"pi": {
|
|
34
41
|
"extensions": [
|
|
35
42
|
"./src/index.ts"
|
|
36
|
-
]
|
|
43
|
+
],
|
|
44
|
+
"video": "https://raw.githubusercontent.com/mystilleef/pi-subagent/main/assets/parallel-agents-demo.mp4"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
37
48
|
},
|
|
38
49
|
"scripts": {
|
|
39
|
-
"verify": "bun check && bun test",
|
|
40
|
-
"coverage": "bun check && bun test --coverage",
|
|
41
|
-
"check": "biome check
|
|
50
|
+
"verify": "bun fix && bun check && bun test",
|
|
51
|
+
"coverage": "bun fix && bun check && bun test --coverage",
|
|
52
|
+
"check": "biome check . && tsc --noEmit",
|
|
53
|
+
"fix": "biome check --write --unsafe .",
|
|
54
|
+
"pack:smoke": "bun scripts/pack-smoke.ts",
|
|
42
55
|
"migrate": "biome migrate --write",
|
|
43
56
|
"release": "sh -c 'npm version \"$1\" -m \"chore(release): %s\" && git push --follow-tags' --"
|
|
44
57
|
},
|
|
45
|
-
"
|
|
46
|
-
"@earendil-works/pi-agent-core": "
|
|
47
|
-
"@earendil-works/pi-ai": "
|
|
48
|
-
"@earendil-works/pi-coding-agent": "
|
|
49
|
-
"@earendil-works/pi-tui": "
|
|
50
|
-
"typebox": "
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@earendil-works/pi-agent-core": "*",
|
|
60
|
+
"@earendil-works/pi-ai": "*",
|
|
61
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
62
|
+
"@earendil-works/pi-tui": "*",
|
|
63
|
+
"typebox": "*"
|
|
51
64
|
},
|
|
52
65
|
"devDependencies": {
|
|
53
66
|
"@biomejs/biome": "^2.4.15",
|
|
67
|
+
"@earendil-works/pi-agent-core": "^0.74.1",
|
|
68
|
+
"@earendil-works/pi-ai": "^0.74.1",
|
|
69
|
+
"@earendil-works/pi-coding-agent": "^0.74.1",
|
|
70
|
+
"@earendil-works/pi-tui": "^0.74.1",
|
|
54
71
|
"@types/bun": "^1.3.14",
|
|
55
72
|
"@types/node": "^25.8.0",
|
|
73
|
+
"typebox": "^1.1.38",
|
|
56
74
|
"typescript": "^6.0.3"
|
|
57
75
|
}
|
|
58
76
|
}
|
package/src/cancel-command.ts
CHANGED
|
@@ -15,7 +15,9 @@ export async function cancelSubagentCommandHandler(
|
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
17
|
const options = [
|
|
18
|
-
...jobs.map(
|
|
18
|
+
...jobs.map(
|
|
19
|
+
(job) => `${job.agentName} ${job.instanceName} (${job.requestId})`,
|
|
20
|
+
),
|
|
19
21
|
"All running subagents",
|
|
20
22
|
];
|
|
21
23
|
const selection = await ctx.ui.select("Cancel subagent", options);
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
resetAgentDiscoveryCache,
|
|
8
8
|
} from "./agent-cache.js";
|
|
9
9
|
import { cancelSubagentCommandHandler } from "./cancel-command.js";
|
|
10
|
+
import { jobsCommandHandler } from "./jobs-command.js";
|
|
10
11
|
import { renderSubagentProgress } from "./progress.js";
|
|
11
12
|
import { renderSubagentResultMessage } from "./run.js";
|
|
12
13
|
import { runCommandHandler } from "./run-command.js";
|
|
@@ -38,6 +39,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI) {
|
|
|
38
39
|
"Cancel active /run subagents: /cancel-subagent [requestId|all]",
|
|
39
40
|
handler: async (args, ctx) => cancelSubagentCommandHandler(ctx, args),
|
|
40
41
|
});
|
|
42
|
+
pi.registerCommand("jobs", {
|
|
43
|
+
description: "List all /run jobs and their statuses: /jobs",
|
|
44
|
+
handler: async (args, ctx) => jobsCommandHandler(ctx, args),
|
|
45
|
+
});
|
|
41
46
|
pi.registerTool({
|
|
42
47
|
name: "subagent",
|
|
43
48
|
label: "Subagent",
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const DEFAULT_ADJECTIVES = [
|
|
2
|
+
"able",
|
|
3
|
+
"agile",
|
|
4
|
+
"alert",
|
|
5
|
+
"amber",
|
|
6
|
+
"ample",
|
|
7
|
+
"apt",
|
|
8
|
+
"arctic",
|
|
9
|
+
"avid",
|
|
10
|
+
"bold",
|
|
11
|
+
"brave",
|
|
12
|
+
"bright",
|
|
13
|
+
"brisk",
|
|
14
|
+
"calm",
|
|
15
|
+
"clever",
|
|
16
|
+
"cosmic",
|
|
17
|
+
"crisp",
|
|
18
|
+
"daring",
|
|
19
|
+
"dawn",
|
|
20
|
+
"eager",
|
|
21
|
+
"early",
|
|
22
|
+
"fair",
|
|
23
|
+
"fast",
|
|
24
|
+
"fierce",
|
|
25
|
+
"fine",
|
|
26
|
+
"fresh",
|
|
27
|
+
"gentle",
|
|
28
|
+
"golden",
|
|
29
|
+
"grand",
|
|
30
|
+
"happy",
|
|
31
|
+
"honest",
|
|
32
|
+
"jolly",
|
|
33
|
+
"keen",
|
|
34
|
+
"kind",
|
|
35
|
+
"lively",
|
|
36
|
+
"lucky",
|
|
37
|
+
"merry",
|
|
38
|
+
"mighty",
|
|
39
|
+
"nimble",
|
|
40
|
+
"noble",
|
|
41
|
+
"novel",
|
|
42
|
+
"patient",
|
|
43
|
+
"proud",
|
|
44
|
+
"quick",
|
|
45
|
+
"quiet",
|
|
46
|
+
"rapid",
|
|
47
|
+
"ready",
|
|
48
|
+
"sharp",
|
|
49
|
+
"smart",
|
|
50
|
+
"solid",
|
|
51
|
+
"steady",
|
|
52
|
+
"swift",
|
|
53
|
+
"tidy",
|
|
54
|
+
"vivid",
|
|
55
|
+
"warm",
|
|
56
|
+
"wise",
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
const DEFAULT_NOUNS = [
|
|
60
|
+
"badger",
|
|
61
|
+
"beacon",
|
|
62
|
+
"bison",
|
|
63
|
+
"brook",
|
|
64
|
+
"cedar",
|
|
65
|
+
"comet",
|
|
66
|
+
"coral",
|
|
67
|
+
"coyote",
|
|
68
|
+
"crane",
|
|
69
|
+
"dolphin",
|
|
70
|
+
"eagle",
|
|
71
|
+
"ember",
|
|
72
|
+
"falcon",
|
|
73
|
+
"finch",
|
|
74
|
+
"forest",
|
|
75
|
+
"fox",
|
|
76
|
+
"gecko",
|
|
77
|
+
"glade",
|
|
78
|
+
"harbor",
|
|
79
|
+
"hawk",
|
|
80
|
+
"heron",
|
|
81
|
+
"island",
|
|
82
|
+
"jaguar",
|
|
83
|
+
"koala",
|
|
84
|
+
"lagoon",
|
|
85
|
+
"lemur",
|
|
86
|
+
"lynx",
|
|
87
|
+
"maple",
|
|
88
|
+
"meadow",
|
|
89
|
+
"otter",
|
|
90
|
+
"panda",
|
|
91
|
+
"panther",
|
|
92
|
+
"pelican",
|
|
93
|
+
"phoenix",
|
|
94
|
+
"puma",
|
|
95
|
+
"raven",
|
|
96
|
+
"reef",
|
|
97
|
+
"river",
|
|
98
|
+
"salmon",
|
|
99
|
+
"sparrow",
|
|
100
|
+
"summit",
|
|
101
|
+
"tiger",
|
|
102
|
+
"valley",
|
|
103
|
+
"violet",
|
|
104
|
+
"walrus",
|
|
105
|
+
"willow",
|
|
106
|
+
"wolf",
|
|
107
|
+
"wren",
|
|
108
|
+
"yak",
|
|
109
|
+
"zephyr",
|
|
110
|
+
] as const;
|
|
111
|
+
|
|
112
|
+
const usedInstanceNames = new Set<string>();
|
|
113
|
+
|
|
114
|
+
let adjectives: readonly string[] = DEFAULT_ADJECTIVES;
|
|
115
|
+
let nouns: readonly string[] = DEFAULT_NOUNS;
|
|
116
|
+
let randomSource: () => number = Math.random;
|
|
117
|
+
|
|
118
|
+
function normalizeRandomIndex(limit: number): number {
|
|
119
|
+
const value = randomSource();
|
|
120
|
+
if (!Number.isFinite(value)) return 0;
|
|
121
|
+
return Math.min(limit - 1, Math.max(0, Math.floor(value * limit)));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function nameAt(index: number): string {
|
|
125
|
+
const adjective = adjectives[Math.floor(index / nouns.length)];
|
|
126
|
+
const noun = nouns[index % nouns.length];
|
|
127
|
+
return `${adjective}-${noun}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function generateSubagentInstanceName(): string {
|
|
131
|
+
const capacity = adjectives.length * nouns.length;
|
|
132
|
+
if (usedInstanceNames.size >= capacity) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"No unused subagent instance names remain for this session.",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const start = normalizeRandomIndex(capacity);
|
|
138
|
+
for (let offset = 0; offset < capacity; offset += 1) {
|
|
139
|
+
const candidate = nameAt((start + offset) % capacity);
|
|
140
|
+
if (!usedInstanceNames.has(candidate)) {
|
|
141
|
+
usedInstanceNames.add(candidate);
|
|
142
|
+
return candidate;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
throw new Error("No unused subagent instance names remain for this session.");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function resetSubagentInstanceNamesForTest() {
|
|
149
|
+
usedInstanceNames.clear();
|
|
150
|
+
adjectives = DEFAULT_ADJECTIVES;
|
|
151
|
+
nouns = DEFAULT_NOUNS;
|
|
152
|
+
randomSource = Math.random;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function configureSubagentInstanceNamesForTest(options: {
|
|
156
|
+
adjectives?: readonly string[];
|
|
157
|
+
nouns?: readonly string[];
|
|
158
|
+
randomSource?: () => number;
|
|
159
|
+
}) {
|
|
160
|
+
usedInstanceNames.clear();
|
|
161
|
+
adjectives = options.adjectives ?? DEFAULT_ADJECTIVES;
|
|
162
|
+
nouns = options.nouns ?? DEFAULT_NOUNS;
|
|
163
|
+
randomSource = options.randomSource ?? Math.random;
|
|
164
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getAllProgressStates,
|
|
5
|
+
type SubagentProgressState,
|
|
6
|
+
} from "./progress-state.js";
|
|
7
|
+
import { listRunJobs } from "./run-registry.js";
|
|
8
|
+
import { renderRunsBoard } from "./ui.js";
|
|
9
|
+
|
|
10
|
+
export async function jobsCommandHandler(
|
|
11
|
+
ctx: ExtensionCommandContext,
|
|
12
|
+
_args: string,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const activeRequestIds = new Set(listRunJobs().map((j) => j.requestId));
|
|
15
|
+
const active: SubagentProgressState[] = [];
|
|
16
|
+
const completed: SubagentProgressState[] = [];
|
|
17
|
+
for (const s of getAllProgressStates()) {
|
|
18
|
+
if (activeRequestIds.has(s.requestId)) active.push(s);
|
|
19
|
+
else if (s.status !== "running") completed.push(s);
|
|
20
|
+
}
|
|
21
|
+
const all = [...active, ...completed];
|
|
22
|
+
if (all.length === 0) {
|
|
23
|
+
ctx.ui.notify("No /run jobs in this session.");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const output = await ctx.ui.custom<string>(
|
|
27
|
+
(_tui, theme, _keybindings, done) => ({
|
|
28
|
+
invalidate() {},
|
|
29
|
+
render(width) {
|
|
30
|
+
const tuiLines = renderRunsBoard(all, theme, width).render(width);
|
|
31
|
+
const notifyWidth = Math.max(1, width - 2);
|
|
32
|
+
const notifyLines = renderRunsBoard(all, theme, notifyWidth).render(
|
|
33
|
+
notifyWidth,
|
|
34
|
+
);
|
|
35
|
+
done(notifyLines.join("\n"));
|
|
36
|
+
return tuiLines;
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
ctx.ui.notify(output);
|
|
41
|
+
}
|
package/src/progress-state.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import {
|
|
2
3
|
isStatusOnlyFailure,
|
|
3
4
|
isStatusOnlySuccess,
|
|
@@ -9,11 +10,35 @@ import {
|
|
|
9
10
|
} from "./normalize.js";
|
|
10
11
|
import type { SubagentDetails } from "./types.js";
|
|
11
12
|
|
|
13
|
+
export type ThemeBg = "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
14
|
+
|
|
12
15
|
export type ProgressStatus = "running" | "success" | "error" | "cancelled";
|
|
13
16
|
|
|
17
|
+
export const STATUS_COLOR: Record<ProgressStatus, ThemeColor> = {
|
|
18
|
+
success: "success",
|
|
19
|
+
error: "error",
|
|
20
|
+
cancelled: "error",
|
|
21
|
+
running: "accent",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const STATUS_ICON: Record<ProgressStatus, string> = {
|
|
25
|
+
success: "✓",
|
|
26
|
+
error: "✗",
|
|
27
|
+
cancelled: "⊘",
|
|
28
|
+
running: "⟳",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const STATUS_BG: Record<ProgressStatus, ThemeBg> = {
|
|
32
|
+
success: "toolSuccessBg",
|
|
33
|
+
error: "toolErrorBg",
|
|
34
|
+
cancelled: "toolErrorBg",
|
|
35
|
+
running: "toolPendingBg",
|
|
36
|
+
};
|
|
37
|
+
|
|
14
38
|
export interface SubagentProgressState {
|
|
15
39
|
requestId: string;
|
|
16
40
|
agent: string;
|
|
41
|
+
instanceName?: string;
|
|
17
42
|
taskPreview: string;
|
|
18
43
|
status: ProgressStatus;
|
|
19
44
|
startTime: number;
|
|
@@ -34,10 +59,12 @@ export function createProgressState(
|
|
|
34
59
|
requestId: string,
|
|
35
60
|
agent: string,
|
|
36
61
|
task: string,
|
|
62
|
+
instanceName = requestId,
|
|
37
63
|
): void {
|
|
38
64
|
store.set(requestId, {
|
|
39
65
|
requestId,
|
|
40
66
|
agent,
|
|
67
|
+
instanceName,
|
|
41
68
|
taskPreview: makeTaskPreview(task),
|
|
42
69
|
status: "running",
|
|
43
70
|
startTime: Date.now(),
|
|
@@ -50,6 +77,9 @@ export function getProgressState(
|
|
|
50
77
|
): SubagentProgressState | undefined {
|
|
51
78
|
return store.get(requestId);
|
|
52
79
|
}
|
|
80
|
+
export function getAllProgressStates(): SubagentProgressState[] {
|
|
81
|
+
return [...store.values()].sort((a, b) => b.startTime - a.startTime);
|
|
82
|
+
}
|
|
53
83
|
|
|
54
84
|
export function patchProgressState(
|
|
55
85
|
requestId: string,
|
|
@@ -231,3 +261,53 @@ export function isToolCallPart(part: unknown): part is {
|
|
|
231
261
|
typeof maybe.name === "string"
|
|
232
262
|
);
|
|
233
263
|
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Format a millisecond duration for compact display.
|
|
267
|
+
* Renders sub-minute durations as decimal seconds (`45.2s`),
|
|
268
|
+
* longer durations as minutes and whole seconds (`2m 15s`).
|
|
269
|
+
*/
|
|
270
|
+
export function formatElapsed(ms: number): string {
|
|
271
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
272
|
+
const mins = Math.floor(ms / 60000);
|
|
273
|
+
const secs = Math.floor((ms % 60000) / 1000);
|
|
274
|
+
return `${mins}m ${secs}s`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Format a raw token count for compact inline display.
|
|
279
|
+
* Values below 1000 rendered as-is. Larger counts use `k`
|
|
280
|
+
* or `M` suffixes with one decimal place, stripping trailing `.0`.
|
|
281
|
+
*/
|
|
282
|
+
export function formatTokenCount(count: number): string {
|
|
283
|
+
if (count < 1000) return String(count);
|
|
284
|
+
const unit = count >= 1_000_000 ? "M" : "k";
|
|
285
|
+
const divisor = count >= 1_000_000 ? 1_000_000 : 1000;
|
|
286
|
+
return `${trimTrailingZero((count / divisor).toFixed(1))}${unit}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function trimTrailingZero(value: string): string {
|
|
290
|
+
return value.endsWith(".0") ? value.slice(0, -2) : value;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function formatContextPercent(state: SubagentProgressState): string {
|
|
294
|
+
const d = state.contextWindowTokens;
|
|
295
|
+
if (!d || d <= 0 || !Number.isFinite(d)) return "--%";
|
|
296
|
+
const n = state.contextTokens;
|
|
297
|
+
if (!n || n <= 0 || !Number.isFinite(n)) return "0%";
|
|
298
|
+
return `${Math.round((n / d) * 100)}%`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Format the one-line statistics header for a subagent progress display.
|
|
303
|
+
* Includes tool count, context window usage, and elapsed time.
|
|
304
|
+
* When the subagent is still running (`durationMs` unset), elapsed is
|
|
305
|
+
* computed live from `startTime`.
|
|
306
|
+
*
|
|
307
|
+
* @returns Single line ending in `\n`, e.g. `"3 tools · 45% ctx · 12.3s\n"`
|
|
308
|
+
*/
|
|
309
|
+
export function formatHeaderStats(state: SubagentProgressState): string {
|
|
310
|
+
const elapsedMs = state.durationMs ?? Date.now() - state.startTime;
|
|
311
|
+
const toolLabel = state.toolCount === 1 ? "tool" : "tools";
|
|
312
|
+
return `${state.toolCount} ${toolLabel} · ${formatContextPercent(state)} ctx · ${formatElapsed(elapsedMs)}\n`;
|
|
313
|
+
}
|