pi-multi-agent-skill 0.1.0 → 0.2.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 +32 -0
- package/README.md +87 -41
- package/package.json +6 -3
- package/skills/multi-agent/SKILL.md +1 -0
- package/skills/multi-agent/references/commands.md +9 -5
- package/skills/multi-agent/references/lifecycle.md +9 -9
- package/skills/multi-agent/references/state.md +5 -3
- package/skills/multi-agent/references/troubleshooting.md +6 -3
- package/skills/multi-agent/references/usage.md +40 -0
- package/skills/multi-agent/scripts/multi_agent.py +65 -19
- package/skills/multi-agent/scripts/usage.py +1325 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.1] - 2026-11-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- A standalone multi-agent usage report shows token counts and recorded costs by date, project, job, model and profile. The skill tells the agent when to run it.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Forked sessions exclude usage from before their creation without reading parent sessions.
|
|
12
|
+
- Model breakdowns use the concrete response model when pi records it.
|
|
13
|
+
- Date cutoffs before the first usage record return empty reports instead of an invalid-range error.
|
|
14
|
+
|
|
15
|
+
## [0.2.0] - 2026-10-26
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
- Environment variables use the `PI_MULTI_AGENT_` prefix, such as `PI_MULTI_AGENT_PI_BIN`. The old `MULTI_AGENT_` names are not read.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `rm --all` deletes every job of the current project that is not running.
|
|
24
|
+
- Starting a job with a name already in use fails and reports the existing job's status and age.
|
|
25
|
+
- `run` and `resume` check that the pi program exists before starting a job.
|
|
26
|
+
- Agents run `pi-request-user-input` in non-interactive mode, so its tool fails instead of opening a dialog.
|
|
27
|
+
|
|
28
|
+
## [0.1.0] - 2026-10-12
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Initial release. The `multi-agent` skill and its CLI run pi agents as named foreground or background jobs that can be waited on, steered, aborted, resumed, and inspected.
|
package/README.md
CHANGED
|
@@ -5,35 +5,10 @@ needs no extension and adds no tools. Your agent starts other agents, runs
|
|
|
5
5
|
them in parallel or in the background, and waits for, steers, stops or
|
|
6
6
|
resumes them. Any harness or agent that has a shell can use it.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
skill, your agent's context holds only the skill's description, a few
|
|
13
|
-
dozen tokens, until it decides to delegate. Then it loads SKILL.md, which
|
|
14
|
-
is about 1.2K tokens and is all it needs for a fully working multi-agent
|
|
15
|
-
setup. The details of each command stay in separate references that it
|
|
16
|
-
reads only when needed.
|
|
17
|
-
- **Everything a subagent tool does, without the tool.** Your agent gets the
|
|
18
|
-
same controls a dedicated extension would give it: start agents, wait for
|
|
19
|
-
them, `steer` one with a correction, `abort` it and keep what it has so
|
|
20
|
-
far, `resume` a finished job with a follow-up task, and see what each job
|
|
21
|
-
is doing. You can run the same commands yourself from a terminal.
|
|
22
|
-
- **Parallel and background agents.** Run a quick question in the
|
|
23
|
-
foreground, or start several jobs at once and collect their results later.
|
|
24
|
-
- **Stops when you stop.** A foreground agent ends when the command that
|
|
25
|
-
started it ends: press Esc, cancel the tool call or close the session, and
|
|
26
|
-
the agent and the commands it was running stop with it, at every level of
|
|
27
|
-
nesting. Background jobs keep running until they finish or time out.
|
|
28
|
-
- **Your full pi setup.** Each agent loads the same extensions, MCP servers,
|
|
29
|
-
skills and settings as an interactive pi, and uses your model unless its
|
|
30
|
-
profile picks another.
|
|
31
|
-
- **Profiles.** They work as in other subagent tools: each one sets an
|
|
32
|
-
agent's system prompt, model, thinking level and tools.
|
|
33
|
-
- **Fork your conversation.** `--fork` hands the agent everything you have
|
|
34
|
-
discussed so far, for tasks that need that context.
|
|
35
|
-
- **Works outside pi.** Claude Code, Codex or any other agent with a shell
|
|
36
|
-
can run the same commands to delegate work to pi agents.
|
|
8
|
+
It aims to be minimal while keeping subagents as useful as an extension and
|
|
9
|
+
tool based approach. A subagent extension adds its tool definitions to every
|
|
10
|
+
request, while this skill adds only a few dozen tokens of description to your
|
|
11
|
+
agent's context until it decides to delegate.
|
|
37
12
|
|
|
38
13
|
## Install
|
|
39
14
|
|
|
@@ -64,6 +39,65 @@ ln -s "$PWD/skills/multi-agent" ~/.pi/agent/skills/multi-agent
|
|
|
64
39
|
|
|
65
40
|
Requirements: pi and Python 3.9 or later. Nothing else to install.
|
|
66
41
|
|
|
42
|
+
Agents run `pi` by default. To run another build of pi, such as pi-bolt, set
|
|
43
|
+
`PI_MULTI_AGENT_PI_BIN` in the environment that starts it. Your agent's shell
|
|
44
|
+
inherits the variable, and so do the agents it starts:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
alias pi-bolt='PI_MULTI_AGENT_PI_BIN=pi-bolt pi-bolt'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
- **Small token cost.** A subagent extension adds its tool definitions to
|
|
53
|
+
every request, whether or not your agent delegates anything. With this
|
|
54
|
+
skill, your agent's context holds only the skill's description, a few
|
|
55
|
+
dozen tokens, until it decides to delegate. Then it loads SKILL.md, which
|
|
56
|
+
is about 1.2K tokens and is all it needs for a fully working multi-agent
|
|
57
|
+
setup. The details of each command stay in separate references that it
|
|
58
|
+
reads only when needed.
|
|
59
|
+
- **Everything a subagent tool does, without the tool.** Your agent gets the
|
|
60
|
+
same controls a dedicated extension would give it: start agents, wait for
|
|
61
|
+
them, `steer` one with a correction, `abort` it and keep what it has so
|
|
62
|
+
far, `resume` a finished job with a follow-up task, and see what each job
|
|
63
|
+
is doing. You can run the same commands yourself from a terminal.
|
|
64
|
+
- **Foreground agents.** Run a quick question and wait for the answer. A
|
|
65
|
+
foreground agent is tied to the command that started it: press Esc, cancel
|
|
66
|
+
the tool call or close the session, and the agent and the commands it was
|
|
67
|
+
running stop with it, at every level of nesting.
|
|
68
|
+
- **Parallel and background agents.** Start several jobs at once and collect
|
|
69
|
+
their results later. A background job is independent of your session: it
|
|
70
|
+
keeps running after the session that started it ends, until it finishes
|
|
71
|
+
or times out, and you can collect its result from a later session.
|
|
72
|
+
- **Your full pi setup.** Each agent loads the same extensions, MCP servers,
|
|
73
|
+
skills and settings as an interactive pi, and uses your model unless its
|
|
74
|
+
profile picks another.
|
|
75
|
+
- **Profiles.** They work as in other subagent tools: each one sets an
|
|
76
|
+
agent's system prompt, model, thinking level and tools.
|
|
77
|
+
- **Fork your conversation.** `--fork` hands the agent everything you have
|
|
78
|
+
discussed so far, for tasks that need that context.
|
|
79
|
+
- **Works outside pi.** Claude Code, Codex or any other agent with a shell
|
|
80
|
+
can run the same commands to delegate work to pi agents.
|
|
81
|
+
|
|
82
|
+
## Non-goals
|
|
83
|
+
|
|
84
|
+
These are deliberate non-goals, to keep the skill small and free of bloat.
|
|
85
|
+
Each is better as a separate extension or skill that you pick to match your
|
|
86
|
+
own workflow. This follows the minimalism of pi itself and the Unix
|
|
87
|
+
philosophy: do one thing well and compose.
|
|
88
|
+
|
|
89
|
+
- **Memory.** Agents do not share or keep memory between jobs. Use a memory
|
|
90
|
+
extension, or have agents write handoff notes.
|
|
91
|
+
- **Scheduling.** There are no cron-style or recurring agents. Use cron,
|
|
92
|
+
`systemd` timers or a scheduler extension to run the commands.
|
|
93
|
+
- **Worktrees.** Agents are not isolated in git worktrees. Just prompt the
|
|
94
|
+
agent to use a worktree.
|
|
95
|
+
- **Dynamic workflows.** There is no scripting layer for orchestration such
|
|
96
|
+
as pipelines, parallel fan-out with gates, or phases. Your agent can
|
|
97
|
+
already start, wait for and chain jobs with plain commands. For
|
|
98
|
+
deterministic scripts, use an extension such as
|
|
99
|
+
[pi-dynamic-workflow](https://github.com/milanglacier/pi-dynamic-workflow).
|
|
100
|
+
|
|
67
101
|
## Usage
|
|
68
102
|
|
|
69
103
|
Your agent drives the skill on its own, but the commands also work by hand:
|
|
@@ -81,21 +115,33 @@ See [`skills/multi-agent/references/`](skills/multi-agent/references/) for
|
|
|
81
115
|
every command and option, how jobs start and stop, the profile format, where
|
|
82
116
|
state is kept and troubleshooting.
|
|
83
117
|
|
|
84
|
-
##
|
|
118
|
+
## Profiles
|
|
85
119
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
The tests use an offline stand-in for pi, so they call no model.
|
|
120
|
+
A profile is a Markdown file with frontmatter that sets an agent's system
|
|
121
|
+
prompt, model, thinking level and tools. `--profile NAME` selects one;
|
|
122
|
+
`multi_agent.py profiles` lists the available ones.
|
|
91
123
|
|
|
92
|
-
|
|
124
|
+
```markdown
|
|
125
|
+
---
|
|
126
|
+
model: anthropic/claude-sonnet-5
|
|
127
|
+
thinking: high
|
|
128
|
+
tools: read, grep, find
|
|
129
|
+
---
|
|
130
|
+
You review code changes and report bugs.
|
|
131
|
+
```
|
|
93
132
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
133
|
+
Profiles are looked up in `<cwd>/.pi/agents/*.md`, then
|
|
134
|
+
`$PI_CODING_AGENT_DIR/agents/*.md` (`~/.pi/agent/agents/` by default); when
|
|
135
|
+
names collide, the local profile wins. Two profiles are built in:
|
|
136
|
+
`general-purpose` (the default) and `Explore`, a read-only search agent.
|
|
137
|
+
Supported fields are `name`, `description`, `model`, `thinking`, `tools`,
|
|
138
|
+
`disallowed_tools`, `prompt_mode` and `enabled`. Fields do not merge across
|
|
139
|
+
files, so a local profile replaces the profile it overrides as a whole and
|
|
140
|
+
must supply the entire profile; `enabled: false` is the exception and hides
|
|
141
|
+
the name on its own. Unknown fields are ignored, and nested YAML is not
|
|
142
|
+
supported. See
|
|
143
|
+
[`references/profiles.md`](skills/multi-agent/references/profiles.md) for the
|
|
144
|
+
field details.
|
|
99
145
|
|
|
100
146
|
## License
|
|
101
147
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-multi-agent-skill",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A minimal multi-agent skill+cli for pi. It adds no extension or tools, so it uses only a few dozen tokens of the context window until the main agent delegates, yet it gives the same controls as a subagent extension. Background agents keep running after the session closes, and any later session can collect their results.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Milan Glacier",
|
|
@@ -24,9 +24,12 @@
|
|
|
24
24
|
"skills/multi-agent/references/*.md",
|
|
25
25
|
"skills/multi-agent/scripts/*.py",
|
|
26
26
|
"README.md",
|
|
27
|
-
"LICENSE"
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"CHANGELOG.md"
|
|
28
29
|
],
|
|
29
30
|
"pi": {
|
|
30
|
-
"skills": [
|
|
31
|
+
"skills": [
|
|
32
|
+
"./skills"
|
|
33
|
+
]
|
|
31
34
|
}
|
|
32
35
|
}
|
|
@@ -137,4 +137,5 @@ only when you need it:
|
|
|
137
137
|
model, tools and system prompt are chosen.
|
|
138
138
|
- `state.md`: where jobs and conversations are stored, and the agent's
|
|
139
139
|
environment.
|
|
140
|
+
- `usage.md`: token and cost report.
|
|
140
141
|
- `troubleshooting.md`: error messages and what to do about them.
|
|
@@ -17,12 +17,12 @@ An empty prompt or message is a usage error. `--help` prints a usage summary.
|
|
|
17
17
|
| `peek NAME [-n N]` | Print the last N lines (default 20) of the job's progress log. |
|
|
18
18
|
| `list [--all]` | List the jobs of the current project. |
|
|
19
19
|
| `profiles` | List the available profiles. |
|
|
20
|
-
| `rm NAME` | Delete
|
|
20
|
+
| `rm NAME` / `rm --all` | Delete one job that is not running, or every such job of the current project, with their conversations. |
|
|
21
21
|
|
|
22
22
|
## Projects and job names
|
|
23
23
|
|
|
24
24
|
A job belongs to the project of the directory where `run` was called. `run`,
|
|
25
|
-
`list` and `
|
|
25
|
+
`list`, `kill --all` and `rm --all` work on the current directory's project only.
|
|
26
26
|
|
|
27
27
|
The other commands look for NAME in the current project first. If the project
|
|
28
28
|
has no job of that name, they search every project and use the job when
|
|
@@ -45,8 +45,9 @@ run it from the job's project.
|
|
|
45
45
|
|
|
46
46
|
A job name contains letters, digits, `.`, `_` and `-`, starts and ends with a
|
|
47
47
|
letter or digit, and has at most 64 characters. `run` refuses a name that
|
|
48
|
-
already exists in the project
|
|
49
|
-
|
|
48
|
+
already exists in the project and reports its status and age. Pick another
|
|
49
|
+
`--name`. Use `resume` or `rm` only for a job you started or the user asked
|
|
50
|
+
about.
|
|
50
51
|
|
|
51
52
|
`--cwd` changes where the agent works and where project profiles are looked
|
|
52
53
|
up (`DIR/.pi/agents/`). The job still belongs to the current directory's
|
|
@@ -197,10 +198,13 @@ It looks up project profiles in the current directory.
|
|
|
197
198
|
|
|
198
199
|
## rm
|
|
199
200
|
|
|
200
|
-
`rm` deletes a job that is not running: its state directory and its
|
|
201
|
+
`rm NAME` deletes a job that is not running: its state directory and its
|
|
201
202
|
conversation file. It prints `removed: NAME` and fails with `job NAME is
|
|
202
203
|
running; kill it first` while the job runs.
|
|
203
204
|
|
|
205
|
+
`rm --all` deletes every job of the current project that is not running,
|
|
206
|
+
with its conversation, and leaves running jobs alone.
|
|
207
|
+
|
|
204
208
|
## Exit codes
|
|
205
209
|
|
|
206
210
|
| Code | Meaning |
|
|
@@ -74,19 +74,19 @@ the next run.
|
|
|
74
74
|
|
|
75
75
|
No person watches an agent. When an extension inside the agent opens a dialog
|
|
76
76
|
(select, confirm, input or editor), it is cancelled at once and logged in the
|
|
77
|
-
progress log as `denied:`.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
message.
|
|
77
|
+
progress log as `denied:`. Extensions that have a non-interactive mode run in
|
|
78
|
+
it (see `state.md`), so tool calls that need a person fail without opening a
|
|
79
|
+
dialog. The agent is told not to retry them and to report what it could not
|
|
80
|
+
do in its final message.
|
|
81
81
|
|
|
82
82
|
## Nesting
|
|
83
83
|
|
|
84
|
-
An agent can start agents of its own. `
|
|
85
|
-
or unset for the user's agent, 1 for an agent it started, and so on. `run` and
|
|
84
|
+
An agent can start agents of its own. `PI_MULTI_AGENT_DEPTH` counts the depth:
|
|
85
|
+
0 or unset for the user's agent, 1 for an agent it started, and so on. `run` and
|
|
86
86
|
`resume` refuse to start when the caller's depth is at least
|
|
87
|
-
`
|
|
87
|
+
`PI_MULTI_AGENT_MAX_DEPTH` (default 2), so by default the user's agent and its
|
|
88
88
|
agents can delegate, and their agents cannot. Agents inherit
|
|
89
|
-
`
|
|
89
|
+
`PI_MULTI_AGENT_MAX_DEPTH` from the environment of the user's agent.
|
|
90
90
|
|
|
91
|
-
`
|
|
91
|
+
`PI_MULTI_AGENT_JOB` holds the job directory of the agent that is running.
|
|
92
92
|
`status` and `list` use it to show the job that started each job.
|
|
@@ -45,11 +45,13 @@ because two processes would then write the same session file.
|
|
|
45
45
|
|
|
46
46
|
## The agent's environment
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
The script starts the program in `PI_MULTI_AGENT_PI_BIN` (default `pi`). An
|
|
49
|
+
agent inherits the environment of the command that started it, plus:
|
|
49
50
|
|
|
50
51
|
| Variable | Value |
|
|
51
52
|
|---|---|
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
53
|
+
| `PI_MULTI_AGENT_JOB` | The agent's job directory. |
|
|
54
|
+
| `PI_MULTI_AGENT_DEPTH` | The agent's nesting depth; see `lifecycle.md`. |
|
|
54
55
|
| `PI_MINIMAL_PERMISSION_SYSTEM_NONINTERACTIVE` | `1`, so that extension never waits for an approval. |
|
|
56
|
+
| `PI_REQUEST_USER_INPUT_NONINTERACTIVE` | `1`, so that extension never asks the user a question. |
|
|
55
57
|
| `PWD` | The agent's working directory. |
|
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
|
|
26
26
|
- **`job is not running; use resume`.** The run has ended or is ending.
|
|
27
27
|
`resume` the job with a new prompt instead of steering it.
|
|
28
|
-
- **`job NAME already exists in this project`.**
|
|
29
|
-
|
|
28
|
+
- **`job NAME already exists in this project`.** Pick another `--name`.
|
|
29
|
+
The error shows the existing job's status and age. Use `resume` or `rm`
|
|
30
|
+
only for a job you started or the user asked about.
|
|
30
31
|
- **`job NAME is running` from `resume` or `rm`.** The job's runner is
|
|
31
32
|
still alive, or another command was starting the job at the same moment.
|
|
32
33
|
`wait` for it, or `kill` it first.
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
- **`--fork needs PI_SESSION_FILE`.** The command does not run inside pi, so
|
|
36
37
|
there is no conversation to copy. Drop `--fork` and write a self-contained
|
|
37
38
|
prompt.
|
|
38
|
-
- **`cannot start an agent: ...
|
|
39
|
+
- **`cannot start an agent: ... PI_MULTI_AGENT_MAX_DEPTH`.** The caller is too
|
|
39
40
|
deep in a chain of agents. Do the work directly.
|
|
40
41
|
- **`the runner of job NAME is not responding`.** The runner did not answer
|
|
41
42
|
`kill` within 10 seconds, or did not exit within 20 seconds after it
|
|
@@ -48,6 +49,8 @@
|
|
|
48
49
|
|
|
49
50
|
## Environment
|
|
50
51
|
|
|
52
|
+
- **`cannot find the pi program`.** Set `PI_MULTI_AGENT_PI_BIN` to the command
|
|
53
|
+
that starts pi, such as `pi-bolt`, or ask the user which one they use.
|
|
51
54
|
- **`flock does not work on ...`.** The state directory is on a file system
|
|
52
55
|
without working file locks, such as some NFS or SMB mounts. Set
|
|
53
56
|
`XDG_STATE_HOME` to a directory on a local file system.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Usage report
|
|
2
|
+
|
|
3
|
+
`usage.py` shows token counts and the cost pi recorded for the multi-agent
|
|
4
|
+
sessions on this machine. Run it only when the user asks what the agents used
|
|
5
|
+
or cost.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python3 "$SKILL_DIR/scripts/usage.py" [VIEW] [OPTIONS]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The default `overview` view shows the all-time total, the token breakdown,
|
|
12
|
+
the top projects and models, and the last seven days. Start there; it answers
|
|
13
|
+
most questions.
|
|
14
|
+
|
|
15
|
+
| View | What it shows |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `daily`, `weekly`, `monthly` | Totals per period, including empty periods. |
|
|
18
|
+
| `project` | Totals per project. |
|
|
19
|
+
| `session` | Totals per job, with its profile and models. |
|
|
20
|
+
| `model` | Totals per provider and model. |
|
|
21
|
+
| `profile` | Totals per profile. |
|
|
22
|
+
|
|
23
|
+
| Option | Meaning |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `--since DATE`, `--until DATE` | Inclusive date range for every view. |
|
|
26
|
+
| `--days N`, `--weeks N`, `--months N` | Window for the time view; defaults 30, 12 and 12. |
|
|
27
|
+
| `--project TEXT` | Only projects whose path contains TEXT; repeatable. |
|
|
28
|
+
| `--all` | All time instead of the default window. |
|
|
29
|
+
| `--limit N` | Rows in a group view; `0` shows all. |
|
|
30
|
+
| `--timezone NAME` | IANA timezone for day boundaries; default local. |
|
|
31
|
+
| `--json` | One JSON object instead of tables. |
|
|
32
|
+
|
|
33
|
+
The report covers every project on the machine, not just the current one. Use
|
|
34
|
+
`--project` to narrow it. Costs come from pi; records without a recorded cost
|
|
35
|
+
are counted as unpriced. Running jobs appear with the usage recorded so far.
|
|
36
|
+
|
|
37
|
+
Group views print every row unless `--limit` is given, so pass `--limit 10`
|
|
38
|
+
and raise it only when the user asks for more. When you need to compare or
|
|
39
|
+
sum numbers, `--json` is easier than a wide table. Report only the numbers
|
|
40
|
+
the user asked for, not the whole output.
|
|
@@ -53,14 +53,14 @@ NOT_RUNNING = "job is not running; use resume"
|
|
|
53
53
|
|
|
54
54
|
def time_scale():
|
|
55
55
|
"""The factor for the timing constants below; tests set it to make them short."""
|
|
56
|
-
value = os.environ.get("
|
|
56
|
+
value = os.environ.get("PI_MULTI_AGENT_TIME_SCALE") or "1"
|
|
57
57
|
try:
|
|
58
58
|
scale = float(value)
|
|
59
59
|
except ValueError:
|
|
60
60
|
scale = 0.0
|
|
61
61
|
if not 0 < scale < float("inf"):
|
|
62
62
|
raise SystemExit(
|
|
63
|
-
f"multi-agent: error:
|
|
63
|
+
f"multi-agent: error: PI_MULTI_AGENT_TIME_SCALE needs a positive number, got: {value}"
|
|
64
64
|
)
|
|
65
65
|
return scale
|
|
66
66
|
|
|
@@ -94,8 +94,8 @@ MULTI_AGENT_BLOCK = """<multi-agent>
|
|
|
94
94
|
You are an agent named {name}, started by another agent to work on one
|
|
95
95
|
task. Whoever reads your result sees only your final message, so make it a
|
|
96
96
|
complete, self-contained answer to the task. No person is available to
|
|
97
|
-
answer questions. If a tool call
|
|
98
|
-
|
|
97
|
+
answer questions. If a tool call fails because it needs a person, do not
|
|
98
|
+
retry it; say in your final message what you could not do and why.
|
|
99
99
|
</multi-agent>
|
|
100
100
|
"""
|
|
101
101
|
|
|
@@ -112,7 +112,7 @@ Usage:
|
|
|
112
112
|
multi_agent.py peek NAME [-n N]
|
|
113
113
|
multi_agent.py list [--all]
|
|
114
114
|
multi_agent.py profiles
|
|
115
|
-
multi_agent.py rm NAME
|
|
115
|
+
multi_agent.py rm (NAME | --all)
|
|
116
116
|
|
|
117
117
|
Options for run:
|
|
118
118
|
--profile P Profile name, matched case-insensitively (default general-purpose)
|
|
@@ -273,7 +273,15 @@ def sessions_dir(project):
|
|
|
273
273
|
|
|
274
274
|
|
|
275
275
|
def pi_bin():
|
|
276
|
-
return os.environ.get("
|
|
276
|
+
return os.environ.get("PI_MULTI_AGENT_PI_BIN") or "pi"
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def check_pi_bin():
|
|
280
|
+
if not shutil.which(pi_bin()):
|
|
281
|
+
raise CommandError(
|
|
282
|
+
f'cannot find the pi program "{pi_bin()}"; '
|
|
283
|
+
"set PI_MULTI_AGENT_PI_BIN to the command that starts pi"
|
|
284
|
+
)
|
|
277
285
|
|
|
278
286
|
|
|
279
287
|
def find_session_file(session_dir, name):
|
|
@@ -583,6 +591,15 @@ def job_state(job_dir):
|
|
|
583
591
|
os.close(fd)
|
|
584
592
|
|
|
585
593
|
|
|
594
|
+
def job_since(job_dir, status):
|
|
595
|
+
"""Return the time used for the job's age, or None when it is missing."""
|
|
596
|
+
started = parse_utc(read_text(job_path(job_dir, "started_at"), "") or "")
|
|
597
|
+
if status == "running":
|
|
598
|
+
return started
|
|
599
|
+
ended = parse_utc(read_text(job_path(job_dir, "ended_at"), "") or "")
|
|
600
|
+
return ended or started
|
|
601
|
+
|
|
602
|
+
|
|
586
603
|
def socket_path(job_dir):
|
|
587
604
|
"""The path of the job's control socket.
|
|
588
605
|
|
|
@@ -719,9 +736,10 @@ def child_env(job_dir, job):
|
|
|
719
736
|
env = dict(os.environ)
|
|
720
737
|
for key in REMOVED_ENV:
|
|
721
738
|
env.pop(key, None)
|
|
722
|
-
env["
|
|
723
|
-
env["
|
|
739
|
+
env["PI_MULTI_AGENT_JOB"] = job_dir
|
|
740
|
+
env["PI_MULTI_AGENT_DEPTH"] = str(job["depth"])
|
|
724
741
|
env["PI_MINIMAL_PERMISSION_SYSTEM_NONINTERACTIVE"] = "1"
|
|
742
|
+
env["PI_REQUEST_USER_INPUT_NONINTERACTIVE"] = "1"
|
|
725
743
|
env["PWD"] = job["cwd"]
|
|
726
744
|
return env
|
|
727
745
|
|
|
@@ -1309,7 +1327,7 @@ def run_runner(job_dir, lock_fd, server, caller_fd=None):
|
|
|
1309
1327
|
|
|
1310
1328
|
def caller_depth():
|
|
1311
1329
|
try:
|
|
1312
|
-
return int(os.environ.get("
|
|
1330
|
+
return int(os.environ.get("PI_MULTI_AGENT_DEPTH") or 0)
|
|
1313
1331
|
except ValueError:
|
|
1314
1332
|
return 0
|
|
1315
1333
|
|
|
@@ -1317,12 +1335,12 @@ def caller_depth():
|
|
|
1317
1335
|
def check_depth():
|
|
1318
1336
|
depth = caller_depth()
|
|
1319
1337
|
try:
|
|
1320
|
-
limit = int(os.environ.get("
|
|
1338
|
+
limit = int(os.environ.get("PI_MULTI_AGENT_MAX_DEPTH") or DEFAULT_MAX_DEPTH)
|
|
1321
1339
|
except ValueError:
|
|
1322
1340
|
limit = DEFAULT_MAX_DEPTH
|
|
1323
1341
|
if depth >= limit:
|
|
1324
1342
|
raise CommandError(
|
|
1325
|
-
f"cannot start an agent: this caller is at depth {depth} and
|
|
1343
|
+
f"cannot start an agent: this caller is at depth {depth} and PI_MULTI_AGENT_MAX_DEPTH is {limit}"
|
|
1326
1344
|
)
|
|
1327
1345
|
return depth + 1
|
|
1328
1346
|
|
|
@@ -1564,6 +1582,7 @@ def cmd_run(argv):
|
|
|
1564
1582
|
if not os.path.isfile(fork):
|
|
1565
1583
|
raise CommandError(f"--fork: PI_SESSION_FILE does not exist: {fork}")
|
|
1566
1584
|
depth = check_depth()
|
|
1585
|
+
check_pi_bin()
|
|
1567
1586
|
profile = find_profile(cwd, args.values.get("--profile") or DEFAULT_PROFILE)
|
|
1568
1587
|
|
|
1569
1588
|
root = jobs_dir(project)
|
|
@@ -1576,9 +1595,16 @@ def cmd_run(argv):
|
|
|
1576
1595
|
try:
|
|
1577
1596
|
os.mkdir(os.path.join(root, name))
|
|
1578
1597
|
except FileExistsError:
|
|
1598
|
+
job_dir = os.path.join(root, name)
|
|
1599
|
+
status = job_state(job_dir)
|
|
1600
|
+
since = job_since(job_dir, status)
|
|
1601
|
+
detail = status
|
|
1602
|
+
if since is not None:
|
|
1603
|
+
event = "started" if status == "running" else "ended"
|
|
1604
|
+
detail += f", {event} {format_age(time.time() - since)} ago"
|
|
1579
1605
|
raise CommandError(
|
|
1580
|
-
f"job {name} already exists in this project
|
|
1581
|
-
f"
|
|
1606
|
+
f"job {name} already exists in this project ({detail}); pick another --name. "
|
|
1607
|
+
f"Run `resume {name}` or `rm {name}` only for a job you started or the user asked about."
|
|
1582
1608
|
)
|
|
1583
1609
|
else:
|
|
1584
1610
|
for _attempt in range(100):
|
|
@@ -1613,7 +1639,7 @@ def cmd_run(argv):
|
|
|
1613
1639
|
"cwd": cwd,
|
|
1614
1640
|
"project": project,
|
|
1615
1641
|
"session_dir": session_dir,
|
|
1616
|
-
"parent": os.environ.get("
|
|
1642
|
+
"parent": os.environ.get("PI_MULTI_AGENT_JOB") or None,
|
|
1617
1643
|
"depth": depth,
|
|
1618
1644
|
"model": model,
|
|
1619
1645
|
"thinking": thinking,
|
|
@@ -1640,6 +1666,7 @@ def cmd_resume(argv):
|
|
|
1640
1666
|
prompt = read_message(args, "prompt")
|
|
1641
1667
|
job_dir = find_job(name)
|
|
1642
1668
|
check_depth()
|
|
1669
|
+
check_pi_bin()
|
|
1643
1670
|
# A runner holds the lock while the job runs, so taking it proves that
|
|
1644
1671
|
# nothing runs, and keeps it so until the new runner holds it.
|
|
1645
1672
|
lock_fd = job_lock(
|
|
@@ -1903,8 +1930,9 @@ def cmd_status(argv):
|
|
|
1903
1930
|
("session_file", sessions[-1] if sessions else "-"),
|
|
1904
1931
|
(
|
|
1905
1932
|
"session_cmd",
|
|
1906
|
-
"cd {} &&
|
|
1933
|
+
"cd {} && {} --session-dir {} --session-id {}".format(
|
|
1907
1934
|
shlex.quote(job.get("cwd", ".")),
|
|
1935
|
+
shlex.quote(pi_bin()),
|
|
1908
1936
|
shlex.quote(session_dir),
|
|
1909
1937
|
shlex.quote(name),
|
|
1910
1938
|
),
|
|
@@ -1938,8 +1966,7 @@ def cmd_list(argv):
|
|
|
1938
1966
|
job = read_json(job_path(job_dir, "job.json"), {})
|
|
1939
1967
|
status = job_state(job_dir)
|
|
1940
1968
|
started = parse_utc(read_text(job_path(job_dir, "started_at"), "") or "")
|
|
1941
|
-
|
|
1942
|
-
since = started if status == "running" else (ended or started)
|
|
1969
|
+
since = job_since(job_dir, status)
|
|
1943
1970
|
if (
|
|
1944
1971
|
"--all" not in args.flags
|
|
1945
1972
|
and status != "running"
|
|
@@ -1989,8 +2016,7 @@ def cmd_profiles(argv):
|
|
|
1989
2016
|
return EXIT_OK
|
|
1990
2017
|
|
|
1991
2018
|
|
|
1992
|
-
def
|
|
1993
|
-
job_dir = find_job(one_name(Args(argv, (), ()), "rm"))
|
|
2019
|
+
def remove_job(job_dir):
|
|
1994
2020
|
name = os.path.basename(job_dir)
|
|
1995
2021
|
lock_fd = job_lock(job_dir, f"job {name} is running; kill it first")
|
|
1996
2022
|
try:
|
|
@@ -2002,6 +2028,26 @@ def cmd_rm(argv):
|
|
|
2002
2028
|
finally:
|
|
2003
2029
|
os.close(lock_fd)
|
|
2004
2030
|
sys.stdout.write(f"removed: {name}\n")
|
|
2031
|
+
|
|
2032
|
+
|
|
2033
|
+
def cmd_rm(argv):
|
|
2034
|
+
args = Args(argv, (), ("--all",))
|
|
2035
|
+
if "--all" in args.flags:
|
|
2036
|
+
if args.positional:
|
|
2037
|
+
raise usage_error("rm takes a job name or --all, not both")
|
|
2038
|
+
finished = [d for d in project_jobs(logical_cwd()) if job_state(d) != "running"]
|
|
2039
|
+
if not finished:
|
|
2040
|
+
sys.stdout.write("no jobs to remove in this project\n")
|
|
2041
|
+
return EXIT_OK
|
|
2042
|
+
code = EXIT_OK
|
|
2043
|
+
for job_dir in finished:
|
|
2044
|
+
try:
|
|
2045
|
+
remove_job(job_dir)
|
|
2046
|
+
except CommandError as exc:
|
|
2047
|
+
sys.stdout.write(f"error: {exc}\n")
|
|
2048
|
+
code = EXIT_FAIL
|
|
2049
|
+
return code
|
|
2050
|
+
remove_job(find_job(one_name(args, "rm")))
|
|
2005
2051
|
return EXIT_OK
|
|
2006
2052
|
|
|
2007
2053
|
|