@fradser/pi-agent-teams 0.9.2 → 1.0.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/.memory/MEMORY.md +1 -0
- package/.memory/attempt-bound-authority.md +19 -0
- package/README.md +137 -58
- package/package.json +13 -5
- package/references/agent-roles.md +23 -13
- package/src/activity.ts +20 -11
- package/src/agent-actions.ts +61 -36
- package/src/agent-host.ts +64 -0
- package/src/capability-tools.ts +34 -0
- package/src/guidance.ts +49 -37
- package/src/index.ts +149 -30
- package/src/recipient.ts +4 -4
- package/src/state.ts +163 -607
- package/src/statefile.ts +28 -244
- package/src/team-machine.ts +827 -535
- package/src/tool-copy.ts +26 -34
- package/src/tools.ts +57 -210
- package/src/types.ts +56 -157
- package/src/ui.ts +85 -59
- package/src/worker.ts +152 -79
- package/src/agents.ts +0 -296
- package/src/leader-reports.ts +0 -92
- package/src/spawner.ts +0 -751
- package/src/work-context.ts +0 -88
- package/src/worker-tools.ts +0 -4
- package/src/worktree.ts +0 -164
package/.memory/MEMORY.md
CHANGED
|
@@ -4,3 +4,4 @@
|
|
|
4
4
|
|
|
5
5
|
- `team-centric-design.md` — Agent Teams current architecture: named resident RPC teammates, shared task board with atomic self-claim, direct P2P inboxes, verify-gated completion, harness-driven wake-ups. Supersedes the deleted run-centric DAG design.
|
|
6
6
|
- `follow-up-queue.md` — Teammate report steering handoff: hand reports immediately to Pi's steering channel without an internal serialized follow-up queue; delivers at safe tool boundaries.
|
|
7
|
+
- `attempt-bound-authority.md` — Worker communication, claims, and submissions bind to the started Assignment Attempt; failed or interrupted Work is recovery-held until an explicit leader assignment, and retired nonterminal reports leave the provider projection while staying in history.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: attempt-bound-authority
|
|
3
|
+
description: Worker communication and submissions bind to the started Assignment Attempt; failed Work is recovery-held instead of silently re-claimable
|
|
4
|
+
type: project
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
A real session exposed four coupled lifecycle defects. After a worker was superseded or stopped, its cancelled attempts kept emitting cancellation acknowledgements into a settled assignment; the `work` tool was hidden for a retained superseded holder, so it could not acknowledge cancellation at all and its resource lease stayed stranded. Turns that had started under one Assignment Attempt adopted a newly assigned `assignmentId` from the live roster, so an old turn could send or submit as replacement Work. A failed Work Item returned to the board as plainly `pending`, so idle residents were repeatedly re-delegated the same doomed work while the leader treated the failure as already handled. Nonterminal reports from retired attempts also kept acting as current leader instructions.
|
|
10
|
+
|
|
11
|
+
## How to apply
|
|
12
|
+
|
|
13
|
+
- `packages/agent-teams/src/worker.ts` binds a turn at `message_start`: `assignmentId`/`taskId` come from the delivered assignment marker, and `authorize()` rejects any call whose roster entry names a different attempt, Work Item, or incarnation. Reads match on `spawnId` as well as name so a replacement resident cannot lend authority.
|
|
14
|
+
- A bound, living, same-spawn roster entry with no assignment clears the binding for that turn: a retried attempt must not dead-end an ordinary peer/board wake.
|
|
15
|
+
- `send`, `queueWorkClaim`, and `queueWorkSubmission` all pass the same authority; a terminal outcome or closed assignment rejects further sends, claims, and submissions. A retained superseded holder keeps exactly one failed cancellation acknowledgement.
|
|
16
|
+
- `packages/agent-teams/src/state.ts` marks failed, crash-released, or stop-released Work `recoveryRequired`, which excludes it from `claimableTasks()` and wake notices; only an explicit leader `work assign` clears it. A harness start failure that never reached the worker stays ordinarily claimable.
|
|
17
|
+
- `packages/agent-teams/src/index.ts` keeps a retired attempt's nonterminal report in session history but removes it from the provider projection, because Pi cannot retract an already-queued steer.
|
|
18
|
+
- Keep BDD coverage in `features/worker-attempt-lifecycle.feature`, `features/session-history-regressions.feature`, and `features/historical-report-delivery.feature`; regressions live in `tests/test_worker_attempt_lifecycle.py`, `tests/test_session_history_regressions.py`, and `tests/test_historical_report_delivery.py`.
|
|
19
|
+
- Guidance states the contract in two sentences; `tests/test_prompt_budget.py` ceilings for `tmActiveLeader`/`tmWorker` were raised deliberately with that change.
|
package/README.md
CHANGED
|
@@ -1,82 +1,112 @@
|
|
|
1
1
|
# @fradser/pi-agent-teams
|
|
2
2
|
|
|
3
|
-
Agent Teams
|
|
3
|
+
Agent Teams combines subagent execution, the task set, and point-to-point messages.
|
|
4
|
+
The four intent tools are the only child tool surface:
|
|
4
5
|
|
|
5
|
-
```
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
```ts
|
|
7
|
+
agent_start({ identity: "reviewer", prompt: "Audit authentication" });
|
|
8
|
+
agent_input({ session: "session:reviewer:<incarnation>", prompt: "Review the correction", delivery: "next" });
|
|
9
|
+
agent_input({ session: "session:reviewer:<incarnation>", prompt: "Keep this read-only", delivery: "steer" });
|
|
10
|
+
agent_list({});
|
|
11
|
+
agent_stop({ session: "session:reviewer:<incarnation>" });
|
|
10
12
|
```
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
Next input has a bounded FIFO queue and rechecks the original host's task policy.
|
|
15
|
+
A label changes display only; an identity selects saved instructions and private data.
|
|
16
|
+
Read [the intent contract](../subagents/README.md#intent-tools) for results, cancellation, and compatibility.
|
|
17
|
+
The `/agent-teams` console remains available.
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
agent({ action: "delegate", name: "reviewer", prompt: "Audit authentication" })
|
|
16
|
-
agent({ action: "start", name: "reviewer" })
|
|
17
|
-
agent({ action: "inspect", name: "reviewer", session: "session:reviewer-...:spawn-..." })
|
|
18
|
-
agent({ action: "stop", session: "session:reviewer-...:spawn-..." })
|
|
19
|
-
```
|
|
19
|
+
## The retired `agent` tool
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`tools` or `tools: []` grants only `agent_event` and `work`, never default shell
|
|
26
|
-
or file access. Delegate/start results and inspection expose the recorded
|
|
27
|
-
`session.tools` grant, with a coordination-only warning when applicable.
|
|
28
|
-
Inspection uses the session's recorded grant, not a changed role definition;
|
|
29
|
-
legacy records without a grant leave it unknown rather than claiming no tools.
|
|
21
|
+
The `agent` tool and its six actions are gone, and no compatibility adapter
|
|
22
|
+
remains. A recorded branch that named `agent` receives the intent family on its
|
|
23
|
+
next session start. No user data moves. Read
|
|
24
|
+
[ADR-0008](../../docs/adr/0008-retire-the-legacy-agent-tool.md) for the decision.
|
|
30
25
|
|
|
31
|
-
|
|
26
|
+
An unknown role takes a compact inline definition:
|
|
32
27
|
|
|
33
28
|
```ts
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
agent_start({ label: "reader", prompt: "Read README.md and cite its install command",
|
|
30
|
+
description: "Read evidence", role_prompt: "Read the assigned file and cite evidence", tools: ["read"] })
|
|
36
31
|
```
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
33
|
+
Only a user-authored disk `agent.md` declares durable identity. Disk definitions
|
|
34
|
+
always outrank inline input; invocation tools may narrow their grant, never widen it.
|
|
35
|
+
There are no persistence, Memory opt-in, worktree or promotion parameters. Choose minimal
|
|
36
|
+
canonical tool IDs (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`,
|
|
37
|
+
`powershell`); extension aliases such as `ffgrep` are not inherited. Omitted
|
|
38
|
+
`tools` or `tools: []` grants only `message` and `task` for temporary team children,
|
|
39
|
+
never default shell or file access. Disk identities also receive `agent_memory`.
|
|
40
|
+
Start receipts and inspection expose the recorded effective grant, with a
|
|
41
|
+
coordination-only warning when applicable.
|
|
42
|
+
Inspection uses the session's recorded grant, not a changed role definition;
|
|
43
|
+
a record without a grant leaves it unknown rather than claiming no tools.
|
|
44
|
+
|
|
45
|
+
`agent_start` always takes a prompt and awaits the correlated
|
|
46
|
+
native input acknowledgement, not merely a writable pipe. It never creates an idle
|
|
47
|
+
child. Rejected, handled, timed-out or closed input never reports acceptance.
|
|
48
|
+
A receipt is not a result.
|
|
49
|
+
|
|
50
|
+
Every temporary or durable child remains **IDLE after a settled result** and accepts
|
|
51
|
+
next input in that exact incarnation. Steer guides active work; `agent_stop` and
|
|
52
|
+
parent shutdown close it. Later tasks notices can wake an idle team participant.
|
|
53
|
+
No child survives the parent Pi process. Hosted next input awaits native input acceptance
|
|
54
|
+
in the same process and history, publishing a new attempt before a later independent
|
|
55
|
+
input. Hosted steer awaits native steering acceptance without changing the
|
|
56
|
+
current sequence or attempt. An uncertain acknowledgement retains authority: inspect
|
|
57
|
+
or stop before retrying, rather than submitting competing input.
|
|
58
|
+
|
|
59
|
+
Canonical definitions are `<agent-dir>/agents/<name>/agent.md`,
|
|
60
|
+
`<project>/.pi/agents/<name>/agent.md`, and
|
|
61
|
+
`<project>/.pi/agents/<name>.local/agent.md`; precedence is project-local > project >
|
|
62
|
+
user > session. Every disk scope receives private Memory, an ordinary scratch directory
|
|
63
|
+
and project history at `<agent-dir>/agents/<name>/projects/<slug>/session/`.
|
|
64
|
+
Project overrides do not create a user-scope definition. Temporary roles use the
|
|
65
|
+
original project cwd with no-session and no private data. Durable children also launch
|
|
66
|
+
in the original project; scratch is data, not a checkout:
|
|
67
|
+
project edits change the original working tree directly. There is no Git isolation,
|
|
68
|
+
branch, automatic commit, merge or write confinement.
|
|
69
|
+
|
|
70
|
+
Identity, data provisioning and offline list entries belong to
|
|
71
|
+
[@fradser/pi-subagents](../subagents/VISION.md); team coordination does not create,
|
|
72
|
+
promote or clear that package's role registry.
|
|
73
|
+
|
|
74
|
+
`task` is activated only in valid assignment/tasks contexts and owns create/list/
|
|
75
|
+
update/complete/reopen. A Task describes what must be done, not who must do it;
|
|
76
|
+
participants take available Tasks with `update status=in_progress`.
|
|
77
|
+
|
|
78
|
+
Failed, interrupted, or crash-released Work returns to the tasks as
|
|
79
|
+
`pending/recovery-required`: the evidence stays available for inspection, and the
|
|
80
|
+
harness deliberately excludes it from autonomous notices. A participant must
|
|
81
|
+
proceed with a stated recovery reason, so a transient failure is never silently
|
|
82
|
+
retried. A retained superseded holder keeps `task` disclosed for exactly one failed
|
|
83
|
+
cancellation acknowledgement.
|
|
53
84
|
|
|
54
85
|
```ts
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
work({ action: "submit", outcome: "success", result: "Verification evidence" })
|
|
86
|
+
task({ action: "create", subject: "Fix storage", resources: ["firmware/storage"] })
|
|
87
|
+
task({ action: "update", id: "fix-storage", status: "in_progress" })
|
|
88
|
+
task({ action: "complete", id: "fix-storage", outcome: "success", result: "Verification evidence" })
|
|
59
89
|
```
|
|
60
90
|
|
|
61
|
-
`
|
|
62
|
-
reply route, and optional `inform`/`request`
|
|
91
|
+
`message` is communication-only. It accepts a body, a recipient or bound
|
|
92
|
+
reply route, and optional `inform`/`request` kind. Work lifecycle authority is
|
|
63
93
|
never inferred from message text.
|
|
64
94
|
|
|
65
95
|
```ts
|
|
66
|
-
|
|
96
|
+
message({ to: "session:reviewer:spawn-...", body: "The policy changed", kind: "inform" })
|
|
67
97
|
```
|
|
68
98
|
|
|
69
99
|
Automatic final answers (including autonomously claimed Work) and explicit Work
|
|
70
100
|
submission use the same attempt-bound acceptance pipeline. Explicit submission
|
|
71
101
|
ends the worker turn without a second automatic submission. Successful results
|
|
72
102
|
reach the Leader once, after execution settles and any verification gate passes.
|
|
73
|
-
Execution failures are reported after settlement without claiming acceptance.
|
|
74
|
-
`
|
|
103
|
+
Execution failures are reported after settlement without claiming acceptance. A refused task transition does not produce an accepted completion or failure report.
|
|
104
|
+
`task list` includes bounded result evidence for deliberate recovery.
|
|
75
105
|
Verification freezes execution and retains deferred mail as Work history.
|
|
76
106
|
An ordinary final answer is a successful candidate, not independent verification;
|
|
77
107
|
ungated acceptance does not independently verify its claims. If requested work
|
|
78
108
|
cannot be performed, including missing tools, workers must explicitly submit
|
|
79
|
-
`
|
|
109
|
+
`task({ action: "complete", outcome: "failed", result: "Blocker and unverified work" })`.
|
|
80
110
|
Blocker prose is not classified as failure. Failed submission produces one failed
|
|
81
111
|
report, not completion or dependent unlock; a rejecting verification gate also
|
|
82
112
|
prevents acceptance. Leaders should delegate concrete acceptance criteria and an
|
|
@@ -85,26 +115,43 @@ acknowledgment unless a decision or action changes.
|
|
|
85
115
|
Do not narrate repeated reports. This guidance does not suppress event transport
|
|
86
116
|
or infer lifecycle changes from communication.
|
|
87
117
|
|
|
118
|
+
A clean process exit while the resident is idle and owes no result stays in the console mailbox.
|
|
119
|
+
It records the stopped state and confirmed stop time without an unexpected-stop notification or another leader turn.
|
|
120
|
+
Unread mail addressed to the current incarnation prevents passive closure, even before inbox routing.
|
|
121
|
+
Malformed or incomplete unread records also prevent passive closure. Valid JSON alone does not prove that a message is valid.
|
|
122
|
+
The close check changes no inbox bytes, offsets, or delivery records. Stale or already-delivered messages do not create a new alarm.
|
|
123
|
+
Process failures, interrupted assignments, and unresolved results still reach the leader. Accepted results remain available after closure.
|
|
124
|
+
|
|
88
125
|
After delegation, continue independent work or end the turn: results resume the
|
|
89
|
-
session automatically.
|
|
90
|
-
|
|
126
|
+
session automatically. `agent_list` is a status summary, not a way to wait, and a
|
|
127
|
+
child's complete answer arrives with its Session Result rather than on request.
|
|
128
|
+
polling or sleep loops. `message` never creates or retries Work; messaging a
|
|
91
129
|
completed assignment returns its recorded result without waking the worker.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
Worker communication and submissions are bound to the Assignment Attempt that
|
|
131
|
+
started the turn, so an old turn cannot report or submit as replacement Work, and
|
|
132
|
+
nonterminal coordination from a retired attempt stays in session history as
|
|
133
|
+
evidence without acting as a current instruction.
|
|
134
|
+
Recovery reuses the existing Task ID. A message does not release authority: wait
|
|
135
|
+
for the attempt to end or stop its stale exact session before taking the Task with
|
|
136
|
+
a stated recovery reason. Do not create competing recovery work through another startup. Read-only/no-write messages are steering, not cancellation or
|
|
96
137
|
proof that the current tool batch stopped. Native steering takes effect at safe
|
|
97
138
|
tool boundaries, before peer follow-up; it cannot undo already executed writes.
|
|
98
|
-
Parked verification requires explicit
|
|
139
|
+
Parked verification requires explicit Task recovery; ordinary
|
|
99
140
|
messages remain deferred. Exact session handles are incarnation-bound, so a stale
|
|
100
141
|
handle cannot stop or inspect a replacement resident. Work persists for later inspection,
|
|
101
142
|
but a different session file does not import another session's tasks automatically.
|
|
102
143
|
|
|
103
144
|
## Coordination and delivery
|
|
104
145
|
|
|
146
|
+
Leader and worker guidance use the owned `agent_teams_leader` and `agent_teams_worker` prompt sections at run start.
|
|
147
|
+
These sections remain present with either Pi's default preamble or an authored `customPrompt`.
|
|
148
|
+
They do not replace the complete system prompt or add context through custom messages.
|
|
149
|
+
Custom-message starts have no user prompt; worker disclosure checks the current assignment without assuming a task notice.
|
|
150
|
+
Historical report filtering and report delivery remain separate from these prompt contributions.
|
|
151
|
+
|
|
105
152
|
Leader guidance uses the same delivery contract before and after a team starts:
|
|
106
153
|
|
|
107
|
-
- Delegate separable work with a task-scoped baseline, acceptance criteria and safe local checks. Use `
|
|
154
|
+
- Delegate separable work with a task-scoped baseline, acceptance criteria and safe local checks. Use `task create` with `dependsOn` for prerequisites and shared `resources` tags for conflicting writes; participants take eligible Tasks. Independent delegation is not a substitute for dependencies.
|
|
108
155
|
- Implementers own assigned local checks. One integration owner checks shared behavior against the integrated candidate. Determine isolation needs before verification; commands running locally may still access user data or production credentials.
|
|
109
156
|
- Record a revision or scoped snapshot for the candidate and exclude unrelated dirty changes. Review and integration checks judge that same candidate; later edits invalidate affected evidence rather than automatically requiring every Agent to rerun every suite.
|
|
110
157
|
- A completed review Work Item can report REWORK. It is not a PASS on the implementation. Review-only work ends with that report, without editing the implementation or waiting for repairs; the implementation owner owns remediation. Implementation delivery stays pending until required Work and blocking reviews have returned, findings are resolved, and applicable evidence matches the final candidate. If only results remain outstanding, yield without announcing completion.
|
|
@@ -114,6 +161,38 @@ Leader guidance uses the same delivery contract before and after a team starts:
|
|
|
114
161
|
|
|
115
162
|
Read [the role reference](references/agent-roles.md#assignment-brief) when preparing an assignment brief.
|
|
116
163
|
|
|
164
|
+
## Child environment
|
|
165
|
+
|
|
166
|
+
A spawned Agent receives an explicit non-secret environment, never the leader's
|
|
167
|
+
complete one. The allowlist covers runtime and locale, editor and pager, Git and
|
|
168
|
+
`SSH_AUTH_SOCK`, proxy and trust anchors, Node and language toolchain locations,
|
|
169
|
+
and the Pi process markers and configuration. `PI_CODING_AGENT_DIR` passes
|
|
170
|
+
through so the child resolves the same `auth.json` and `models.json` as the
|
|
171
|
+
leader; that is where Pi keeps OAuth credentials and literal `apiKey` values, so
|
|
172
|
+
a child needs the config directory rather than the parent's credential set.
|
|
173
|
+
|
|
174
|
+
Add an exact name to `PI_TEAMMATE_ENV_ALLOW` (comma-separated) when a provider
|
|
175
|
+
resolves credentials through environment interpolation:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
export PI_TEAMMATE_ENV_ALLOW="MY_PROVIDER_API_KEY,AWS_PROFILE"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Opting in a credential-shaped name works and is reported, so a widening stays
|
|
182
|
+
visible. The allowlist variable itself does not travel to the child, so a nested
|
|
183
|
+
spawn reaches its own strict default. Spawn bindings the harness supplies always
|
|
184
|
+
win over both the allowlist and the leader environment.
|
|
185
|
+
|
|
186
|
+
The withheld count, the credential-shaped names, and the recovery instruction
|
|
187
|
+
appear in the `/agent-teams` detail view. That stays passive console telemetry
|
|
188
|
+
and never wakes the leader. Names embedding a private URL — npm's
|
|
189
|
+
`npm_config_//registry.example.com/:_authToken` shape — are redacted to a
|
|
190
|
+
recognizable prefix, and no withheld value is ever rendered.
|
|
191
|
+
|
|
192
|
+
This is leakage defense, not containment. A child granted `bash` can still read
|
|
193
|
+
`~/.pi/agent/auth.json`, `~/.aws/credentials`, or a project `.env` from disk;
|
|
194
|
+
kernel write and read confinement is a separate layer that does not exist yet.
|
|
195
|
+
|
|
117
196
|
## Message display
|
|
118
197
|
|
|
119
198
|
Collapsed `[message]` rows use the current terminal width for their preview and
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fradser/pi-agent-teams",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Agent-to-Agent communication for Pi — point-to-point messaging, notices, Agent Presence, and the Team Console. A bundle: it registers `message` and loads the pi-subagents and pi-tasks extensions, so installing it gives the subagent intent tools plus `task` and `message` with one registrant per tool.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"teammates",
|
|
@@ -35,10 +35,18 @@
|
|
|
35
35
|
},
|
|
36
36
|
"pi": {
|
|
37
37
|
"extensions": [
|
|
38
|
-
"./index.ts"
|
|
38
|
+
"./index.ts",
|
|
39
|
+
"./node_modules/@fradser/pi-subagents/index.ts",
|
|
40
|
+
"./node_modules/@fradser/pi-tasks/index.ts"
|
|
39
41
|
]
|
|
40
42
|
},
|
|
41
43
|
"dependencies": {
|
|
42
|
-
"@fradser/pi-kit": "0.
|
|
44
|
+
"@fradser/pi-kit": "0.6.0",
|
|
45
|
+
"@fradser/pi-subagents": "0.2.0",
|
|
46
|
+
"@fradser/pi-tasks": "0.2.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@earendil-works/pi-coding-agent": "1.5.0",
|
|
50
|
+
"@earendil-works/pi-tui": "1.5.0"
|
|
43
51
|
}
|
|
44
|
-
}
|
|
52
|
+
}
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
Abstract reference for generating new agent definitions on demand. This file
|
|
4
4
|
is NOT discoverable and CANNOT be spawned directly. Derive a concrete
|
|
5
5
|
definition from the anatomy, archetype axes, and invariants below, tailor it
|
|
6
|
-
to the task, and
|
|
7
|
-
|
|
6
|
+
to the task, and pass it as an inline session role. Only a user-authored disk
|
|
7
|
+
`agent.md` declares durable identity: `<agent-dir>/agents/<name>/agent.md`,
|
|
8
|
+
`<cwd>/.pi/agents/<name>/agent.md`, or `<cwd>/.pi/agents/<name>.local/agent.md`.
|
|
9
|
+
Project-local overrides project, which overrides user, which overrides session.
|
|
10
|
+
Creating a disk definition requires explicit user authorization, not a tool flag.
|
|
8
11
|
|
|
9
12
|
## Definition anatomy
|
|
10
13
|
|
|
@@ -15,12 +18,19 @@ Frontmatter:
|
|
|
15
18
|
role, phrased as "use when ...". This is the most load-bearing field.
|
|
16
19
|
- `tools` — the explicit minimal set; follows the mutability axis (see below).
|
|
17
20
|
Canonical built-ins are `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`,
|
|
18
|
-
`powershell` (
|
|
19
|
-
Omitted tools or `[]` grant only coordination tools (`
|
|
21
|
+
`powershell` (declared by `@fradser/pi-subagents`).
|
|
22
|
+
Omitted tools or `[]` grant only coordination tools (`message`, `task`),
|
|
20
23
|
not file or shell access. No aliases or implicit default `bash`.
|
|
21
|
-
- optional `model`, `verify
|
|
22
|
-
reviewer prompt, not a shell command. State
|
|
23
|
-
|
|
24
|
+
- optional `model`, `verify`. `verify` is an independent completion-gate
|
|
25
|
+
reviewer prompt, not a shell command. State evidence and acceptance criteria; a concrete
|
|
26
|
+
local gate is not a second broad project review.
|
|
27
|
+
|
|
28
|
+
Every disk scope has private Memory, an ordinary scratch workspace and project-scoped
|
|
29
|
+
Pi history under the configured agent directory. Inline session roles have none and
|
|
30
|
+
run in the original project cwd with no-session. Scratch is not a project checkout:
|
|
31
|
+
project edits change the original working tree directly, with no write confinement,
|
|
32
|
+
Git isolation or automatic commit. Never invent persistence, Memory or workspace flags.
|
|
33
|
+
Results leave both kinds IDLE for `send`; only `stop` or parent shutdown closes them.
|
|
24
34
|
|
|
25
35
|
The Markdown body is the role prompt, built from five parts:
|
|
26
36
|
|
|
@@ -30,8 +40,8 @@ The Markdown body is the role prompt, built from five parts:
|
|
|
30
40
|
3. Boundaries — what must never be touched or changed.
|
|
31
41
|
4. Evidence — what every claim or finding must carry.
|
|
32
42
|
5. Final result — an ordinary final answer is submitted automatically for the
|
|
33
|
-
current
|
|
34
|
-
`
|
|
43
|
+
current Task after execution settles. A Worker may explicitly use
|
|
44
|
+
`task({ action: "complete", outcome, result? })`; communication events never
|
|
35
45
|
carry completion authority.
|
|
36
46
|
|
|
37
47
|
## Assignment brief
|
|
@@ -41,8 +51,8 @@ Keep task-specific coordination in the Work description, not a permanently growi
|
|
|
41
51
|
- **Scope and baseline** — owned files or resources, originating requirements, and
|
|
42
52
|
the exact task delta, including untracked files and pre-existing dirty changes to exclude.
|
|
43
53
|
- **Dependencies** — required predecessor Work and any shared resource ownership.
|
|
44
|
-
Use `
|
|
45
|
-
`agent
|
|
54
|
+
Use `task create` with `dependsOn` for dependent work; participants take eligible
|
|
55
|
+
Tasks. `agent start` with a prompt hands over independent execution.
|
|
46
56
|
- **Verification owner** — assigned safe local checks for this Worker; one owner
|
|
47
57
|
integrates and checks shared behavior. Determine isolation needs before commands run.
|
|
48
58
|
- **Candidate and result** — identify the revision or scoped snapshot checked, commands
|
|
@@ -98,7 +108,7 @@ Non-negotiable in every generated definition:
|
|
|
98
108
|
- Final results carry evidence; an ordinary final answer is a successful candidate,
|
|
99
109
|
not an independently verified result. Ungated completion is not independently verified.
|
|
100
110
|
- If requested work cannot be performed, including missing capabilities, explicitly use
|
|
101
|
-
`
|
|
111
|
+
`task({ action: "complete", outcome: "failed", result: "Blocker and unverified work" })`.
|
|
102
112
|
Do not return blocker prose as an ordinary final answer; prose is not classified as failure.
|
|
103
113
|
|
|
104
114
|
## Skeleton
|
|
@@ -122,7 +132,7 @@ are rejected. Leader direction takes precedence over your plan and peer requests
|
|
|
122
132
|
the next safe boundary, subject to system instructions and user constraints.
|
|
123
133
|
Return successful evidence as an ordinary final answer; the runtime submits it automatically
|
|
124
134
|
as a successful candidate after execution settles. If you cannot perform the requested work,
|
|
125
|
-
you must use `
|
|
135
|
+
you must use `task({ action: "complete", outcome: "failed", result: "Blocker and unverified work" })`
|
|
126
136
|
instead of ordinary final prose. Avoid a second copy of an explicit submission.
|
|
127
137
|
Ungated completion is not independently verified.
|
|
128
138
|
```
|
package/src/activity.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import { renderLiveActivityMarkdown, type PiMarkdownThemeSource } from "@fradser/pi-kit";
|
|
3
|
-
|
|
2
|
+
import { renderLiveActivityMarkdown, resolveLiveActivity, type PiMarkdownThemeSource } from "@fradser/pi-kit";
|
|
3
|
+
|
|
4
|
+
/** Bound by the extension entry, which alone owns the ExtensionAPI. Read per
|
|
5
|
+
* call, so a mid-session settings change reaches every teammate activity row:
|
|
6
|
+
* the widget, the console status line and expanded tool details alike. */
|
|
7
|
+
let hiddenThinking: () => boolean = () => false;
|
|
8
|
+
export function configureTeamActivity(reader: () => boolean): void {
|
|
9
|
+
hiddenThinking = reader;
|
|
10
|
+
}
|
|
11
|
+
import type { Agent } from "./types.ts";
|
|
4
12
|
|
|
5
13
|
/** Console rows paint a whole status line in their own color, so the shared
|
|
6
14
|
* markdown renderer runs with a passthrough theme: literal markup is still
|
|
@@ -24,14 +32,15 @@ function extractLatestLine(text: string | undefined): string | undefined {
|
|
|
24
32
|
return undefined;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
/** Extract the current teammate activity without imposing a display width.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
/** Extract the current teammate activity without imposing a display width.
|
|
36
|
+
* `hideThinking` comes from Pi's setting, so the shared pi-kit policy replaces
|
|
37
|
+
* reasoning with Pi's label on both the widget and the console row. */
|
|
38
|
+
export function runningTeammateActivity(teammate: Agent, hideThinking: boolean = hiddenThinking()): string {
|
|
39
|
+
return resolveLiveActivity({
|
|
40
|
+
tool: teammate.activeTool?.replace(/\s+/g, " ").trim(),
|
|
41
|
+
thinking: extractLatestLine(teammate.liveThinking),
|
|
42
|
+
text: extractLatestLine(teammate.liveText),
|
|
43
|
+
}, hideThinking) ?? "Working...";
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
export interface TeammateRowWidths {
|
|
@@ -54,7 +63,7 @@ export function teammateRowWidths(spinner: string, agent: string, width: number)
|
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
/** Fit a console status label to the requested activity width. */
|
|
57
|
-
export function
|
|
66
|
+
export function formatAgentLabel(
|
|
58
67
|
spinner: string,
|
|
59
68
|
activity: string,
|
|
60
69
|
maxActivityWidth?: number,
|
package/src/agent-actions.ts
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { WORKER_BUILTIN_TOOLS } from "./worker-tools.ts";
|
|
1
|
+
import { AGENT_BUILTIN_TOOLS } from "@fradser/pi-subagents";
|
|
3
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
5
|
-
import { resolveAgent } from "
|
|
6
|
-
import {
|
|
7
|
-
import { normalizeCoordinationParams, requireParsedParams, type
|
|
8
|
-
import { snapshotWorkContext } from "
|
|
3
|
+
import { isValidAgentName, listAgents } from "./state.ts";
|
|
4
|
+
import { resolveAgent } from "@fradser/pi-subagents";
|
|
5
|
+
import { shutdownAgentExact, spawnAgent } from "./team-machine.ts";
|
|
6
|
+
import { normalizeCoordinationParams, requireParsedParams, type Agent } from "./types.ts";
|
|
7
|
+
import { snapshotWorkContext } from "@fradser/pi-subagents";
|
|
9
8
|
import { exactSessionRoute, resolveExactSession } from "./recipient.ts";
|
|
10
9
|
|
|
11
10
|
export interface AgentActionRuntime {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
spawnAgent: typeof spawnAgent;
|
|
12
|
+
shutdownAgentExact: typeof shutdownAgentExact;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
type Definition = Parameters<typeof
|
|
15
|
+
type Definition = Parameters<typeof spawnAgent>[0]["definition"];
|
|
17
16
|
|
|
18
|
-
function session(teammate:
|
|
17
|
+
function session(teammate: Agent) {
|
|
19
18
|
const tools = teammate.tools;
|
|
20
|
-
const coordinationOnly = tools !== undefined && tools.every((tool) => tool === "
|
|
19
|
+
const coordinationOnly = tools !== undefined && tools.every((tool) => tool === "message" || tool === "task");
|
|
21
20
|
return {
|
|
22
21
|
tools,
|
|
23
22
|
...(coordinationOnly ? { warning: "coordination-only: no file or shell tools granted. Delegate execution work with explicit canonical tools; no bash is granted by default." } : {}),
|
|
@@ -29,17 +28,15 @@ function session(teammate: Teammate) {
|
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
function requireName(name: string): void {
|
|
32
|
-
if (!
|
|
31
|
+
if (!isValidAgentName(name)) throw new Error(`Invalid Agent name "${name}".`);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
export function runAgentAction(
|
|
36
35
|
params: {
|
|
37
|
-
action: "
|
|
36
|
+
action: "start" | "inspect" | "stop";
|
|
38
37
|
name?: string;
|
|
39
38
|
prompt?: string;
|
|
40
39
|
definition?: Definition;
|
|
41
|
-
resources?: string[];
|
|
42
|
-
verify?: string;
|
|
43
40
|
model?: string;
|
|
44
41
|
fork?: boolean;
|
|
45
42
|
session?: string;
|
|
@@ -55,50 +52,78 @@ export function runAgentAction(
|
|
|
55
52
|
if (!params.name) throw new Error("Agent inspect requires a name.");
|
|
56
53
|
requireName(params.name);
|
|
57
54
|
if (!params.session) throw new Error("Agent inspect requires an exact session handle.");
|
|
58
|
-
const matched = resolveExactSession(params.session,
|
|
59
|
-
if (!matched || matched.
|
|
55
|
+
const matched = resolveExactSession(params.session, listAgents());
|
|
56
|
+
if (!matched || matched.name !== params.name) throw new Error(`No current session "${params.session}" belongs to @${params.name}.`);
|
|
60
57
|
return { action: "inspect", outcome: "inspected", agent: params.name, sessions: [session(matched)] };
|
|
61
58
|
}
|
|
62
59
|
if (params.action === "stop") {
|
|
63
60
|
if (!params.session) throw new Error("Agent stop requires an exact session handle.");
|
|
64
|
-
const matched = resolveExactSession(params.session,
|
|
61
|
+
const matched = resolveExactSession(params.session, listAgents());
|
|
65
62
|
if (!matched) throw new Error(`No living session named "${params.session}".`);
|
|
66
|
-
return runtime.
|
|
63
|
+
return runtime.shutdownAgentExact(matched.name, matched.spawnId).then((result) => {
|
|
67
64
|
if (!result.ok) throw new Error(result.error);
|
|
68
|
-
|
|
65
|
+
// The name comes from the resolved session, not from the request: a stop
|
|
66
|
+
// carries only a handle, so a receipt keyed on the request would be unnamed
|
|
67
|
+
// and a row drawn from it would have nothing to show.
|
|
68
|
+
return { action: "stop", outcome: "stopped", agent: matched.name, session: params.session, body: result.body };
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
-
if (!params.name) throw new Error(
|
|
71
|
+
if (!params.name) throw new Error("Agent start requires a name.");
|
|
72
72
|
requireName(params.name);
|
|
73
|
-
if (
|
|
74
|
-
|
|
73
|
+
if (params.definition !== undefined) {
|
|
74
|
+
const role = params.definition as { description?: string; prompt?: string };
|
|
75
|
+
const hasDescription = typeof role.description === "string" && role.description.trim().length > 0;
|
|
76
|
+
const hasPrompt = typeof role.prompt === "string" && role.prompt.trim().length > 0;
|
|
77
|
+
// Both halves or neither. Half a role produces a child that reports a result
|
|
78
|
+
// nobody asked for, because it has no instructions and nothing told it to
|
|
79
|
+
// wait.
|
|
80
|
+
if (hasDescription !== hasPrompt) {
|
|
81
|
+
throw new Error("An inline role needs both description and role_prompt, or neither.");
|
|
82
|
+
}
|
|
75
83
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
// A prompt needs a role; an idle resident does not. A prompted child with no
|
|
85
|
+
// role would run without instructions and report a result nobody asked for,
|
|
86
|
+
// which is worse than refusing. A child started with no prompt is waiting for
|
|
87
|
+
// work, and requiring a role up front would make the normal two spawn shapes —
|
|
88
|
+
// do this one thing, or sit ready — behave differently from the tool.
|
|
89
|
+
if (params.prompt && !params.definition && !resolveAgent(params.name, _cwd)) {
|
|
90
|
+
throw new Error(`A prompt needs a role. @${params.name} has no definition: pass description and role_prompt, or use a name that already has one. Choose only needed canonical tools: ${AGENT_BUILTIN_TOOLS.join(", ")}. Omitted tools or [] grant coordination-only access.`);
|
|
91
|
+
}
|
|
92
|
+
// One spawn verb with an optional prompt. `delegate` and `start` were the same
|
|
93
|
+
// operation dispatched twice, and the only difference was whether a prompt was
|
|
94
|
+
// delivered; two names meant a caller had to pick between two identically
|
|
95
|
+
// shaped paths and the wrong pick was invisible.
|
|
96
|
+
const prompt = params.prompt?.trim();
|
|
97
|
+
if (params.prompt !== undefined && !prompt) throw new Error("An empty prompt is not a task. Omit it to leave the agent idle.");
|
|
79
98
|
const context = params.fork ? snapshotWorkContext(sessionManager) : undefined;
|
|
80
|
-
const result = runtime.
|
|
99
|
+
const result = runtime.spawnAgent({
|
|
81
100
|
name: params.name,
|
|
82
|
-
|
|
83
|
-
|
|
101
|
+
role: params.name,
|
|
102
|
+
// A prompt is delivered, not recorded: spawning creates no task, so there is
|
|
103
|
+
// no work id, no resource lease, and no verify gate to attach. Anything that
|
|
104
|
+
// needs a record is a task the caller creates and the participant takes.
|
|
105
|
+
...(prompt ? { prompt } : {}),
|
|
84
106
|
...(params.model ? { model: params.model } : {}),
|
|
85
107
|
...(params.definition ? { definition: params.definition } : {}),
|
|
108
|
+
...(context ? { context } : {}),
|
|
86
109
|
});
|
|
87
110
|
if (!result.ok) throw new Error(result.error);
|
|
88
|
-
const receipt = (teammate:
|
|
89
|
-
action:
|
|
111
|
+
const receipt = (teammate: Agent) => ({
|
|
112
|
+
action: "start",
|
|
90
113
|
outcome: "started",
|
|
91
114
|
agent: params.name,
|
|
115
|
+
prompted: Boolean(prompt),
|
|
92
116
|
session: session(teammate),
|
|
93
|
-
...(params.action === "delegate" ? { work: { id: teammate.workId, state: "claimed" }, assignment: { id: teammate.assignment?.id } } : {}),
|
|
94
117
|
});
|
|
95
|
-
|
|
118
|
+
// The machine offers one bounded startup receipt: native readiness without
|
|
119
|
+
// input, or correlated accepted input with a kickoff. Neither means completed.
|
|
120
|
+
if (result.readiness) {
|
|
96
121
|
return result.readiness.then((error) => {
|
|
97
122
|
if (error) throw new Error(error);
|
|
98
|
-
const current = resolveExactSession(exactSessionRoute(result.
|
|
123
|
+
const current = resolveExactSession(exactSessionRoute(result.agent.name, result.agent.spawnId), listAgents());
|
|
99
124
|
if (!current || current.status === "stopped") throw new Error("Resident exited or was replaced before startup readiness completed.");
|
|
100
125
|
return receipt(current);
|
|
101
126
|
});
|
|
102
127
|
}
|
|
103
|
-
return receipt(result.
|
|
128
|
+
return receipt(result.agent);
|
|
104
129
|
}
|