@tshu-w/pi-agents 0.2.0 → 0.2.2

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/docs/SPEC.md CHANGED
@@ -1,26 +1,18 @@
1
1
  # Pi Agents Specification
2
2
 
3
- Pi Agents lets Agents in Pi discover, message, and wait for each other.
4
- Programs built on these operations are specified in PROGRAM.md.
3
+ Pi Agents lets Agents in Pi discover, message, and wait for each other. Programs built on these operations are specified in PROGRAM.md.
5
4
 
6
- Terms follow Pi's
7
- [Pi Durable specification](https://github.com/earendil-works/pi/blob/main/packages/durable/docs/spec.md).
5
+ Terms follow Pi's [Pi Durable specification](https://github.com/earendil-works/pi/blob/main/packages/durable/docs/spec.md).
8
6
 
9
7
  ## 1. Semantic model
10
8
 
11
- An **Agent** is an addressable conversation with a stable identity. The Agent
12
- that spawns another Agent is its **owner**. A root Agent has no owner and is
13
- usually opened by a user. Ownership forms trees.
9
+ An **Agent** is an addressable conversation with a stable identity. The Agent that spawns another Agent is its **owner**. A root Agent has no owner and is usually opened by a user. Ownership forms trees.
14
10
 
15
- An Agent is shown by its name and **short ID**: the shortest prefix of its
16
- ID, at least 8 characters, that no other Agent visible to the reader has.
17
- An address is an ID, a name, or a unique ID prefix, tried in that order.
11
+ An Agent is shown by its name and **short ID**: the shortest prefix of its ID, at least 8 characters, that no other Agent visible to the reader has. An address is an ID, a name, or a unique ID prefix, tried in that order.
18
12
 
19
- A **message** carries text from one Agent to another. An **input** asks the
20
- recipient to act. A **write** only adds to the recipient's conversation.
13
+ A **message** carries text from one Agent to another. An **input** asks the recipient to act. A **write** only adds to the recipient's conversation.
21
14
 
22
- A **turn** is one continuous run of an Agent's model and tools. Its final
23
- assistant message is the **answer** to the inputs it handled.
15
+ A **turn** is one continuous run of an Agent's model and tools. Its final assistant message is the **answer** to the inputs it handled.
24
16
 
25
17
  An owner can **wait** for answers to the inputs it sends to its owned Agents.
26
18
 
@@ -30,100 +22,61 @@ An owner can **wait** for answers to the inputs it sends to its owned Agents.
30
22
 
31
23
  ### 2.1 spawn
32
24
 
33
- `spawn` creates a named, owned Agent and sends it a first input. It
34
- accepts an optional working directory, model, thinking level, and forked
35
- context, and returns the Agent's ID. Names are unique among siblings.
25
+ `spawn` creates a named, owned Agent and sends it a first input. It accepts an optional working directory, model, thinking level, and forked context, and returns the Agent's ID. Names are unique among siblings.
36
26
 
37
27
  ### 2.2 send
38
28
 
39
- `send` sends a message to a visible Agent by address. An ambiguous or
40
- unknown address is rejected.
29
+ `send` sends a message to a visible Agent by address. An ambiguous or unknown address is rejected.
41
30
 
42
- An input to an Agent with a turn in progress is either answered by the current turn
43
- (`steer`) or by a new turn after it (`followUp`, the default).
31
+ An input to an Agent with a turn in progress is either answered by the current turn (`steer`) or by a new turn after it (`followUp`, the default).
44
32
 
45
- A write is added at the recipient's next tool boundary, or at once if it is
46
- idle. A write can go to several Agents at once.
33
+ A write is added at the recipient's next tool boundary, or at once if it is idle. A write can go to several Agents at once.
47
34
 
48
- When the user of a root Agent clears its queued messages, messages from other
49
- Agents stay queued. After an Agent's turn is stopped, messages from other
50
- Agents, notifications included, become writes until its next input from its
51
- user or owner.
35
+ When the user of a root Agent clears its queued messages, messages from other Agents stay queued. After an Agent's turn is stopped, messages from other Agents, notifications included, become writes until its next input from its user or owner.
52
36
 
53
37
  ### 2.3 wait
54
38
 
55
- `wait` returns results of inputs sent to owned Agents. It can target
56
- specific Agents and take a timeout.
39
+ `wait` returns results of inputs sent to owned Agents. It can target specific Agents and take a timeout.
57
40
 
58
41
  ### 2.4 list
59
42
 
60
- `list` returns visible Agents with their ID, name, working directory,
61
- state, and latest request: the last user message or input it received.
43
+ `list` returns visible Agents with their ID, name, working directory, state, and latest request: the last user message or input it received.
62
44
 
63
45
  ### 2.5 abort
64
46
 
65
- `abort` stops an owned Agent's current turn and withdraws its queued
66
- inputs. The Agent stays available.
47
+ `abort` stops an owned Agent's current turn and withdraws its queued inputs. The Agent stays available.
67
48
 
68
49
  ## 3. Lifecycle
69
50
 
70
- An Agent is `running` while a turn is in progress, `waiting` while its turn
71
- waits in `wait` (§4), `queued` while its next turn waits for a slot (§4),
72
- and `idle` otherwise. An Agent goes offline when its runtime unloads it; it
73
- stays `idle`.
51
+ An Agent is `running` while a turn is in progress, `waiting` while its turn waits in `wait` (§4), `queued` while its next turn waits for a slot (§4), and `idle` otherwise. An Agent goes offline when its runtime unloads it; it stays `idle`.
74
52
 
75
- `send` returns once the recipient's runtime accepts the message, waking it
76
- if needed. Acceptance does not mean the recipient has handled it; a message
77
- not yet handled is lost if the recipient exits.
53
+ `send` returns once the recipient's runtime accepts the message, waking it if needed. Acceptance does not mean the recipient has handled it; a message not yet handled is lost if the recipient exits.
78
54
 
79
55
  An input to an offline Agent wakes it; a write to one is rejected.
80
56
 
81
- Owned Agents run independently of their owner's turns. When their owner
82
- goes offline, they go offline too, from the bottom of the tree up, and their
83
- unfinished inputs end as `aborted`. They return as `idle` when their owner
84
- is loaded again.
57
+ Owned Agents run independently of their owner's turns. When their owner goes offline, they go offline too, from the bottom of the tree up, and their unfinished inputs end as `aborted`. They return as `idle` when their owner is loaded again.
85
58
 
86
- On abort or when going offline, work that has not stopped within a timeout
87
- is abandoned: it no longer counts as live work and may keep running until
88
- the process exits; the timeout is reported.
59
+ On abort or when going offline, work that has not stopped within a timeout is abandoned: it no longer counts as live work and may keep running until the process exits; the timeout is reported.
89
60
 
90
- A root Agent is `running` while it has a turn in progress or pending messages,
91
- or while its tree has live work (§7 Task panel); otherwise it is `idle`.
61
+ A root Agent is `running` while it has a turn in progress or pending messages, or while its tree has live work (§7 Task panel); otherwise it is `idle`.
92
62
 
93
63
  ## 4. Visibility and limits
94
64
 
95
- An Agent sees every Agent in its own tree. A root Agent also sees other root
96
- Agents.
65
+ An Agent sees every Agent in its own tree. A root Agent also sees other root Agents.
97
66
 
98
67
  Limits apply to owned Agents in each tree.
99
68
 
100
- An owned Agent needs a slot to run a turn. `maxConcurrent` (default 3) is
101
- the number of slots. An input that would start a turn waits for a free
102
- slot, and `spawn` or `send` reports that it is queued. A `steer` input to
103
- an Agent with a turn in progress joins it and needs no new slot. An Agent gives
104
- up its slot while it waits in `wait`, and takes one again before
105
- continuing, ahead of inputs that have not started.
69
+ An owned Agent needs a slot to run a turn. `maxConcurrent` (default 3) is the number of slots. An input that would start a turn waits for a free slot, and `spawn` or `send` reports that it is queued. A `steer` input to an Agent with a turn in progress joins it and needs no new slot. An Agent gives up its slot while it waits in `wait`, and takes one again before continuing, ahead of inputs that have not started.
106
70
 
107
- An input is outstanding from acceptance until its turn ends or it is
108
- withdrawn. `maxOutstanding` (default 8) limits outstanding inputs; an input
109
- beyond it is rejected. Notifications (§5) do not count.
71
+ An input is outstanding from acceptance until its turn ends or it is withdrawn. `maxOutstanding` (default 8) limits outstanding inputs; an input beyond it is rejected. Notifications (§5) do not count.
110
72
 
111
73
  ## 5. Results and notifications
112
74
 
113
- An input sent to an owned Agent ends as `completed` when its turn answers
114
- it, `failed` when its turn fails, or `aborted` when it is aborted or
115
- withdrawn. Its result is the answer or the error. Only inputs from the
116
- owner have results; the recipient replies to other senders with `send`.
75
+ An input sent to an owned Agent ends as `completed` when its turn answers it, `failed` when its turn fails, or `aborted` when it is aborted or withdrawn. Its result is the answer or the error. Only inputs from the owner have results; the recipient replies to other senders with `send`.
117
76
 
118
- `wait` returns unread results in order and marks them read. On timeout, it
119
- returns the results so far and names the Agents still pending. A steer input
120
- to the waiting Agent, from any sender, ends `wait` the same way; a write does
121
- not. It can also return read results again.
77
+ `wait` returns unread results in order and marks them read. On timeout, it returns the results so far and names the Agents still pending. A steer input to the waiting Agent, from any sender, ends `wait` the same way; a write does not. It can also return read results again.
122
78
 
123
- When an input ends and its owner is not waiting for that Agent, the owner
124
- receives a notification, unless the owner aborted it. The notification is an input delivered as `steer` (a write after the owner's turn is stopped, §2.2),
125
- and it names the Agent and how the input ended. It does not include the
126
- result; the owner reads the result with `wait`.
79
+ When an input ends and its owner is not waiting for that Agent, the owner receives a notification, unless the owner aborted it. The notification is an input delivered as `steer` (a write after the owner's turn is stopped, §2.2), and it names the Agent and how the input ended. It does not include the result; the owner reads the result with `wait`.
127
80
 
128
81
  A recipient sees each message with its sender's name and ID.
129
82
 
@@ -133,17 +86,7 @@ A recipient sees each message with its sender's name and ID.
133
86
 
134
87
  description:
135
88
 
136
- Create, message, and coordinate Agents. `spawn` creates an owned Agent and
137
- sends its first input; `send` sends an input or a write to a visible Agent;
138
- `wait` waits for owned Agents and returns unread results; `list` finds
139
- visible Agents; `abort` stops an owned Agent's current turn and queued
140
- inputs while keeping it available. When an owned Agent finishes an input
141
- while its owner is not waiting for it, the owner receives a notification
142
- naming the Agent and outcome; `wait` returns the result. Owned Agents under
143
- the same root share `${maxConcurrent}` execution slots and a limit of
144
- `${maxOutstanding}` inputs that have not ended. Inputs that start a new
145
- turn queue when all slots are busy; new inputs are rejected at the input
146
- limit.
89
+ Create, message, and coordinate Agents. `spawn` creates an owned Agent and sends its first input; `send` sends an input or a write to a visible Agent; `wait` waits for owned Agents and returns unread results; `list` finds visible Agents; `abort` stops an owned Agent's current turn and queued inputs while keeping it available. When an owned Agent finishes an input while its owner is not waiting for it, the owner receives a notification naming the Agent and outcome; `wait` returns the result. Owned Agents under the same root share `${maxConcurrent}` execution slots and a limit of `${maxOutstanding}` inputs that have not ended. Inputs that start a new turn queue when all slots are busy; new inputs are rejected at the input limit.
147
90
 
148
91
  promptSnippet:
149
92
 
@@ -151,13 +94,9 @@ Create, message, and coordinate Agents
151
94
 
152
95
  promptGuidelines:
153
96
 
154
- - Use `agent(action='spawn', name=..., message=...)` for concrete, bounded
155
- work with a clear expected result. Keep the first message self-contained;
156
- use `context='fork'` only when the Agent needs the caller's conversation.
157
- - For independent work, start the Agents before waiting and partition tasks
158
- into non-overlapping responsibilities.
159
- - Use `agent(action='wait', ...)` when the next step depends on Agent
160
- results. Prefer one longer wait over repeated short polling.
97
+ - Use `agent(action='spawn', name=..., message=...)` for concrete, bounded work with a clear expected result. Keep the first message self-contained; use `context='fork'` only when the Agent needs the caller's conversation.
98
+ - For independent work, start the Agents before waiting and partition tasks into non-overlapping responsibilities.
99
+ - Use `agent(action='wait', ...)` when the next step depends on Agent results. Prefer one longer wait over repeated short polling.
161
100
 
162
101
  parameters:
163
102
 
@@ -167,7 +106,7 @@ parameters:
167
106
  - `cwd`: Working directory for `spawn` (default: the caller's current working directory).
168
107
  - `context`: Context for `spawn` (default: fresh). fresh starts without the caller's conversation; fork snapshots it.
169
108
  - `model`: Model for `spawn` as provider/modelId (default: the caller's model).
170
- - `thinkingLevel`: Thinking level for `spawn`: off, minimal, low, medium, high, or xhigh (default: the caller's level).
109
+ - `thinkingLevel`: Thinking level for `spawn`: off, minimal, low, medium, high, xhigh, or max (default: the caller's level).
171
110
  - `target`: Agent ID, name, or unique ID prefix for `send`, `wait`, or `abort`. `wait` and `send` with `deliverAs='write'` also accept an array. When omitted for `wait`, selects all owned Agents with pending or unread results.
172
111
  - `deliverAs`: Delivery for `send` (default: followUp). followUp waits until the recipient's current turn ends; steer delivers after its current tool calls, before the next model call; write delivers like steer but never starts a turn and has no result.
173
112
  - `history`: Number of most recent read results to return again per selected Agent for `wait` (default: 0).
@@ -179,8 +118,7 @@ parameters:
179
118
 
180
119
  results:
181
120
 
182
- In these formats, `<id>` is the Agent's short ID. Details use full IDs;
183
- arrays remain present when empty.
121
+ In these formats, `<id>` is the Agent's short ID. Details use full IDs; arrays remain present when empty.
184
122
 
185
123
  `spawn`:
186
124
 
@@ -227,9 +165,7 @@ Still pending: <name> (<id>), ...
227
165
  Stopped waiting for a new message. Wait again to keep waiting.
228
166
  ```
229
167
 
230
- A result returned again through `history` adds `history="true"`. A single
231
- unread completed result, with nothing pending, is returned as the answer
232
- alone. When there are no results or pending inputs:
168
+ A result returned again through `history` adds `history="true"`. A single unread completed result, with nothing pending, is returned as the answer alone. When there are no results or pending inputs:
233
169
 
234
170
  ```text
235
171
  No results.
@@ -238,15 +174,12 @@ No results.
238
174
  Details:
239
175
 
240
176
  ```text
241
- { results: [{ id, name, state, history, result? }], pending: [id], interrupted? }
177
+ { results: [{ id, name, state, history, result? }], pending: [{ id, name }], interrupted? }
242
178
  ```
243
179
 
244
- `state` is the input outcome (§5); `history` marks a previously read result;
245
- `interrupted` is `true` when a steer input ended the wait.
180
+ `state` is the input outcome (§5); `history` marks a previously read result; `interrupted` is `true` when a steer input ended the wait.
246
181
 
247
- `list` returns one entry per Agent, followed by its latest request, if any,
248
- and its time. With a query, entries are ordered by their most recent match,
249
- and a matching summary or user message shows its latest excerpt instead:
182
+ `list` returns one entry per Agent, followed by its latest request, if any, and its time. With a query, entries are ordered by their most recent match, and a matching summary or user message shows its latest excerpt instead:
250
183
 
251
184
  ```text
252
185
  <name> (<id>) <state> <cwd>
@@ -271,8 +204,7 @@ Details:
271
204
  { total, agents: [{ id, name?, ownerId?, state }] }
272
205
  ```
273
206
 
274
- `agents` is the current page; `total` counts all matches. `state` is the
275
- Agent's current state (§3).
207
+ `agents` is the current page; `total` counts all matches. `state` is the Agent's current state (§3).
276
208
 
277
209
  `abort`:
278
210
 
@@ -291,8 +223,7 @@ For unnamed root Agents, show the ID without a name or parentheses.
291
223
 
292
224
  errors:
293
225
 
294
- Invalid calls fail with an error. Errors that need a follow-up say what to
295
- do:
226
+ Invalid calls fail with an error. Errors that need a follow-up say what to do:
296
227
 
297
228
  ```text
298
229
  No visible Agent matches "<target>". Use list to find Agents.
@@ -318,8 +249,7 @@ Input rejected: <N> inputs have not ended. Wait for results or abort an Agent.
318
249
  <name> (<id>) is not owned by the caller.
319
250
  ```
320
251
 
321
- When an aborted turn does not stop within the timeout (§3), `abort` fails
322
- with the following; only restarting Pi stops it.
252
+ When an aborted turn does not stop within the timeout (§3), `abort` fails with the following; only restarting Pi stops it.
323
253
 
324
254
  ```text
325
255
  Agent <name> (<id>) did not stop within the timeout and may still be running.
@@ -327,19 +257,13 @@ Agent <name> (<id>) did not stop within the timeout and may still be running.
327
257
 
328
258
  truncation:
329
259
 
330
- Text beyond Pi's output limits is truncated with Pi's native notice, and
331
- the full text is saved to a file whose path appears in the notice. `wait`
332
- adds results in order; the result that crosses the limit is truncated but
333
- keeps its closing tag, and later results are omitted. A truncated result
334
- counts as read. Omitted results stay unread, and the text ends with:
260
+ Text beyond Pi's output limits is truncated with Pi's native notice, and the full text is saved to a file whose path appears in the notice. `wait` adds results in order; the result that crosses the limit is truncated but keeps its closing tag, and later results are omitted. A truncated result counts as read. Omitted results stay unread, and the text ends with:
335
261
 
336
262
  ```text
337
263
  [Results omitted: <name> (<id>), ... Use wait with fewer targets.]
338
264
  ```
339
265
 
340
- Details retain metadata, `truncation`, and `fullOutputPath`.
341
- `wait.results[].result` contains only retained text and is absent for
342
- omitted results.
266
+ Details retain metadata, `truncation`, and `fullOutputPath`. `wait.results[].result` contains only retained text and is absent for omitted results.
343
267
 
344
268
  messages:
345
269
 
@@ -349,18 +273,16 @@ A notification to the owner:
349
273
  Agent <name> (<id>) <outcome>.
350
274
  ```
351
275
 
352
- A recipient sees each message in an `agent-message` element naming the
353
- sender. An input from a sender other than the owner also carries a note
354
- asking for a reply:
276
+ A recipient sees each message in an `agent-message` element naming the sender. An input from a sender other than the owner also carries a note asking for a reply:
355
277
 
356
278
  ```text
357
- <agent-message from="<name>" id="<id>">
279
+ <agent-message name="<name>" id="<id>">
358
280
  <body>
359
281
  </agent-message>
360
282
  ```
361
283
 
362
284
  ```text
363
- <agent-message from="<name>" id="<id>" note="Reply with send">
285
+ <agent-message name="<name>" id="<id>" note="Reply with send">
364
286
  <body>
365
287
  </agent-message>
366
288
  ```
@@ -371,59 +293,61 @@ asking for a reply:
371
293
 
372
294
  A panel above the editor shows the current Agent's live work:
373
295
 
374
- Tasks (<N> live, /tasks to view)
375
- Agent <name> (<id>) <state> <time> <cost> <N> pending
376
- Agent <name> (<id>) <state> <time> <cost>
377
- Program <id> running <time> <cost>
378
- Agent <name> (<id>) <state> <time> <cost>
379
- +<N> more
380
-
381
- The live work is the owned Agents that are not `idle`, and the running
382
- background Programs with their Agents that are not `idle`. The rows list
383
- them as a tree in the order they started, each
384
- under its owner and each Program under its caller, with the owners needed to
385
- keep the tree. Rows that do not fit are counted in the last row. The time is
386
- how long the Agent's current turn or the Program has run, and the cost what the
387
- replies in that turn have cost, or for a Program, those of all the Agents under
388
- it. When a reply has no price, the cost is the input and output tokens instead,
389
- like `7.3M tok`.
390
-
391
- The panel updates as states change, and its times each second. It is shown
392
- while it has a row.
393
- `/tasks panel` hides or shows it.
296
+ ```text
297
+ Tasks (<N> live, /tasks to view)
298
+ Agent <name> (<id>) <state> <time> <cost> <N> pending
299
+ Agent <name> (<id>) <state> <time> <cost>
300
+ Program <id> running <time> <cost>
301
+ Agent <name> (<id>) <state> <time> <cost>
302
+ +<N> more
303
+ ```
304
+
305
+ The live work is the owned Agents that are not `idle`, and the running background Programs with their Agents that are not `idle`. The rows list them as a tree in the order they started, each under its owner and each Program under its caller, with the owners needed to keep the tree. Rows that do not fit are counted in the last row. The time is how long the Agent's current turn or the Program has run, and the cost what the replies in that turn have cost, or for a Program, those of all the Agents under it. When a reply has no price, the cost is the input and output tokens instead, like `7.3M tok`.
306
+
307
+ The panel updates as states change, and its times each second. It is shown while it has a row. `/tasks panel` hides or shows it.
394
308
 
395
309
  ### Queued messages
396
310
 
397
- Messages from other Agents that wait in the Session's queue, as during a turn,
398
- are listed above the editor, below the user's queued messages, until the
399
- Session takes them in:
311
+ Messages from other Agents that wait in the Session's queue, as during a turn, are listed above the editor, below the user's queued messages, until the Session takes them in:
312
+
313
+ ```text
314
+ Steering: [<sender>] <body>
315
+ Write: [<sender>] <body>
316
+ Follow-up: [<sender>] <body>
317
+ ```
318
+
319
+ ### Messages and results
400
320
 
401
- Steering: [<sender>] <body>
402
- Write: [<sender>] <body>
403
- Follow-up: [<sender>] <body>
321
+ A message from another Agent shows its sender after the label:
322
+
323
+ ```text
324
+ [pi-agents] from <name> (<id>)
325
+
326
+ <body>
327
+ ```
328
+
329
+ The user's input from the task viewer shows as a user message. `wait` shows each result with the first lines of its body; a Program's body starts at its output:
330
+
331
+ ```text
332
+ <name> (<id>) <state>
333
+ <body>
334
+ Program <id> <state> <time>
335
+ <body>
336
+ ```
337
+
338
+ Expanding (`app.tools.expand`) shows them as Pi shows them by default.
404
339
 
405
340
  ### Task viewer
406
341
 
407
- `/tasks` lists the Agents in the current Agent's tree, including its Programs'
408
- Agents, and its background Programs, live ones first and newest first, with
409
- their state and latest activity, and opens the selected one in a viewer. An
410
- Agent not owned by the current Agent is named with its owners, as
411
- `<name> ‹ <owner> ‹ …`, where a Program is `Program <id>`. The viewer follows
412
- an Agent's conversation, or a Program as it shows in the foreground, while the
413
- current Session keeps running.
342
+ `/tasks` lists the Agents in the current Agent's tree, including its Programs' Agents, and its background Programs, live ones first and newest first, with their state and latest activity, and opens the selected one in a viewer. An Agent not owned by the current Agent is named with its owners, as `<name> ‹ <owner> ‹ …`, where a Program is `Program <id>`. The viewer follows an Agent's conversation as Pi's chat shows it, or a Program as it shows in the foreground, while the current Session keeps running.
414
343
 
415
- Text entered in an Agent's viewer is the user's input to it, sent as in Pi's
416
- editor; a Program takes no input.
344
+ Text entered in an Agent's viewer is the user's input to it, sent as in Pi's editor; a Program takes no input.
417
345
 
418
- `app.interrupt` stops an Agent's current turn, as `abort` does, or a Program,
419
- as `stop` does. `app.clear` clears the input, and closes the viewer when the
420
- input is empty.
346
+ Keys act as in Pi's editor, on the viewed Agent or Program; on an empty input, `Ctrl+C` (`app.clear`) closes the viewer. The viewer scrolls as fullscreen mode does.
421
347
 
422
348
  ### Workbench
423
349
 
424
- `pd agents` opens the workbench, which lists every root Session on this
425
- machine and lets the user open and message them. Sessions started with `pd`
426
- keep running after their terminal closes. See [HOSTS.md](HOSTS.md).
350
+ `pd agents` opens the workbench, which lists every root Session on this machine and lets the user open and message them. Sessions started with `pd` keep running after their terminal closes. See [HOSTS.md](HOSTS.md).
427
351
 
428
352
  ## 8. Configuration
429
353
 
@@ -439,26 +363,13 @@ Settings live under `"pi-agents"` in the global `settings.json`:
439
363
  }
440
364
  ```
441
365
 
442
- - `maxConcurrent`, `maxOutstanding`: limits from §4. Both are positive
443
- integers with `maxConcurrent <= maxOutstanding`.
444
- - `extensions`: extension paths loaded by owned Agents, relative to Pi's
445
- agent directory. An owned Agent can use only tools its owner can use.
366
+ - `maxConcurrent`, `maxOutstanding`: limits from §4. Both are positive integers with `maxConcurrent <= maxOutstanding`.
367
+ - `extensions`: extension paths loaded by owned Agents, relative to Pi's agent directory. An owned Agent can use only tools its owner can use.
446
368
 
447
369
  Invalid settings produce a startup warning, and the defaults are used.
448
370
 
449
371
  ## 9. Runtime
450
372
 
451
- Each Agent is a persistent Pi Session. A root Agent is an ordinary Session.
452
- An owned Agent is a child Session that records its owner and root, and runs
453
- in its owner's process.
454
-
455
- A delivered message enters the recipient's current branch. An offline root
456
- Agent is started in a host, detached, with its saved working directory and
457
- model and the current configuration (HOSTS §4).
458
- If a root process cannot finish cleanup within a grace period on exit, it is
459
- forcibly terminated.
460
- Before shutting down an idle Session, another extension can emit
461
- `busy:query` on `pi.events` with `{ busy: false }`; it is set to `true`
462
- while the tree has live work.
463
- One runtime uses a Session at a time. If a Session is occupied at startup, Pi
464
- reports it and exits.
373
+ Each Agent is a persistent Pi Session. A root Agent is an ordinary Session. An owned Agent is a child Session that records its owner and root, and runs in its owner's process.
374
+
375
+ A delivered message enters the recipient's current branch. An offline root Agent is started in a host, detached, with its saved working directory and model and the current configuration (HOSTS §4). If a root process cannot finish cleanup within a grace period on exit, it is forcibly terminated. Before shutting down an idle Session, another extension can emit `busy:query` on `pi.events` with `{ busy: false }`; it is set to `true` while the tree has live work. One runtime uses a Session at a time. If a Session is occupied at startup, Pi reports it and exits.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tshu-w/pi-agents",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Multi-agent runtime for Pi: persistent async agents, dynamic workflows, and inter-agent messaging",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -26,6 +26,7 @@ import {
26
26
  nodes,
27
27
  resolveTarget,
28
28
  scheduler,
29
+ sender,
29
30
  shortId,
30
31
  TREE_ENTRY,
31
32
  trackTreeWork,
@@ -33,16 +34,18 @@ import {
33
34
  visibleIds,
34
35
  type AgentState,
35
36
  type Delivery,
37
+ type Sender,
36
38
  type Entry,
37
39
  type Spend,
38
40
  type TreeMetadata,
39
41
  } from "./registry.ts";
42
+ import { defaultSessionDir } from "../roots/paths.mjs";
40
43
 
41
44
  const AGENT_ENTRY = "pi-agents-agent";
42
45
  const SHUTDOWN_TIMEOUT_MS = 10_000;
43
46
  const EXTENSION_PATH = fileURLToPath(new URL("../index.ts", import.meta.url));
44
47
  export const SUBMIT_RESULT_TOOL_NAME = "submit_result";
45
- export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
48
+ export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
46
49
 
47
50
  export type Outcome = "completed" | "failed" | "aborted";
48
51
  type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
@@ -75,16 +78,11 @@ export interface AgentRecord {
75
78
  }
76
79
 
77
80
 
78
- function defaultSessionDirectory(cwd: string, agentDir: string): string {
79
- const safePath = `--${path.resolve(cwd).replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
80
- return path.join(path.resolve(agentDir), "sessions", safePath);
81
- }
82
-
83
81
  function childSessionDirectory(cwd: string, currentCwd: string, currentSessionDir: string, agentDir: string): string {
84
- const targetDefault = defaultSessionDirectory(cwd, agentDir);
82
+ const targetDefault = defaultSessionDir(cwd, agentDir);
85
83
  if (!currentSessionDir) return path.join(targetDefault, "subagents");
86
84
  const current = path.resolve(currentSessionDir);
87
- const currentDefault = defaultSessionDirectory(currentCwd, agentDir);
85
+ const currentDefault = defaultSessionDir(currentCwd, agentDir);
88
86
  if (current === currentDefault ||
89
87
  (path.basename(current) === "subagents" && path.dirname(current) === currentDefault)) {
90
88
  return path.join(targetDefault, "subagents");
@@ -205,6 +203,8 @@ interface Input {
205
203
  text: string;
206
204
  /** The user's input from the task viewer. */
207
205
  user?: boolean;
206
+ /** The Agent that sent it. */
207
+ sender?: Sender;
208
208
  record?: InputRecord;
209
209
  permit?: symbol;
210
210
  schema?: unknown;
@@ -214,7 +214,7 @@ interface Input {
214
214
  silent?: boolean;
215
215
  }
216
216
 
217
- type Message = Pick<Input, "text" | "user">;
217
+ type Message = Pick<Input, "text" | "user" | "sender">;
218
218
 
219
219
  interface Turn {
220
220
  inputs: Input[];
@@ -386,8 +386,9 @@ export class Agents {
386
386
  scheduler.releaseOutstanding(this.self.rootId, permit);
387
387
  throw error;
388
388
  }
389
- const text = messageText({ id: this.self.id, name: this.self.name() }, true, "followUp", request.message, this.ids());
390
- const queued = this.enqueue(agent, { text, permit, record: this.newRecord(agent) });
389
+ const self = { id: this.self.id, name: this.self.name() };
390
+ const text = messageText(self, true, "followUp", request.message, this.ids());
391
+ const queued = this.enqueue(agent, { text, sender: sender(self, this.ids()), permit, record: this.newRecord(agent) });
391
392
  return { record: agent.record, queued };
392
393
  }
393
394
 
@@ -415,13 +416,13 @@ export class Agents {
415
416
  }
416
417
 
417
418
  /**
418
- * Sends an input from the owner and resolves when it ends. With a schema, the Agent must call
419
+ * Sends an input from the owner, `from` as its text names it, and resolves when it ends. With a schema, the Agent must call
419
420
  * `submit_result` with a matching value, which becomes the input's `value`; `text` tells it so.
420
421
  * An input not ended within `timeoutMs` is aborted alone, and `timedOut` is set.
421
422
  */
422
- async request(id: string, text: string, delivery: "followUp" | "steer", schema?: unknown, timeoutMs?: number): Promise<{ record: InputRecord; timedOut: boolean }> {
423
+ async request(id: string, text: string, delivery: "followUp" | "steer", from: Sender, schema?: unknown, timeoutMs?: number): Promise<{ record: InputRecord; timedOut: boolean }> {
423
424
  const agent = this.agent(id);
424
- const input: Input = { text, permit: this.reserve(), record: this.newRecord(agent), done: deferred() };
425
+ const input: Input = { text, sender: from, permit: this.reserve(), record: this.newRecord(agent), done: deferred() };
425
426
  if (schema !== undefined) input.schema = schema;
426
427
  this.add(agent, input, delivery);
427
428
  let stopping: Promise<void> | undefined;
@@ -449,16 +450,16 @@ export class Agents {
449
450
  input.submitted = { value: args.value };
450
451
  }
451
452
 
452
- accept(id: string, text: string, delivery: Delivery, options: { fromOwner: boolean; notification: boolean }): { queued: boolean } {
453
+ accept(id: string, text: string, delivery: Delivery, options: { fromOwner: boolean; notification: boolean; sender?: Sender }): { queued: boolean } {
453
454
  const agent = this.agent(id);
454
455
  if (agent.stopped && !options.fromOwner) delivery = "write";
455
456
  if (delivery === "write") {
456
- this.write(agent, text);
457
+ this.write(agent, text, options.sender);
457
458
  return { queued: false };
458
459
  }
459
460
  // Notifications keep their owner informed even at the input limit.
460
461
  const permit = options.notification ? undefined : this.reserve();
461
- const input: Input = { text, permit, record: options.fromOwner && !options.notification ? this.newRecord(agent) : undefined };
462
+ const input: Input = { text, sender: options.sender, permit, record: options.fromOwner && !options.notification ? this.newRecord(agent) : undefined };
462
463
  return { queued: this.add(agent, input, delivery) };
463
464
  }
464
465
 
@@ -620,8 +621,8 @@ export class Agents {
620
621
  if (errors.length > 0) throw new Error(errors.map((error) => error instanceof Error ? error.message : String(error)).join("\n"));
621
622
  }
622
623
 
623
- private write(agent: Owned, text: string): void {
624
- const message = customMessage(text, "write");
624
+ private write(agent: Owned, text: string, from?: Sender): void {
625
+ const message = customMessage(text, "write", { sender: from });
625
626
  const session = agent.session;
626
627
  if (session?.isStreaming) {
627
628
  session.agent.steer({ role: "custom", ...message, timestamp: Date.now() });
@@ -663,9 +664,9 @@ export class Agents {
663
664
  this.changed();
664
665
  if (!turn.started) return;
665
666
  if (agent.session?.isStreaming) {
666
- agent.session.agent.steer({ role: "custom", ...customMessage(input.text, "steer", undefined, input.user), timestamp: Date.now() });
667
+ agent.session.agent.steer({ role: "custom", ...customMessage(input.text, "steer", { user: input.user, sender: input.sender }), timestamp: Date.now() });
667
668
  } else {
668
- turn.pending.push({ text: input.text, user: input.user });
669
+ turn.pending.push({ text: input.text, user: input.user, sender: input.sender });
669
670
  }
670
671
  }
671
672
 
@@ -691,11 +692,11 @@ export class Agents {
691
692
  for (const input of turn.inputs) if (input.record) input.record.state = "running";
692
693
  this.persist(agent);
693
694
  this.changed();
694
- let batch: Message[] = turn.inputs.map(({ text, user }) => ({ text, user }));
695
+ let batch: Message[] = turn.inputs.map(({ text, user, sender }) => ({ text, user, sender }));
695
696
  while (batch.length > 0 && !turn.abort.signal.aborted) {
696
- for (const { text, user } of batch.slice(0, -1)) await session.sendCustomMessage(customMessage(text, "steer", undefined, user), { triggerTurn: false });
697
- const last = batch.at(-1)!;
698
- await session.sendCustomMessage(customMessage(last.text, "steer", undefined, last.user), { triggerTurn: true });
697
+ for (const { text, ...rest } of batch.slice(0, -1)) await session.sendCustomMessage(customMessage(text, "steer", rest), { triggerTurn: false });
698
+ const { text, ...rest } = batch.at(-1)!;
699
+ await session.sendCustomMessage(customMessage(text, "steer", rest), { triggerTurn: true });
699
700
  await session.waitForIdle();
700
701
  const leftover = takeQueued(session.agent);
701
702
  for (const message of leftover) {
@@ -703,7 +704,7 @@ export class Agents {
703
704
  }
704
705
  batch = [
705
706
  ...turn.pending.splice(0),
706
- ...leftover.flatMap(({ content, details }) => details.delivery === "write" ? [] : [{ text: content, user: details.user }]),
707
+ ...leftover.flatMap(({ content, details }) => details.delivery === "write" ? [] : [{ text: content, user: details.user, sender: details.sender }]),
707
708
  ];
708
709
  }
709
710
  const last = lastAnswer(session, before);