pi-subagents 0.11.12 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +89 -8
- package/async-execution.ts +1 -0
- package/async-job-tracker.ts +21 -15
- package/async-status.ts +175 -0
- package/chain-execution.ts +309 -166
- package/index.ts +42 -10
- package/package.json +6 -5
- package/parallel-utils.ts +1 -0
- package/prompt-template-bridge.ts +10 -1
- package/result-watcher.ts +28 -15
- package/schemas.ts +12 -1
- package/settings.ts +2 -0
- package/slash-commands.ts +11 -0
- package/subagent-executor.ts +327 -94
- package/subagent-runner.ts +331 -181
- package/subagents-status.ts +230 -0
- package/types.ts +1 -0
- package/utils.ts +50 -16
- package/worktree.ts +326 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.12.1] - 2026-04-03
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Updated session lifecycle handling for pi 0.65.0 by removing legacy post-transition resets and relying on `session_start` reinitialization, matching pi's removal of `session_switch` and `session_fork` extension events.
|
|
9
|
+
|
|
10
|
+
## [0.12.0] - 2026-03-31
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Added git worktree isolation for parallel execution via `worktree: true`. Applies to top-level parallel `tasks`, chain steps with `{ parallel: [...] }`, and async/background chain execution. Each parallel task gets its own temporary git worktree, and the aggregated output now includes per-task diff stats plus the directory path containing full patch files.
|
|
14
|
+
- Added `worktree.ts` to manage worktree lifecycle, diff capture, patch generation, and cleanup for isolated parallel runs.
|
|
15
|
+
- Added `count: N` shorthand for top-level parallel `tasks` and chain `parallel` entries so one authored task can expand into repeated identical runs without manual duplication.
|
|
16
|
+
- Added `subagent_status({ action: "list" })` to list active async runs with flattened step/member status summaries.
|
|
17
|
+
- Added `/subagents-status`, a read-only overlay for active async runs plus recent completed/failed runs with per-run step details. The overlay auto-refreshes while open and preserves the selected run when possible.
|
|
18
|
+
- Documented worktree isolation, async status surfaces, and the reorganized test layout in the README.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Consolidated tests under `test/unit`, `test/integration`, `test/e2e`, and `test/support`, replacing the old mixed root-level and `test/` layout. Test scripts now target those directories explicitly.
|
|
22
|
+
- Integration tests now use a tiny local file-based mock `pi` harness instead of relying on the external subprocess harness for normal subagent execution.
|
|
23
|
+
- Removed legacy extra session lifecycle resets and now rely on immutable-session `session_start` reinitialization, matching pi's removal of post-transition `session_switch`/`session_fork` events.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Loader-based tests now resolve `.js` → `.ts` imports correctly when the repository path contains spaces or other URL-escaped characters. Added a focused regression test for the custom test loader.
|
|
27
|
+
- Worktree-isolated parallel runs now reject task-level `cwd` overrides that differ from the shared batch/step `cwd`, instead of silently ignoring them. Applies to foreground parallel runs, chain parallel steps, and async/background execution.
|
|
28
|
+
- Worktree diff capture now includes committed, modified, and newly created files without accidentally including the synthetic `node_modules` symlink used inside temporary worktrees.
|
|
29
|
+
- Worktree setup now cleans up already-created worktrees if a later worktree in the same batch fails to initialize.
|
|
30
|
+
- Prompt-template delegated parallel responses now preserve the aggregate worktree summary text instead of dropping it when rebuilding the final delegated output.
|
|
31
|
+
- Async status and result JSON files are now written atomically so readers do not observe partial JSON during background updates.
|
|
32
|
+
- `readStatus()` now returns `null` only for genuinely missing files and preserves real inspect/read/parse failures with context.
|
|
33
|
+
- Async status polling and result watching now log status/result/watcher failures instead of silently swallowing them, making background completion/debugging failures visible.
|
|
34
|
+
- Slash-command tests now match the current live snapshot contract instead of asserting the stale pre-finalized inline state.
|
|
35
|
+
|
|
5
36
|
## [0.11.12] - 2026-03-28
|
|
6
37
|
|
|
7
38
|
### Changed
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Use url in the prompt to take screenshot: $@
|
|
|
34
34
|
|
|
35
35
|
Then `/take-screenshot https://example.com` switches to Sonnet, delegates to the `browser-screenshoter` agent with `/tmp/screenshots` as the working directory, and restores your model when done. Runtime overrides like `--cwd=<path>` and `--subagent=<name>` work too.
|
|
36
36
|
|
|
37
|
-
pi-prompt-template-model is entirely optional — pi-subagents works standalone through the `subagent` tool and slash commands.
|
|
37
|
+
pi-prompt-template-model is entirely optional — pi-subagents works standalone through the `subagent` tool and slash commands. If you want reusable prompt-template workflows on top of subagents, including `/chain-prompts` and compare-style prompts like `pi-prompt-template-model`'s `/best-of-n` example, install [pi-prompt-template-model](https://github.com/nicobailon/pi-prompt-template-model) separately and copy any example prompts you want from its `examples/` directory into `~/.pi/agent/prompts/`.
|
|
38
38
|
|
|
39
39
|
## Agents
|
|
40
40
|
|
|
@@ -131,6 +131,7 @@ Subagents only get direct MCP tools when `mcp:` items are explicitly listed. Eve
|
|
|
131
131
|
| `/run <agent> <task>` | Run a single agent with a task |
|
|
132
132
|
| `/chain agent1 "task1" -> agent2 "task2"` | Run agents in sequence with per-step tasks |
|
|
133
133
|
| `/parallel agent1 "task1" -> agent2 "task2"` | Run agents in parallel with per-step tasks |
|
|
134
|
+
| `/subagents-status` | Open the async status overlay for active and recent runs |
|
|
134
135
|
| `/agents` | Open the Agents Manager overlay |
|
|
135
136
|
|
|
136
137
|
All commands validate agent names locally and tab-complete them, then route through the tool framework for full live progress rendering. Results are sent to the conversation for the LLM to discuss.
|
|
@@ -190,7 +191,7 @@ Add `--bg` at the end of any slash command to run in the background:
|
|
|
190
191
|
/parallel scout "scan frontend" -> scout "scan backend" -> scout "scan infra" --bg
|
|
191
192
|
```
|
|
192
193
|
|
|
193
|
-
Without `--bg`, the run is foreground: the tool call stays active and streams progress until completion. With `--bg`, the run is launched asynchronously: control returns immediately, and completion arrives later via notification. In both cases subagents run as separate processes. Check status with `subagent_status
|
|
194
|
+
Without `--bg`, the run is foreground: the tool call stays active and streams progress until completion. With `--bg`, the run is launched asynchronously: control returns immediately, and completion arrives later via notification. In both cases subagents run as separate processes. Check status with the `subagent_status` tool, or open the `/subagents-status` slash command for a read-only overlay listing active runs and recent completions.
|
|
194
195
|
|
|
195
196
|
### Forked Context Execution
|
|
196
197
|
|
|
@@ -300,6 +301,7 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
|
|
|
300
301
|
- **Agent Templates**: Create agents from presets (Scout, Planner, Implementer, Code Reviewer, Blank Chain)
|
|
301
302
|
- **Skill Injection**: Agents declare skills in frontmatter; skills get injected into system prompts
|
|
302
303
|
- **Parallel-in-Chain**: Fan-out/fan-in patterns with `{ parallel: [...] }` steps within chains
|
|
304
|
+
- **Worktree Isolation**: `worktree: true` gives each parallel agent its own git worktree, preventing filesystem conflicts during concurrent execution
|
|
303
305
|
- **Chain Clarification TUI**: Interactive preview/edit of chain templates and behaviors before execution
|
|
304
306
|
- **Agent Frontmatter Extensions**: Agents declare default chain behavior (`output`, `defaultReads`, `defaultProgress`, `skill`)
|
|
305
307
|
- **Chain Artifacts**: Shared directory at `<tmpdir>/pi-chain-runs/{runId}/` for inter-step files
|
|
@@ -442,6 +444,9 @@ These are the parameters the **LLM agent** passes when it calls the `subagent` t
|
|
|
442
444
|
// Parallel
|
|
443
445
|
{ tasks: [{ agent: "scout", task: "a" }, { agent: "scout", task: "b" }] }
|
|
444
446
|
|
|
447
|
+
// Parallel with count shorthand (run the same task 3 times)
|
|
448
|
+
{ tasks: [{ agent: "scout", task: "audit auth", count: 3 }] }
|
|
449
|
+
|
|
445
450
|
// Parallel with forked context (each task gets its own isolated fork)
|
|
446
451
|
{ tasks: [{ agent: "scout", task: "audit frontend" }, { agent: "reviewer", task: "audit backend" }], context: "fork" }
|
|
447
452
|
|
|
@@ -472,7 +477,7 @@ These are the parameters the **LLM agent** passes when it calls the `subagent` t
|
|
|
472
477
|
{ chain: [
|
|
473
478
|
{ agent: "scout", task: "Gather context for the codebase" },
|
|
474
479
|
{ parallel: [
|
|
475
|
-
{ agent: "worker", task: "Implement auth based on {previous}" },
|
|
480
|
+
{ agent: "worker", task: "Implement auth based on {previous}", count: 2 },
|
|
476
481
|
{ agent: "worker", task: "Implement API based on {previous}" }
|
|
477
482
|
]},
|
|
478
483
|
{ agent: "reviewer", task: "Review all changes from {previous}" }
|
|
@@ -488,6 +493,22 @@ These are the parameters the **LLM agent** passes when it calls the `subagent` t
|
|
|
488
493
|
], concurrency: 2, failFast: true } // limit concurrency, stop on first failure
|
|
489
494
|
]}
|
|
490
495
|
|
|
496
|
+
// Worktree isolation — each parallel agent gets its own git worktree
|
|
497
|
+
{ tasks: [
|
|
498
|
+
{ agent: "worker", task: "Implement auth" },
|
|
499
|
+
{ agent: "worker", task: "Implement API" }
|
|
500
|
+
], worktree: true }
|
|
501
|
+
|
|
502
|
+
// Worktree isolation in a chain (fan-out with isolation, fan-in for review)
|
|
503
|
+
{ chain: [
|
|
504
|
+
{ agent: "scout", task: "Gather context" },
|
|
505
|
+
{ parallel: [
|
|
506
|
+
{ agent: "worker", task: "Implement feature A based on {previous}" },
|
|
507
|
+
{ agent: "worker", task: "Implement feature B based on {previous}" }
|
|
508
|
+
], worktree: true },
|
|
509
|
+
{ agent: "reviewer", task: "Review changes from {previous}" }
|
|
510
|
+
]}
|
|
511
|
+
|
|
491
512
|
// Async chain with parallel step (runs in background)
|
|
492
513
|
{ chain: [
|
|
493
514
|
{ agent: "scout", task: "Gather context" },
|
|
@@ -501,10 +522,15 @@ These are the parameters the **LLM agent** passes when it calls the `subagent` t
|
|
|
501
522
|
|
|
502
523
|
**subagent_status tool:**
|
|
503
524
|
```typescript
|
|
504
|
-
{
|
|
525
|
+
{ action: "list" } // active async runs only
|
|
526
|
+
{ id: "a53ebe46" } // inspect one run
|
|
505
527
|
{ dir: "<tmpdir>/pi-async-subagent-runs/a53ebe46-..." }
|
|
506
528
|
```
|
|
507
529
|
|
|
530
|
+
**/subagents-status slash command:**
|
|
531
|
+
|
|
532
|
+
Opens a small read-only overlay that shows active async runs plus recent completed/failed runs. It auto-refreshes every 2 seconds while open, keeps the current run selected when possible, and uses `↑↓` to select a run plus `Esc` to close.
|
|
533
|
+
|
|
508
534
|
## Management Actions
|
|
509
535
|
|
|
510
536
|
Agent definitions are not loaded into LLM context by default. Management actions let the LLM discover, inspect, create, and modify agent and chain definitions at runtime through the `subagent` tool — no manual file editing or restart required. Newly created agents are immediately usable in the same session. Set `action` and omit execution payloads (`task`, `chain`, `tasks`).
|
|
@@ -578,7 +604,8 @@ Notes:
|
|
|
578
604
|
| `output` | `string \| false` | agent default | Override output file for single agent (absolute path as-is, relative path resolved against cwd) |
|
|
579
605
|
| `skill` | `string \| string[] \| false` | agent default | Override skills (comma-separated string, array, or false to disable) |
|
|
580
606
|
| `model` | string | agent default | Override model for single agent |
|
|
581
|
-
| `tasks` | `{agent, task, cwd?, skill?}[]` | - | Parallel tasks
|
|
607
|
+
| `tasks` | `{agent, task, cwd?, count?, skill?}[]` | - | Parallel tasks. Foreground runs directly; background requests are converted to an equivalent chain. `count` repeats one task entry N times with the same settings. |
|
|
608
|
+
| `worktree` | boolean | false | Create isolated git worktrees for each parallel task. Requires clean git state. Per-worktree diffs included in output. |
|
|
582
609
|
| `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
|
|
583
610
|
| `context` | `"fresh" \| "fork"` | `fresh` | Execution context mode. `fork` uses a real branched session from the parent's current leaf for each child run |
|
|
584
611
|
| `chainDir` | string | `<tmpdir>/pi-chain-runs/` | Persistent directory for chain artifacts (default auto-cleaned after 24h) |
|
|
@@ -616,6 +643,7 @@ Notes:
|
|
|
616
643
|
| `parallel` | ParallelTask[] | required | Array of tasks to run concurrently |
|
|
617
644
|
| `concurrency` | number | 4 | Max concurrent tasks |
|
|
618
645
|
| `failFast` | boolean | false | Stop remaining tasks on first failure |
|
|
646
|
+
| `worktree` | boolean | false | Create isolated git worktrees for each parallel task |
|
|
619
647
|
|
|
620
648
|
*ParallelTask fields:* (same as sequential step)
|
|
621
649
|
|
|
@@ -624,6 +652,7 @@ Notes:
|
|
|
624
652
|
| `agent` | string | required | Agent name |
|
|
625
653
|
| `task` | string | `{previous}` | Task template |
|
|
626
654
|
| `cwd` | string | - | Override working directory |
|
|
655
|
+
| `count` | number | 1 | Repeat this parallel task N times with the same settings |
|
|
627
656
|
| `output` | `string \| false` | agent default | Override output (namespaced to parallel-N/M-agent/) |
|
|
628
657
|
| `reads` | `string[] \| false` | agent default | Override files to read |
|
|
629
658
|
| `progress` | boolean | agent default | Override progress tracking |
|
|
@@ -634,7 +663,48 @@ Status tool:
|
|
|
634
663
|
|
|
635
664
|
| Tool | Description |
|
|
636
665
|
|------|-------------|
|
|
637
|
-
| `subagent_status` |
|
|
666
|
+
| `subagent_status` | List active async runs or inspect one run by id or dir |
|
|
667
|
+
|
|
668
|
+
## Worktree Isolation
|
|
669
|
+
|
|
670
|
+
When multiple agents run in parallel against the same repo, they can clobber each other's file changes. Pass `worktree: true` to give each parallel agent its own git worktree branched from HEAD.
|
|
671
|
+
|
|
672
|
+
```typescript
|
|
673
|
+
// Top-level parallel with worktree isolation
|
|
674
|
+
{ tasks: [
|
|
675
|
+
{ agent: "worker", task: "Implement auth", count: 2 },
|
|
676
|
+
{ agent: "worker", task: "Implement API" }
|
|
677
|
+
], worktree: true }
|
|
678
|
+
|
|
679
|
+
// Chain with worktree-isolated parallel step
|
|
680
|
+
{ chain: [
|
|
681
|
+
{ agent: "scout", task: "Gather context" },
|
|
682
|
+
{ parallel: [
|
|
683
|
+
{ agent: "worker", task: "Implement feature A based on {previous}" },
|
|
684
|
+
{ agent: "worker", task: "Implement feature B based on {previous}" }
|
|
685
|
+
], worktree: true },
|
|
686
|
+
{ agent: "reviewer", task: "Review all changes from {previous}" }
|
|
687
|
+
]}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
After the parallel step completes, per-agent diff stats are appended to the output (and become `{previous}` for the next chain step). Full patch files are written to disk. The caller or next step can decide what to keep.
|
|
691
|
+
|
|
692
|
+
**Requirements:**
|
|
693
|
+
|
|
694
|
+
- Must be inside a git repository
|
|
695
|
+
- Working tree must be clean (no uncommitted changes) — commit or stash first
|
|
696
|
+
- `node_modules/` is symlinked into each worktree to avoid reinstalling
|
|
697
|
+
- Worktree runs use the shared parallel/step `cwd`. Task-level `cwd` overrides must be omitted or match that shared `cwd`; if you need different working directories, disable `worktree` or split the run.
|
|
698
|
+
|
|
699
|
+
**What happens under the hood:**
|
|
700
|
+
|
|
701
|
+
1. `git worktree add` creates a temporary worktree per agent in `<tmpdir>/pi-worktree-*`
|
|
702
|
+
2. Each agent runs in its worktree's cwd (preserving subdirectory context)
|
|
703
|
+
3. After execution, `git add -A && git diff --cached` captures all changes (committed, modified, and new files)
|
|
704
|
+
4. Diff stats appear in the aggregated output; full `.patch` files are written to the artifacts directory
|
|
705
|
+
5. Worktrees and temp branches are cleaned up in a `finally` block
|
|
706
|
+
|
|
707
|
+
If you use [pi-prompt-template-model](https://github.com/nicobailon/pi-prompt-template-model), worktree isolation is also available via `worktree: true` in chain template frontmatter or the `--worktree` CLI flag on `chain-prompts`. `pi-prompt-template-model` compare-style prompts can route through the same worktree machinery too; see the `pi-prompt-template-model` README and `examples/` directory for the installable prompt templates.
|
|
638
708
|
|
|
639
709
|
## Chain Variables
|
|
640
710
|
|
|
@@ -772,14 +842,18 @@ Async runs write a dedicated observability folder:
|
|
|
772
842
|
subagent-log-<id>.md
|
|
773
843
|
```
|
|
774
844
|
|
|
775
|
-
`status.json` is the source of truth for async progress and powers the TUI widget
|
|
776
|
-
|
|
845
|
+
`status.json` is the source of truth for async progress and powers both the TUI widget and `/subagents-status`. Async status and result files are written atomically, so readers do not observe partial JSON during background updates.
|
|
846
|
+
|
|
847
|
+
For programmatic access:
|
|
777
848
|
|
|
778
849
|
```typescript
|
|
850
|
+
subagent_status({ action: "list" })
|
|
779
851
|
subagent_status({ id: "<id>" })
|
|
780
852
|
subagent_status({ dir: "<tmpdir>/pi-async-subagent-runs/<id>" })
|
|
781
853
|
```
|
|
782
854
|
|
|
855
|
+
For an interactive overview, run the `/subagents-status` slash command to open the overlay listing active runs and recent completed/failed runs. The overlay auto-refreshes every 2 seconds while it is open.
|
|
856
|
+
|
|
783
857
|
## Events
|
|
784
858
|
|
|
785
859
|
Async events:
|
|
@@ -799,8 +873,10 @@ Async events:
|
|
|
799
873
|
├── chain-execution.ts # Chain orchestration (sequential + parallel)
|
|
800
874
|
├── chain-serializer.ts # Parse/serialize .chain.md files
|
|
801
875
|
├── async-execution.ts # Async/background execution support
|
|
876
|
+
├── async-status.ts # Async run discovery, listing, and formatting
|
|
802
877
|
├── execution.ts # Core runSync, applyThinkingSuffix
|
|
803
878
|
├── render.ts # TUI rendering (widget, tool result display)
|
|
879
|
+
├── subagents-status.ts # Async status overlay component
|
|
804
880
|
├── artifacts.ts # Artifact management
|
|
805
881
|
├── formatters.ts # Output formatting utilities
|
|
806
882
|
├── schemas.ts # TypeBox parameter schemas
|
|
@@ -808,6 +884,7 @@ Async events:
|
|
|
808
884
|
├── types.ts # Shared types and constants
|
|
809
885
|
├── subagent-runner.ts # Async runner (detached process)
|
|
810
886
|
├── parallel-utils.ts # Parallel execution utilities for async runner
|
|
887
|
+
├── worktree.ts # Git worktree isolation for parallel execution
|
|
811
888
|
├── pi-spawn.ts # Cross-platform pi CLI spawning
|
|
812
889
|
├── single-output.ts # Solo agent output file handling
|
|
813
890
|
├── notify.ts # Async completion notifications
|
|
@@ -827,5 +904,9 @@ Async events:
|
|
|
827
904
|
├── agent-templates.ts # Agent/chain creation templates
|
|
828
905
|
├── render-helpers.ts # Shared pad/row/header/footer helpers
|
|
829
906
|
├── run-history.ts # Per-agent run recording (JSONL)
|
|
907
|
+
├── test/unit/ # Fast unit tests for pure modules
|
|
908
|
+
├── test/integration/ # Loader-based execution/integration tests
|
|
909
|
+
├── test/e2e/ # End-to-end sandbox tests
|
|
910
|
+
├── test/support/ # Shared test loader, helpers, and mock pi harness
|
|
830
911
|
└── text-editor.ts # Shared text editor (word nav, paste)
|
|
831
912
|
```
|
package/async-execution.ts
CHANGED
package/async-job-tracker.ts
CHANGED
|
@@ -30,24 +30,30 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
30
30
|
if (job.status === "complete" || job.status === "failed") {
|
|
31
31
|
continue;
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
try {
|
|
34
|
+
const status = readStatus(job.asyncDir);
|
|
35
|
+
if (status) {
|
|
36
|
+
job.status = status.state;
|
|
37
|
+
job.mode = status.mode;
|
|
38
|
+
job.currentStep = status.currentStep ?? job.currentStep;
|
|
39
|
+
job.stepsTotal = status.steps?.length ?? job.stepsTotal;
|
|
40
|
+
job.startedAt = status.startedAt ?? job.startedAt;
|
|
41
|
+
job.updatedAt = status.lastUpdate ?? Date.now();
|
|
42
|
+
if (status.steps?.length) {
|
|
43
|
+
job.agents = status.steps.map((step) => step.agent);
|
|
44
|
+
}
|
|
45
|
+
job.sessionDir = status.sessionDir ?? job.sessionDir;
|
|
46
|
+
job.outputFile = status.outputFile ?? job.outputFile;
|
|
47
|
+
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
48
|
+
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
49
|
+
continue;
|
|
43
50
|
}
|
|
44
|
-
job.sessionDir = status.sessionDir ?? job.sessionDir;
|
|
45
|
-
job.outputFile = status.outputFile ?? job.outputFile;
|
|
46
|
-
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
47
|
-
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
48
|
-
} else {
|
|
49
51
|
job.status = job.status === "queued" ? "running" : job.status;
|
|
50
52
|
job.updatedAt = Date.now();
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(`Failed to read async status for '${job.asyncDir}':`, error);
|
|
55
|
+
job.status = "failed";
|
|
56
|
+
job.updatedAt = Date.now();
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
|
package/async-status.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { formatDuration, formatTokens, shortenPath } from "./formatters.js";
|
|
4
|
+
import { type AsyncStatus, type TokenUsage } from "./types.js";
|
|
5
|
+
import { readStatus } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
export interface AsyncRunStepSummary {
|
|
8
|
+
index: number;
|
|
9
|
+
agent: string;
|
|
10
|
+
status: string;
|
|
11
|
+
durationMs?: number;
|
|
12
|
+
tokens?: TokenUsage;
|
|
13
|
+
skills?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AsyncRunSummary {
|
|
17
|
+
id: string;
|
|
18
|
+
asyncDir: string;
|
|
19
|
+
state: "queued" | "running" | "complete" | "failed";
|
|
20
|
+
mode: "single" | "chain";
|
|
21
|
+
cwd?: string;
|
|
22
|
+
startedAt: number;
|
|
23
|
+
lastUpdate?: number;
|
|
24
|
+
endedAt?: number;
|
|
25
|
+
currentStep?: number;
|
|
26
|
+
steps: AsyncRunStepSummary[];
|
|
27
|
+
sessionDir?: string;
|
|
28
|
+
outputFile?: string;
|
|
29
|
+
totalTokens?: TokenUsage;
|
|
30
|
+
sessionFile?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AsyncRunListOptions {
|
|
34
|
+
states?: Array<AsyncRunSummary["state"]>;
|
|
35
|
+
limit?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AsyncRunOverlayData {
|
|
39
|
+
active: AsyncRunSummary[];
|
|
40
|
+
recent: AsyncRunSummary[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getErrorMessage(error: unknown): string {
|
|
44
|
+
return error instanceof Error ? error.message : String(error);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isNotFoundError(error: unknown): boolean {
|
|
48
|
+
return typeof error === "object"
|
|
49
|
+
&& error !== null
|
|
50
|
+
&& "code" in error
|
|
51
|
+
&& (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isAsyncRunDir(root: string, entry: string): boolean {
|
|
55
|
+
const entryPath = path.join(root, entry);
|
|
56
|
+
try {
|
|
57
|
+
return fs.statSync(entryPath).isDirectory();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (isNotFoundError(error)) return false;
|
|
60
|
+
throw new Error(`Failed to inspect async run path '${entryPath}': ${getErrorMessage(error)}`, {
|
|
61
|
+
cause: error instanceof Error ? error : undefined,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }): AsyncRunSummary {
|
|
67
|
+
return {
|
|
68
|
+
id: status.runId || path.basename(asyncDir),
|
|
69
|
+
asyncDir,
|
|
70
|
+
state: status.state,
|
|
71
|
+
mode: status.mode,
|
|
72
|
+
cwd: status.cwd,
|
|
73
|
+
startedAt: status.startedAt,
|
|
74
|
+
lastUpdate: status.lastUpdate,
|
|
75
|
+
endedAt: status.endedAt,
|
|
76
|
+
currentStep: status.currentStep,
|
|
77
|
+
steps: (status.steps ?? []).map((step, index) => ({
|
|
78
|
+
index,
|
|
79
|
+
agent: step.agent,
|
|
80
|
+
status: step.status,
|
|
81
|
+
...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
|
|
82
|
+
...(step.tokens ? { tokens: step.tokens } : {}),
|
|
83
|
+
...(step.skills ? { skills: step.skills } : {}),
|
|
84
|
+
})),
|
|
85
|
+
...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
|
|
86
|
+
...(status.outputFile ? { outputFile: status.outputFile } : {}),
|
|
87
|
+
...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
|
|
88
|
+
...(status.sessionFile ? { sessionFile: status.sessionFile } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
|
|
93
|
+
const rank = (state: AsyncRunSummary["state"]): number => {
|
|
94
|
+
switch (state) {
|
|
95
|
+
case "running": return 0;
|
|
96
|
+
case "queued": return 1;
|
|
97
|
+
case "failed": return 2;
|
|
98
|
+
case "complete": return 3;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return [...runs].sort((a, b) => {
|
|
102
|
+
const byState = rank(a.state) - rank(b.state);
|
|
103
|
+
if (byState !== 0) return byState;
|
|
104
|
+
const aTime = a.lastUpdate ?? a.endedAt ?? a.startedAt;
|
|
105
|
+
const bTime = b.lastUpdate ?? b.endedAt ?? b.startedAt;
|
|
106
|
+
return bTime - aTime;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
|
|
111
|
+
let entries: string[];
|
|
112
|
+
try {
|
|
113
|
+
entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (isNotFoundError(error)) return [];
|
|
116
|
+
throw new Error(`Failed to list async runs in '${asyncDirRoot}': ${getErrorMessage(error)}`, {
|
|
117
|
+
cause: error instanceof Error ? error : undefined,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const allowedStates = options.states ? new Set(options.states) : undefined;
|
|
122
|
+
const runs: AsyncRunSummary[] = [];
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
const asyncDir = path.join(asyncDirRoot, entry);
|
|
125
|
+
const status = readStatus(asyncDir) as (AsyncStatus & { cwd?: string }) | null;
|
|
126
|
+
if (!status) continue;
|
|
127
|
+
const summary = statusToSummary(asyncDir, status);
|
|
128
|
+
if (allowedStates && !allowedStates.has(summary.state)) continue;
|
|
129
|
+
runs.push(summary);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const sorted = sortRuns(runs);
|
|
133
|
+
return options.limit !== undefined ? sorted.slice(0, options.limit) : sorted;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function listAsyncRunsForOverlay(asyncDirRoot: string, recentLimit = 5): AsyncRunOverlayData {
|
|
137
|
+
const all = listAsyncRuns(asyncDirRoot);
|
|
138
|
+
const recent = all
|
|
139
|
+
.filter((run) => run.state === "complete" || run.state === "failed")
|
|
140
|
+
.sort((a, b) => (b.lastUpdate ?? b.endedAt ?? b.startedAt) - (a.lastUpdate ?? a.endedAt ?? a.startedAt))
|
|
141
|
+
.slice(0, recentLimit);
|
|
142
|
+
return {
|
|
143
|
+
active: all.filter((run) => run.state === "queued" || run.state === "running"),
|
|
144
|
+
recent,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function formatStepLine(step: AsyncRunStepSummary): string {
|
|
149
|
+
const parts = [`${step.index + 1}. ${step.agent}`, step.status];
|
|
150
|
+
if (step.durationMs !== undefined) parts.push(formatDuration(step.durationMs));
|
|
151
|
+
if (step.tokens) parts.push(`${formatTokens(step.tokens.total)} tok`);
|
|
152
|
+
return parts.join(" | ");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatRunHeader(run: AsyncRunSummary): string {
|
|
156
|
+
const stepCount = run.steps.length || 1;
|
|
157
|
+
const stepLabel = run.currentStep !== undefined ? `step ${run.currentStep + 1}/${stepCount}` : `steps ${stepCount}`;
|
|
158
|
+
const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
|
|
159
|
+
return `${run.id} | ${run.state} | ${run.mode} | ${stepLabel} | ${cwd}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {
|
|
163
|
+
if (runs.length === 0) return `No ${heading.toLowerCase()}.`;
|
|
164
|
+
|
|
165
|
+
const lines = [`${heading}: ${runs.length}`, ""];
|
|
166
|
+
for (const run of runs) {
|
|
167
|
+
lines.push(`- ${formatRunHeader(run)}`);
|
|
168
|
+
for (const step of run.steps) {
|
|
169
|
+
lines.push(` ${formatStepLine(step)}`);
|
|
170
|
+
}
|
|
171
|
+
if (run.sessionFile) lines.push(` session: ${shortenPath(run.sessionFile)}`);
|
|
172
|
+
lines.push("");
|
|
173
|
+
}
|
|
174
|
+
return lines.join("\n").trimEnd();
|
|
175
|
+
}
|