@maxiaochao/pi-toolkit 0.12.2 → 0.13.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 +5 -0
- package/README.md +5 -4
- package/package.json +7 -5
- package/packages/background-tasks/CHANGELOG.md +9 -0
- package/packages/background-tasks/README.md +114 -0
- package/packages/background-tasks/TESTING.md +56 -0
- package/packages/background-tasks/docs/demo.gif +0 -0
- package/packages/background-tasks/docs/screenshot.png +0 -0
- package/packages/background-tasks/extensions/background-tasks.ts +200 -0
- package/packages/background-tasks/package.json +43 -0
- package/packages/background-tasks/src/background-tasks.ts +240 -0
- package/packages/background-tasks/src/output.ts +128 -0
- package/packages/background-tasks/src/widget.ts +54 -0
- package/packages/background-tasks/tests/extension.mjs +192 -0
- package/packages/background-tasks/tests/fixtures/provider.ts +63 -0
- package/packages/background-tasks/tests/helpers.mjs +55 -0
- package/packages/background-tasks/tests/load.mjs +28 -0
- package/packages/background-tasks/tests/output.mjs +55 -0
- package/packages/background-tasks/tests/processes.mjs +96 -0
- package/packages/background-tasks/tests/runtime.mjs +30 -0
- package/packages/background-tasks/tests/tasks.mjs +201 -0
- package/packages/background-tasks/tests/tui-smoke.py +195 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 2026-10-20 - v0.13.0
|
|
2
|
+
|
|
3
|
+
- background-tasks v0.1.0: bundle background shell tasks with runtime limits, bounded logs, completion notifications and /bg controls
|
|
4
|
+
- Bundle and enable `@maxiaochao/pi-background-tasks` by default: managed shell jobs, Bash-aligned bounded output, completion followUps, `/bg` controls and an above-editor status widget. Keep the standalone package available as an alternative installation. The toolkit now requires Pi 1.3.0 or later.
|
|
5
|
+
|
|
1
6
|
## 2026-10-20 - v0.12.2
|
|
2
7
|
|
|
3
8
|
- herdr-subagents v0.7.1: clarify Persona responsibilities and dispatch guidance
|
package/README.md
CHANGED
|
@@ -4,15 +4,15 @@ Personal Pi distribution that bundles focused extensions, agent skills, the Nigh
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
Install the complete toolkit:
|
|
7
|
+
Requires Pi **1.3.0 or later**. Install the complete toolkit:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
pi install npm:@maxiaochao/pi-toolkit
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
That one install enables
|
|
13
|
+
That one install enables six child packages by default, because their source is embedded in the toolkit tarball: `pi-cache-export`, `pi-codex-edit`, `pi-herdr-subagents`, `pi-scheduler`, `pi-background-tasks`, and `pi-worktree`.
|
|
14
14
|
|
|
15
|
-
`pi-openai-account-pool` is
|
|
15
|
+
`pi-openai-account-pool` is optional and stays a separate install:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
pi install npm:@maxiaochao/pi-openai-account-pool
|
|
@@ -34,6 +34,7 @@ Restart Pi after updating. `pi update --extensions` has the same effect when upd
|
|
|
34
34
|
| [pi-codex-edit](packages/codex-edit/README.md) | Codex-harness `apply_patch` routing for selected GPT models | Yes | `pi install npm:@maxiaochao/pi-codex-edit` | `pi update npm:@maxiaochao/pi-codex-edit` | The core lies in the fact that the Codex model is naturally good at apply_patch, and the model is trained on this during training. However, hash-based in-context patch schemes have high token costs and complexity |
|
|
35
35
|
| [pi-herdr-subagents](packages/herdr-subagents/README.md) | Visible Herdr Pi Agents coordinated by a small task state machine | Yes | `pi install npm:@maxiaochao/pi-herdr-subagents` | `pi update npm:@maxiaochao/pi-herdr-subagents` | I wanted simple serial or bounded-parallel dispatch while each task stays a real interactive Pi session in its own Herdr pane, and the main conversation only receives short summaries instead of every child's full transcript |
|
|
36
36
|
| [pi-openai-account-pool](packages/openai-account-pool/README.md) | Several ChatGPT Plus/Pro logins behind `openai-codex`, switching accounts on a proven quota limit | No | `pi install npm:@maxiaochao/pi-openai-account-pool` | `pi update npm:@maxiaochao/pi-openai-account-pool` | One `/login openai-codex` credential overwrites the previous one, so hitting the subscription limit meant logging in again by hand mid-task; this keeps the accounts I authorized side by side and continues the same request on the next usable one |
|
|
37
|
+
| [pi-background-tasks](packages/background-tasks/README.md) | Background shell jobs, bounded logs, completion notifications and a fixed status widget | Yes | `pi install npm:@maxiaochao/pi-background-tasks` | `pi update npm:@maxiaochao/pi-background-tasks` | Long builds and delayed log checks should keep running while the main agent does other work; these are local processes, not subagents |
|
|
37
38
|
| [pi-scheduler](packages/scheduler/README.md) | Session-scoped interval prompts with Agent and `/schedule` controls | Yes | `pi install npm:@maxiaochao/pi-scheduler` | `pi update npm:@maxiaochao/pi-scheduler` | I don't recommend this, it's just that it sends messages to the main session at loop intervals |
|
|
38
39
|
| [pi-worktree](packages/worktree/README.md) | Worktree CLI, current-session switching Extension, and agent skill | Yes | `pi install npm:@maxiaochao/pi-worktree` | `pi update npm:@maxiaochao/pi-worktree` | In addition to the basic worktree CLI + skill, what can be achieved is to directly switch to the worktree path in the main conversation to continue the task conversation after communicating the requirements in the main conversation |
|
|
39
40
|
|
|
@@ -41,7 +42,7 @@ Each child README owns its feature description, motivation or upstream alignment
|
|
|
41
42
|
|
|
42
43
|
Herdr Subagents tasks require an explicit `agent` Persona; no default `worker` is selected. Choose `reviewer` for independent reviews of proposals, designs, documents, implementations, or analysis; `explorer` for codebase investigation; `worker` for file-changing work; and `analyst` for charter-defined agent-team roles. See [built-in Personas](packages/herdr-subagents/README.md#built-in-personas) for selection guidance and the distinction between parent-facing descriptions and child instructions. Personas can opt into local extension files or npm/git Pi packages through `extensions`. Set top-level `defaultExtensions` in `~/.pi/agent/herdr-subagents.json` (or project `.pi/herdr-subagents.json`) to share a fallback list; Persona-specific lists take precedence, and `[]` clears inherited sources. This uses native Pi package semantics: package-provided skills, prompt templates, and themes may also load, and `--no-skills` does not exclude explicitly selected package skills. See the [extension-source contract](packages/herdr-subagents/README.md#explicit-extension-sources-and-pi-packages) for precedence, isolation, and examples. The [real Herdr smoke test](packages/herdr-subagents/TESTING.md) preserves the configured analyst model and explicit authentication extensions while isolating unrelated settings.
|
|
43
44
|
|
|
44
|
-
Multiple standalone children may be installed together. `pi-openai-account-pool` may also be installed alongside the complete toolkit because the toolkit does not embed it. Every other child in the table above is embedded, so a second install exposes the same resource twice.
|
|
45
|
+
Multiple standalone children may be installed together. `pi-openai-account-pool` may also be installed alongside the complete toolkit because the toolkit does not embed it. Every other child in the table above, including `pi-background-tasks`, is embedded, so a second install exposes the same resource twice.
|
|
45
46
|
|
|
46
47
|
Use `--local` with install or remove when the package declaration belongs to a project's `.pi/settings.json` rather than personal settings.
|
|
47
48
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxiaochao/pi-toolkit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Personal pi coding agent toolkit: scheduler, /btw, /cache_export, apply_patch, browser automation, and bundled theme/skills.",
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "Personal pi coding agent toolkit: background tasks, scheduler, /btw, /cache_export, apply_patch, browser automation, and bundled theme/skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
7
7
|
"packages/*"
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"packages/codex-edit",
|
|
18
18
|
"packages/herdr-subagents",
|
|
19
19
|
"packages/scheduler",
|
|
20
|
+
"packages/background-tasks",
|
|
20
21
|
"packages/worktree",
|
|
21
22
|
"themes",
|
|
22
23
|
"README.md",
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
"./packages/codex-edit/extensions/codex-edit.ts",
|
|
37
38
|
"./packages/herdr-subagents/extensions/herdr-subagents.ts",
|
|
38
39
|
"./packages/scheduler/extensions/scheduler.ts",
|
|
40
|
+
"./packages/background-tasks/extensions/background-tasks.ts",
|
|
39
41
|
"./packages/worktree/extensions/worktree.ts"
|
|
40
42
|
],
|
|
41
43
|
"skills": [
|
|
@@ -51,7 +53,7 @@
|
|
|
51
53
|
},
|
|
52
54
|
"peerDependencies": {
|
|
53
55
|
"@earendil-works/pi-ai": "*",
|
|
54
|
-
"@earendil-works/pi-coding-agent": "
|
|
56
|
+
"@earendil-works/pi-coding-agent": ">=1.3.0",
|
|
55
57
|
"@earendil-works/pi-tui": "*",
|
|
56
58
|
"typebox": "*"
|
|
57
59
|
},
|
|
@@ -59,9 +61,9 @@
|
|
|
59
61
|
"ws": "^8.19.0"
|
|
60
62
|
},
|
|
61
63
|
"scripts": {
|
|
62
|
-
"test": "node tests/package-layout.mjs && node tests/release-check.mjs && npm --prefix packages/todo test && npm --prefix packages/cache-export test && npm --prefix packages/codex-edit test && npm --prefix packages/herdr-subagents test && npm --prefix packages/openai-account-pool test && npm --prefix packages/scheduler test && npm --prefix packages/worktree test && node --experimental-strip-types extensions/btw/tests/history.mjs",
|
|
64
|
+
"test": "node tests/package-layout.mjs && node tests/release-check.mjs && npm --prefix packages/todo test && npm --prefix packages/cache-export test && npm --prefix packages/codex-edit test && npm --prefix packages/herdr-subagents test && npm --prefix packages/openai-account-pool test && npm --prefix packages/scheduler test && npm --prefix packages/background-tasks test && npm --prefix packages/worktree test && node --experimental-strip-types extensions/btw/tests/history.mjs",
|
|
63
65
|
"test:root": "node tests/package-layout.mjs && node tests/release-check.mjs && node --experimental-strip-types extensions/btw/tests/history.mjs",
|
|
64
|
-
"load-test": "pi -ne -e ./packages/cache-export/extensions/cache-export.ts && pi -ne -e ./extensions/btw/index.ts && pi -ne -e ./packages/codex-edit/extensions/codex-edit.ts && pi -ne -e ./packages/herdr-subagents/extensions/herdr-subagents.ts && pi -ne -e ./packages/scheduler/extensions/scheduler.ts && pi -ne -e ./packages/worktree/extensions/worktree.ts",
|
|
66
|
+
"load-test": "pi -ne -e ./packages/cache-export/extensions/cache-export.ts && pi -ne -e ./extensions/btw/index.ts && pi -ne -e ./packages/codex-edit/extensions/codex-edit.ts && pi -ne -e ./packages/herdr-subagents/extensions/herdr-subagents.ts && pi -ne -e ./packages/scheduler/extensions/scheduler.ts && pi -ne -e ./packages/background-tasks/extensions/background-tasks.ts && pi -ne -e ./packages/worktree/extensions/worktree.ts && npm --prefix packages/background-tasks run load-test -- --toolkit",
|
|
65
67
|
"load-test:root": "pi -ne -e ./extensions/btw/index.ts",
|
|
66
68
|
"eval:agents": "node evaluations/agents-md/run.mjs",
|
|
67
69
|
"eval:agents:test": "node evaluations/agents-md/tests/run-tests.mjs",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2026-10-20 - v0.1.0
|
|
4
|
+
|
|
5
|
+
- bundle background shell tasks with runtime limits, bounded logs, completion notifications and /bg controls
|
|
6
|
+
- Bundle and enable this package in the complete toolkit by default while retaining standalone installation as an alternative.
|
|
7
|
+
|
|
8
|
+
- Add session-scoped background shell tasks with two concurrent slots, finite runtime limits, private bounded logs, Bash-aligned output views, and explicit cancellation.
|
|
9
|
+
- Add completion followUps, `/bg` controls, and an above-editor widget with five-second finished rows. Reuse Pi's native shell backend without subagents or extra system-prompt rules.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Pi Background Tasks
|
|
2
|
+
|
|
3
|
+
Run shell commands in the background, then notify the main agent when they finish. These are local processes, not subagents.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
A command such as `sleep 60; python3 ...` can spend most of its time waiting to check a build log. A normal Bash call keeps waiting for that command. This package returns a task ID immediately so the main agent can do other work.
|
|
12
|
+
|
|
13
|
+
It uses Pi's public `createLocalBashOperations()` backend for shell execution and process-group cancellation. It adds session ownership, bounded logs, status, and completion messages. It neither replaces Bash nor starts another model or Herdr pane.
|
|
14
|
+
|
|
15
|
+
## Install and update
|
|
16
|
+
|
|
17
|
+
Requires Pi **1.3.0 or later**.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pi install npm:@maxiaochao/pi-background-tasks
|
|
21
|
+
pi update npm:@maxiaochao/pi-background-tasks
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The complete `@maxiaochao/pi-toolkit` package **bundles and enables Background Tasks by default**. Choose either the toolkit or this standalone package; do not install both, because the tool and `/bg` command would be registered twice.
|
|
25
|
+
|
|
26
|
+
## Agent tool
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
background_task({ action: "start", command: "sleep 10; printf 'checks complete\\n'", description: "Delayed check", timeout: 120 })
|
|
30
|
+
background_task({ action: "list" })
|
|
31
|
+
background_task({ action: "output", id: "<returned-task-id>" })
|
|
32
|
+
background_task({ action: "cancel", id: "<returned-task-id>" })
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Start returns a running task's ID, log path and runtime limit, not the command's final output. An invalid start is rejected before execution. A later execution error becomes a failed task.
|
|
36
|
+
|
|
37
|
+
- `command`: unchanged multi-line shell script; only for start.
|
|
38
|
+
- `description`: optional display label, shortened to 200 characters.
|
|
39
|
+
- `cwd`: optional directory, resolved relative to the current session directory.
|
|
40
|
+
- `timeout`: integer seconds, **1800 by default**, from 1 through 7200. Zero and unlimited mode are rejected.
|
|
41
|
+
- `id`: required for output and cancel. IDs belong only to their originating session.
|
|
42
|
+
|
|
43
|
+
At most **two** tasks run at once. A third request fails immediately; it is not queued. Cancelling waits for process cleanup. Cancelling an already-ended task returns its status without another completion notification.
|
|
44
|
+
|
|
45
|
+
List shows running tasks and the latest 20 finished tasks, with total and omitted counts. Older logs remain queryable by their IDs until the session closes.
|
|
46
|
+
|
|
47
|
+
## User controls and status
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
/bg
|
|
51
|
+
/bg list
|
|
52
|
+
/bg output <returned-task-id>
|
|
53
|
+
/bg cancel <returned-task-id>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The commands inspect the same jobs without asking a model to perform the operation. Start a command through the agent tool; there is no second shell parser in `/bg`.
|
|
57
|
+
|
|
58
|
+
A fixed widget **above the editor** shows each task's ID, status, elapsed time, runtime limit and description. It coexists with Scheduler, Todo, and Herdr widgets. Finished rows remain for five seconds; hiding a row does not delete its record. RPC gets a compact active count; non-interactive tools work without the widget.
|
|
59
|
+
|
|
60
|
+
Completion, failure, cancellation, timeout and output-limit termination each send one `followUp` message to the main agent. It waits for the current turn, rather than interrupting it. Notifications contain metadata and a log path, **not the output body**. The agent can call output when it needs the details.
|
|
61
|
+
|
|
62
|
+
## Output and safety limits
|
|
63
|
+
|
|
64
|
+
| View | Limit |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| Model-visible output | Last 2000 lines or 50 KiB, whichever is reached first, like Bash |
|
|
67
|
+
| Codemode's `structuredContent.output` | Up to 1 MiB of raw log bytes; larger logs keep their first and last 512 KiB with an omission marker |
|
|
68
|
+
| Disk capture | 32 MiB per task, 256 MiB total per session |
|
|
69
|
+
|
|
70
|
+
Display/program-view truncation does **not** stop the command. Disk limits **do** stop the overflowing task with `output_limit`; a full session budget rejects new starts. Nothing silently evicts old logs.
|
|
71
|
+
|
|
72
|
+
Output includes a log path for further reads. `truncated` describes the program view, not disk capture. `task.capture_complete` is false while running or when log data was lost. The states are `running`, `succeeded`, `failed`, `cancelled`, `timed_out` and `output_limit`.
|
|
73
|
+
|
|
74
|
+
Output streams to private temporary logs (directory 0700, files 0600 on Unix), with bounded memory and range-limited reads. Log errors stop the task and are reported. Stdout and stderr are merged; strict ordering across the two streams is not promised. Logs and command arguments can contain secrets: choose commands accordingly.
|
|
75
|
+
|
|
76
|
+
## Codemode and Bash
|
|
77
|
+
|
|
78
|
+
Use Bash when you need a command's result before proceeding. Codemode composes tool calls and filters their results; it does not itself own background jobs.
|
|
79
|
+
|
|
80
|
+
Codemode can start a managed job and finish immediately:
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
const started = await tools.background_task({
|
|
84
|
+
action: "start",
|
|
85
|
+
command: "sleep 10; printf 'done\\n'",
|
|
86
|
+
timeout: 120,
|
|
87
|
+
});
|
|
88
|
+
text(started.task);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Later scripts can list, read, or cancel the job. This does not hold a Codemode worker open. Merely starting an unawaited Bash call is different: Codemode cancels unfinished nested calls when its script ends.
|
|
92
|
+
|
|
93
|
+
The tool description is compact. This package adds no `promptGuidelines` or extra global system-prompt rules.
|
|
94
|
+
|
|
95
|
+
## Lifetime and scope
|
|
96
|
+
|
|
97
|
+
- Exit, new/resumed/forked sessions and reload terminate active tasks, remove the widget and timers, and delete this package's captured logs. Commands' own files are left alone. History is not restored.
|
|
98
|
+
- Aborting the main agent's current turn does not cancel an accepted job. Use cancel or `/bg cancel`.
|
|
99
|
+
- Cancellation and runtime limits stop the native shell process group. Scripts must wait for their own work. Daemonized/detached commands (`nohup`, `setsid`, self-backgrounding processes), existing PID adoption, PTYs and interactive stdin are not supported.
|
|
100
|
+
- This is a distinct **local execution** tool. A third-party sandbox or remote backend installed only for Bash does not automatically cover it. Enable it only where direct local shell execution is permitted.
|
|
101
|
+
- It is not an OS sandbox: CPU/memory quotas, cleanup after SIGKILL/power loss, automatic retries, deadline renewal and persistence are out of scope. Native signal delivery and timers are best-effort.
|
|
102
|
+
- A completion message already queued in Pi cannot be selectively withdrawn. Shutdown prevents new messages from old jobs; it cannot retract one already delivered to the host.
|
|
103
|
+
|
|
104
|
+
## Remove
|
|
105
|
+
|
|
106
|
+
For a standalone installation:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pi remove npm:@maxiaochao/pi-background-tasks
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
With the toolkit, disable its Background Tasks extension through `pi config` instead of removing a separate package.
|
|
113
|
+
|
|
114
|
+
For deterministic, real-process, real-TUI and media checks, see [TESTING.md](TESTING.md).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Testing Pi Background Tasks
|
|
2
|
+
|
|
3
|
+
Run from the repository root, with Node 22.19+ and Pi 1.3.0+ on PATH:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm --prefix packages/background-tasks test
|
|
7
|
+
npm --prefix packages/background-tasks run load-test
|
|
8
|
+
npm pack ./packages/background-tasks --dry-run
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Tests use Node's built-in test runner. They resolve host peers from the installed Pi rather than installing another copy. npm's workspace `node_modules/.bin` directories are excluded from this lookup because they can contain an older development Pi. Set `PI_TEST_BINARY=/absolute/path/to/pi` to choose a specific host.
|
|
12
|
+
|
|
13
|
+
- `tasks.mjs`: fake execution and clock at the task-manager interface; immediate return, two slots, validation, timeout/cancel races, final failure, disk budgets, write errors, bounded history and shutdown.
|
|
14
|
+
- `output.mjs`: compares both output views with the real host Bash tool. Covers line/byte limits, long lines, empty output, missing final newline, UTF-8 split across chunks, invalid UTF-8 and logs larger than 1 MiB.
|
|
15
|
+
- `processes.mjs`: the native backend, a multi-line Python log check, non-blocking concurrency, PID-observed process-tree cleanup, silent busy-loop timeout and the default 32 MiB disk limit.
|
|
16
|
+
- `extension.mjs`: real extension registration with fake execution/clock; schema errors, command parity, parent-signal independence, followUp, no output in notifications, above-editor placement, five-second retention, narrow/CJK rendering, coexistence and non-TUI modes.
|
|
17
|
+
|
|
18
|
+
Load-test uses an isolated temporary agent directory, the production extension and an offline fixture provider. It does not access real provider credentials or alter the user's installed packages. From the workspace, `npm --prefix packages/background-tasks run load-test -- --toolkit` loads the complete toolkit directory through its manifest and verifies exactly one background-task tool; the root load-test runs this check too.
|
|
19
|
+
|
|
20
|
+
## Real Pi TUI
|
|
21
|
+
|
|
22
|
+
Requires Python 3 with the existing `pexpect` module; process fixtures use Bash, Python and `ps`. The current native-process/TUI suite is verified on Linux.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python3 packages/background-tasks/tests/tui-smoke.py .cache/verification/background-tui
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This starts a real Pi in a PTY with the production extension, Scheduler, the built-in Codemode tool and a deterministic fixture provider. It checks two jobs, rejection of a third, Codemode completion while jobs remain alive, explicit output, cancellation, automatic main-agent followUp, reload/new/quit cleanup and log removal. No network model calls occur. Mocked UI tests separately verify the exact five-second boundary.
|
|
29
|
+
|
|
30
|
+
Results and the actual ANSI stream are stored under the chosen evidence directory. Real screenshots require an isolated X11 terminal window, `gnome-terminal.real`, `xwininfo` and ImageMagick `import`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
gnome-terminal.real --wait --window --title='Pi Background Tasks Smoke' --geometry=140x42 -- \
|
|
34
|
+
env BG_SMOKE_RENDER=1 BG_SMOKE_CAPTURE=1 python3 \
|
|
35
|
+
"$PWD/packages/background-tasks/tests/tui-smoke.py" "$PWD/.cache/verification/background-tui-live"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
It relays the real PTY output, captures only the test window, then closes it. Inspect the running, completed and history screenshots; check the widget has disappeared in the last one. Provider decisions are fixtures; tool execution, shell jobs, queues, TUI and cleanup are real.
|
|
39
|
+
|
|
40
|
+
For the original delayed-check scenario, also run one 60-second sleep + Python heredoc check against temporary build log/exit files. The normal regression uses a one-second delay to keep the suite short.
|
|
41
|
+
|
|
42
|
+
## Media and workspace checks
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
node scripts/capture-package-media.mjs background-tasks
|
|
46
|
+
node tests/package-layout.mjs
|
|
47
|
+
node tests/release-check.mjs
|
|
48
|
+
npm test
|
|
49
|
+
npm run load-test
|
|
50
|
+
npm pack --dry-run
|
|
51
|
+
git diff --check
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The media command requires the real-TUI screenshot prerequisites above, plus Pillow. It reruns the real TUI and renders PNG/GIF from captured running/completed/history frames, not invented output. Inspect and click both links in `docs/package-media-gallery.html` with Chrome. The root tarball must include `packages/background-tasks/`, and the root default loadout must enable its extension exactly once. The standalone tarball remains independently loadable.
|
|
55
|
+
|
|
56
|
+
Record actual checks and any missing prerequisites in `docs/verification.md`. A missing GUI, skipped browser check or unavailable Pi does not count as a successful end-to-end verification.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { createLocalBashOperations, type BashOperations, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
|
+
import { stripVTControlCharacters } from "node:util";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { BackgroundTasks, errorText, summary, systemClock, type Clock, type TaskSnapshot } from "../src/background-tasks.ts";
|
|
7
|
+
import { BackgroundWidget, FINISHED_VISIBLE_MS, WIDGET_KEY, duration, visibleTasks } from "../src/widget.ts";
|
|
8
|
+
|
|
9
|
+
const actions = ["start", "list", "output", "cancel"] as const;
|
|
10
|
+
type Action = typeof actions[number];
|
|
11
|
+
interface Params {
|
|
12
|
+
action: Action;
|
|
13
|
+
command?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
cwd?: string;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
id?: string;
|
|
18
|
+
}
|
|
19
|
+
const Parameters = Type.Object({
|
|
20
|
+
action: StringEnum(actions, { description: "start returns a task ID immediately; list checks tasks; output reads a bash-limited snapshot; cancel stops one task" }),
|
|
21
|
+
command: Type.Optional(Type.String({ description: "Shell command for start; may be multi-line. Runs locally, with stdin closed." })),
|
|
22
|
+
description: Type.Optional(Type.String({ description: "Short display label for start" })),
|
|
23
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for start; defaults to the session directory. Relative paths resolve from it." })),
|
|
24
|
+
timeout: Type.Optional(Type.Integer({ minimum: 1, maximum: 7200, description: "Runtime limit for start, in seconds: default 1800 (30 minutes), maximum 7200 (2 hours); no unlimited mode" })),
|
|
25
|
+
id: Type.Optional(Type.String({ description: "Task ID for output or cancel; output shows the last 2000 lines or 50 KiB. Codemode receives up to 1 MiB, keeping head/tail if larger, plus a log path." })),
|
|
26
|
+
}, { additionalProperties: false });
|
|
27
|
+
const TaskSchema = Type.Object({
|
|
28
|
+
id: Type.String(), status: StringEnum(["running", "succeeded", "failed", "cancelled", "timed_out", "output_limit"] as const),
|
|
29
|
+
description: Type.String(), cwd: Type.String(), started_at: Type.Number(), ended_at: Type.Optional(Type.Number()),
|
|
30
|
+
timeout: Type.Number(), elapsed_seconds: Type.Number(), log_path: Type.String(), bytes: Type.Number(),
|
|
31
|
+
capture_complete: Type.Boolean(), exit_code: Type.Optional(Type.Number()), reason: Type.Optional(Type.String()),
|
|
32
|
+
});
|
|
33
|
+
const OutputSchema = Type.Object({
|
|
34
|
+
action: Type.Optional(StringEnum(actions)), task: Type.Optional(TaskSchema), tasks: Type.Optional(Type.Array(TaskSchema)),
|
|
35
|
+
running: Type.Optional(Type.Number()), total: Type.Optional(Type.Number()), omitted: Type.Optional(Type.Number()),
|
|
36
|
+
output: Type.Optional(Type.String()), truncated: Type.Optional(Type.Boolean()), full_output_path: Type.Optional(Type.String()),
|
|
37
|
+
error: Type.Optional(Type.String()),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function validate(params: Params): void {
|
|
41
|
+
if (!actions.includes(params.action)) throw new Error("action must be start, list, output, or cancel");
|
|
42
|
+
const allowed = params.action === "start" ? ["action", "command", "description", "cwd", "timeout"]
|
|
43
|
+
: params.action === "list" ? ["action"] : ["action", "id"];
|
|
44
|
+
if (Object.keys(params).some((key) => !allowed.includes(key))) throw new Error(`Unexpected parameter for ${params.action}`);
|
|
45
|
+
if (params.action === "start" && typeof params.command !== "string") throw new Error("start requires command");
|
|
46
|
+
if ((params.action === "output" || params.action === "cancel") && (typeof params.id !== "string" || !params.id)) throw new Error(`${params.action} requires id`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatTask(task: TaskSnapshot): string {
|
|
50
|
+
return `${task.id} · ${task.status} · ${duration(task.elapsed_seconds)} / ${duration(task.timeout)}${task.exit_code === undefined ? "" : ` · exit ${task.exit_code}`} · ${JSON.stringify(task.description)}`;
|
|
51
|
+
}
|
|
52
|
+
function parseCommand(input: string): Params {
|
|
53
|
+
const parts = input.trim().split(/\s+/).filter(Boolean);
|
|
54
|
+
if (parts.length === 0 || (parts.length === 1 && parts[0] === "list")) return { action: "list" };
|
|
55
|
+
if (parts.length === 2 && (parts[0] === "output" || parts[0] === "cancel")) return { action: parts[0], id: parts[1] };
|
|
56
|
+
throw new Error("usage: /bg [list | output <id> | cancel <id>]");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default function (pi: ExtensionAPI, dependencies: { operations?: BashOperations; clock?: Clock; logRoot?: string } = {}) {
|
|
60
|
+
const clock = dependencies.clock ?? systemClock;
|
|
61
|
+
let manager: BackgroundTasks | undefined;
|
|
62
|
+
let context: ExtensionContext | undefined;
|
|
63
|
+
let generation = 0;
|
|
64
|
+
let timer: unknown;
|
|
65
|
+
let mounted: { widget: BackgroundWidget; tui: { requestRender(): void } } | undefined;
|
|
66
|
+
|
|
67
|
+
const clearTimer = () => {
|
|
68
|
+
if (timer !== undefined) clock.clearTimeout(timer);
|
|
69
|
+
timer = undefined;
|
|
70
|
+
};
|
|
71
|
+
const refresh = () => {
|
|
72
|
+
if (!context?.hasUI || !manager) return;
|
|
73
|
+
const list = manager.list();
|
|
74
|
+
if (context.mode !== "tui") {
|
|
75
|
+
context.ui.setStatus(WIDGET_KEY, list.running ? `Background tasks ${list.running}/2` : undefined);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const visible = visibleTasks(list.tasks, clock.now());
|
|
79
|
+
if (visible.length === 0) {
|
|
80
|
+
clearTimer();
|
|
81
|
+
if (mounted) {
|
|
82
|
+
context.ui.setWidget(WIDGET_KEY, undefined);
|
|
83
|
+
mounted.tui.requestRender();
|
|
84
|
+
mounted = undefined;
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (mounted) {
|
|
89
|
+
mounted.widget.invalidate();
|
|
90
|
+
mounted.tui.requestRender();
|
|
91
|
+
} else {
|
|
92
|
+
context.ui.setWidget(WIDGET_KEY, (tui, theme) => {
|
|
93
|
+
const widget = new BackgroundWidget(() => manager?.list().tasks ?? [], clock, theme);
|
|
94
|
+
mounted = { widget, tui };
|
|
95
|
+
return widget;
|
|
96
|
+
}, { placement: "aboveEditor" });
|
|
97
|
+
}
|
|
98
|
+
if (timer === undefined) {
|
|
99
|
+
const delay = Math.min(1000, ...visible.filter((task) => task.ended_at !== undefined).map((task) => task.ended_at! + FINISHED_VISIBLE_MS - clock.now()));
|
|
100
|
+
timer = clock.setTimeout(() => { timer = undefined; refresh(); }, Math.max(1, delay));
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const dispose = async () => {
|
|
104
|
+
generation++;
|
|
105
|
+
const previous = manager;
|
|
106
|
+
const previousContext = context;
|
|
107
|
+
manager = undefined;
|
|
108
|
+
context = undefined;
|
|
109
|
+
clearTimer();
|
|
110
|
+
try {
|
|
111
|
+
if (previousContext?.hasUI) {
|
|
112
|
+
previousContext.ui.setStatus(WIDGET_KEY, undefined);
|
|
113
|
+
if (previousContext.mode === "tui") previousContext.ui.setWidget(WIDGET_KEY, undefined);
|
|
114
|
+
}
|
|
115
|
+
mounted = undefined;
|
|
116
|
+
} finally { await previous?.shutdown(); }
|
|
117
|
+
};
|
|
118
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
119
|
+
await dispose();
|
|
120
|
+
context = ctx;
|
|
121
|
+
const epoch = generation;
|
|
122
|
+
manager = new BackgroundTasks({
|
|
123
|
+
operations: dependencies.operations ?? createLocalBashOperations(), clock, logRoot: dependencies.logRoot,
|
|
124
|
+
onChange() { if (generation === epoch) refresh(); },
|
|
125
|
+
onComplete(task) {
|
|
126
|
+
if (generation !== epoch || !context) return;
|
|
127
|
+
pi.sendMessage({
|
|
128
|
+
customType: "background-task-finished", display: true, details: { task },
|
|
129
|
+
content: `[Background task finished]\n${formatTask(task)}${task.reason ? `\nReason: ${task.reason}` : ""}\nCaptured log: ${task.log_path}${task.capture_complete ? "" : "\nOutput capture is incomplete."}`,
|
|
130
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
pi.on("session_shutdown", dispose);
|
|
135
|
+
|
|
136
|
+
const perform = async (params: Params, cwd: string) => {
|
|
137
|
+
validate(params);
|
|
138
|
+
if (!manager) throw new Error("Background tasks session is not ready");
|
|
139
|
+
if (params.action === "start") {
|
|
140
|
+
const task = manager.start({ command: params.command!, description: params.description, cwd: params.cwd, timeout: params.timeout }, cwd);
|
|
141
|
+
return {
|
|
142
|
+
text: `Started ${formatTask(task)}\nLog: ${task.log_path}\nAt most 2 tasks run at once. Tasks stop on session changes, reload, or exit; captured logs are then removed.`,
|
|
143
|
+
data: { action: params.action, task }, details: { action: params.action, task }, isError: false,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (params.action === "list") {
|
|
147
|
+
const list = manager.list();
|
|
148
|
+
return {
|
|
149
|
+
text: list.total ? `${list.running}/2 running; ${list.total} total${list.omitted ? `; ${list.omitted} older tasks omitted` : ""}\n${list.tasks.map(formatTask).join("\n")}` : "No background tasks in this session.",
|
|
150
|
+
data: { action: params.action, ...list }, details: { action: params.action, ...list }, isError: false,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (params.action === "cancel") {
|
|
154
|
+
const task = await manager.cancel(params.id!);
|
|
155
|
+
return { text: formatTask(task), data: { action: params.action, task }, details: { action: params.action, task }, isError: false };
|
|
156
|
+
}
|
|
157
|
+
const { text, output, truncated, truncation, ...task } = manager.output(params.id!);
|
|
158
|
+
const capturedText = task.capture_complete ? text : text.replace("Full output:", "Captured output:");
|
|
159
|
+
return {
|
|
160
|
+
text: `${formatTask(task)}\n[Untrusted shell output${task.status === "running" ? "; running snapshot" : ""}]\n${capturedText}\nCaptured log: ${task.log_path}${task.capture_complete ? "" : "\nCapture incomplete (task still running or output was lost)."}`,
|
|
161
|
+
data: { action: params.action, task, output, truncated, full_output_path: task.log_path },
|
|
162
|
+
details: { action: params.action, task, truncation },
|
|
163
|
+
isError: ["failed", "timed_out", "output_limit"].includes(task.status),
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
pi.registerTool({
|
|
168
|
+
name: "background_task", label: "Background task",
|
|
169
|
+
description: "Start shell commands as background tasks and return task IDs immediately. Supports status, output, and cancellation; notifies the main agent on completion.",
|
|
170
|
+
parameters: Parameters, outputSchema: OutputSchema,
|
|
171
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
172
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
173
|
+
try {
|
|
174
|
+
if (signal?.aborted) throw new Error("Tool call aborted before task acceptance");
|
|
175
|
+
const result = await perform(params, ctx?.cwd ?? context?.cwd ?? process.cwd());
|
|
176
|
+
return { content: [{ type: "text" as const, text: result.text }], structuredContent: result.data, details: result.details, isError: result.isError };
|
|
177
|
+
} catch (error) {
|
|
178
|
+
const message = errorText(error);
|
|
179
|
+
return { content: [{ type: "text" as const, text: `Error: ${message}` }], structuredContent: { error: message }, details: { error: message }, isError: true };
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
renderCall(args, theme) {
|
|
183
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("background_task "))}${theme.fg("muted", summary(String(args.action ?? "")))}`, 0, 0);
|
|
184
|
+
},
|
|
185
|
+
renderResult(result, _options, theme) {
|
|
186
|
+
const block = result.content[0];
|
|
187
|
+
const text = block?.type === "text" ? stripVTControlCharacters(block.text).replace(/[\x00-\x08\x0b-\x1f\x7f\uFFF9-\uFFFB]/g, "") : "";
|
|
188
|
+
return new Text(theme.fg(result.isError ? "error" : "muted", text), 0, 0);
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
pi.registerCommand("bg", {
|
|
192
|
+
description: "List background shell tasks, read output, or cancel a task",
|
|
193
|
+
handler: async (args, ctx) => {
|
|
194
|
+
try {
|
|
195
|
+
const result = await perform(parseCommand(args), ctx.cwd);
|
|
196
|
+
pi.sendMessage({ customType: "background-task-inspect", content: result.text, display: true, details: result.details }, { triggerTurn: false });
|
|
197
|
+
} catch (error) { ctx.ui.notify(errorText(error), "error"); }
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maxiaochao/pi-background-tasks",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Session-scoped background shell tasks with completion notifications for Pi.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"background-tasks"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
"extensions",
|
|
13
|
+
"src",
|
|
14
|
+
"tests",
|
|
15
|
+
"docs",
|
|
16
|
+
"README.md",
|
|
17
|
+
"TESTING.md",
|
|
18
|
+
"CHANGELOG.md"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/chaochaoxiaochao/pi-toolkit.git",
|
|
23
|
+
"directory": "packages/background-tasks"
|
|
24
|
+
},
|
|
25
|
+
"pi": {
|
|
26
|
+
"extensions": [
|
|
27
|
+
"./extensions/background-tasks.ts"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@earendil-works/pi-ai": "*",
|
|
32
|
+
"@earendil-works/pi-coding-agent": ">=1.3.0",
|
|
33
|
+
"@earendil-works/pi-tui": "*",
|
|
34
|
+
"typebox": "*"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "node --experimental-strip-types --import ./tests/runtime.mjs --test tests/tasks.mjs tests/output.mjs tests/processes.mjs tests/extension.mjs",
|
|
38
|
+
"load-test": "node --experimental-strip-types tests/load.mjs"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|