@narumitw/pi-subagents 2.1.3 → 3.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/README.md +181 -1250
- package/dist/child-communication-bridge.ts +136 -0
- package/dist/child-communication-bridge.ts.map +7 -0
- package/dist/chunks/chunk-UKTWUQRM.js +667 -0
- package/dist/chunks/chunk-UKTWUQRM.js.map +7 -0
- package/dist/index.ts +1448 -1598
- package/dist/index.ts.map +4 -4
- package/docs/design-principles.md +31 -0
- package/docs/ipc-transport-idea.md +103 -0
- package/docs/tools.md +109 -0
- package/package.json +11 -19
- package/src/broker-credentials.ts +71 -0
- package/src/child-communication-bridge.ts +153 -0
- package/src/child-communication-tools.ts +147 -0
- package/src/completion-renderer.ts +60 -0
- package/src/index.ts +1 -1
- package/src/message-broker.ts +628 -0
- package/src/model-output.ts +42 -0
- package/src/process.ts +688 -0
- package/src/runtime.ts +538 -0
- package/src/subagents.ts +33 -34
- package/src/tools.ts +416 -0
- package/src/types.ts +85 -0
- package/src/widget.ts +130 -0
- package/dist/chunks/auto-transport-FUUKFDIG.ts +0 -109
- package/dist/chunks/auto-transport-FUUKFDIG.ts.map +0 -7
- package/dist/chunks/capability-grant-PR72SWWS.ts +0 -19
- package/dist/chunks/capability-grant-PR72SWWS.ts.map +0 -7
- package/dist/chunks/child-peer-bridge.ts +0 -108
- package/dist/chunks/child-peer-bridge.ts.map +0 -7
- package/dist/chunks/chunk-2LMJU25E.ts +0 -1641
- package/dist/chunks/chunk-2LMJU25E.ts.map +0 -7
- package/dist/chunks/chunk-47SK2QYC.ts +0 -56
- package/dist/chunks/chunk-47SK2QYC.ts.map +0 -7
- package/dist/chunks/chunk-4AQSF7AS.ts +0 -29
- package/dist/chunks/chunk-4AQSF7AS.ts.map +0 -7
- package/dist/chunks/chunk-5DGRMKNV.ts +0 -126
- package/dist/chunks/chunk-5DGRMKNV.ts.map +0 -7
- package/dist/chunks/chunk-6H6TBBED.ts +0 -108
- package/dist/chunks/chunk-6H6TBBED.ts.map +0 -7
- package/dist/chunks/chunk-6KJ34M6S.ts +0 -159
- package/dist/chunks/chunk-6KJ34M6S.ts.map +0 -7
- package/dist/chunks/chunk-6NSJVPXX.ts +0 -480
- package/dist/chunks/chunk-6NSJVPXX.ts.map +0 -7
- package/dist/chunks/chunk-7AAJEUSL.ts +0 -238
- package/dist/chunks/chunk-7AAJEUSL.ts.map +0 -7
- package/dist/chunks/chunk-BFK2Z4ZF.ts +0 -189
- package/dist/chunks/chunk-BFK2Z4ZF.ts.map +0 -7
- package/dist/chunks/chunk-BOAXY55Y.ts +0 -153
- package/dist/chunks/chunk-BOAXY55Y.ts.map +0 -7
- package/dist/chunks/chunk-D4CR7T73.ts +0 -353
- package/dist/chunks/chunk-D4CR7T73.ts.map +0 -7
- package/dist/chunks/chunk-FEVPWRMU.ts +0 -300
- package/dist/chunks/chunk-FEVPWRMU.ts.map +0 -7
- package/dist/chunks/chunk-G6VZTCFH.ts +0 -70
- package/dist/chunks/chunk-G6VZTCFH.ts.map +0 -7
- package/dist/chunks/chunk-H3FP6DLR.ts +0 -160
- package/dist/chunks/chunk-H3FP6DLR.ts.map +0 -7
- package/dist/chunks/chunk-HA36LPCO.ts +0 -130
- package/dist/chunks/chunk-HA36LPCO.ts.map +0 -7
- package/dist/chunks/chunk-HOP5FXDT.ts +0 -27
- package/dist/chunks/chunk-HOP5FXDT.ts.map +0 -7
- package/dist/chunks/chunk-IGAMVWFN.ts +0 -139
- package/dist/chunks/chunk-IGAMVWFN.ts.map +0 -7
- package/dist/chunks/chunk-ILEQ27AL.ts +0 -45
- package/dist/chunks/chunk-ILEQ27AL.ts.map +0 -7
- package/dist/chunks/chunk-ITVWPNU4.ts +0 -819
- package/dist/chunks/chunk-ITVWPNU4.ts.map +0 -7
- package/dist/chunks/chunk-IWC32VPY.ts +0 -172
- package/dist/chunks/chunk-IWC32VPY.ts.map +0 -7
- package/dist/chunks/chunk-JSZIP73U.ts +0 -362
- package/dist/chunks/chunk-JSZIP73U.ts.map +0 -7
- package/dist/chunks/chunk-JU6LUNLP.ts +0 -380
- package/dist/chunks/chunk-JU6LUNLP.ts.map +0 -7
- package/dist/chunks/chunk-KMGKCEO4.ts +0 -38
- package/dist/chunks/chunk-KMGKCEO4.ts.map +0 -7
- package/dist/chunks/chunk-LASD73CM.ts +0 -339
- package/dist/chunks/chunk-LASD73CM.ts.map +0 -7
- package/dist/chunks/chunk-LEOYDZI3.ts +0 -22
- package/dist/chunks/chunk-LEOYDZI3.ts.map +0 -7
- package/dist/chunks/chunk-LL4LP2T7.ts +0 -737
- package/dist/chunks/chunk-LL4LP2T7.ts.map +0 -7
- package/dist/chunks/chunk-N2T5IN4X.ts +0 -18
- package/dist/chunks/chunk-N2T5IN4X.ts.map +0 -7
- package/dist/chunks/chunk-N7BLVXKK.ts +0 -73
- package/dist/chunks/chunk-N7BLVXKK.ts.map +0 -7
- package/dist/chunks/chunk-NLT67IZS.ts +0 -322
- package/dist/chunks/chunk-NLT67IZS.ts.map +0 -7
- package/dist/chunks/chunk-NTRPLF46.ts +0 -63
- package/dist/chunks/chunk-NTRPLF46.ts.map +0 -7
- package/dist/chunks/chunk-ONDTY4EL.ts +0 -91
- package/dist/chunks/chunk-ONDTY4EL.ts.map +0 -7
- package/dist/chunks/chunk-OVPHFGKO.ts +0 -1769
- package/dist/chunks/chunk-OVPHFGKO.ts.map +0 -7
- package/dist/chunks/chunk-P7OH4XMF.ts +0 -60
- package/dist/chunks/chunk-P7OH4XMF.ts.map +0 -7
- package/dist/chunks/chunk-PBZMBTNJ.ts +0 -422
- package/dist/chunks/chunk-PBZMBTNJ.ts.map +0 -7
- package/dist/chunks/chunk-PGLSFLYW.ts +0 -52
- package/dist/chunks/chunk-PGLSFLYW.ts.map +0 -7
- package/dist/chunks/chunk-RRR66UWR.ts +0 -28
- package/dist/chunks/chunk-RRR66UWR.ts.map +0 -7
- package/dist/chunks/chunk-RSUXZD6S.ts +0 -275
- package/dist/chunks/chunk-RSUXZD6S.ts.map +0 -7
- package/dist/chunks/chunk-RTCVYIZA.ts +0 -221
- package/dist/chunks/chunk-RTCVYIZA.ts.map +0 -7
- package/dist/chunks/chunk-RY2AEGMZ.ts +0 -34
- package/dist/chunks/chunk-RY2AEGMZ.ts.map +0 -7
- package/dist/chunks/chunk-SWGQLFSD.ts +0 -60
- package/dist/chunks/chunk-SWGQLFSD.ts.map +0 -7
- package/dist/chunks/chunk-TM2R67J3.ts +0 -52
- package/dist/chunks/chunk-TM2R67J3.ts.map +0 -7
- package/dist/chunks/chunk-TMZRHIIK.ts +0 -497
- package/dist/chunks/chunk-TMZRHIIK.ts.map +0 -7
- package/dist/chunks/chunk-TZ34IQ3M.ts +0 -59
- package/dist/chunks/chunk-TZ34IQ3M.ts.map +0 -7
- package/dist/chunks/chunk-UMBPMVJW.ts +0 -22
- package/dist/chunks/chunk-UMBPMVJW.ts.map +0 -7
- package/dist/chunks/chunk-VDG7LTYE.ts +0 -80
- package/dist/chunks/chunk-VDG7LTYE.ts.map +0 -7
- package/dist/chunks/chunk-X4NMONPE.ts +0 -73
- package/dist/chunks/chunk-X4NMONPE.ts.map +0 -7
- package/dist/chunks/chunk-YPJEN6NU.ts +0 -65
- package/dist/chunks/chunk-YPJEN6NU.ts.map +0 -7
- package/dist/chunks/chunk-YU53SHA7.ts +0 -328
- package/dist/chunks/chunk-YU53SHA7.ts.map +0 -7
- package/dist/chunks/completion-delivery-RSJU6BXL.ts +0 -17
- package/dist/chunks/completion-delivery-RSJU6BXL.ts.map +0 -7
- package/dist/chunks/config-status-FKGDECZ3.ts +0 -35
- package/dist/chunks/config-status-FKGDECZ3.ts.map +0 -7
- package/dist/chunks/config-ui-ABHYNGQ7.ts +0 -1212
- package/dist/chunks/config-ui-ABHYNGQ7.ts.map +0 -7
- package/dist/chunks/consult-LJU3IQY5.ts +0 -519
- package/dist/chunks/consult-LJU3IQY5.ts.map +0 -7
- package/dist/chunks/context-QDKLVQXE.ts +0 -11
- package/dist/chunks/context-QDKLVQXE.ts.map +0 -7
- package/dist/chunks/create-stateful-transport-JWC2EFYL.ts +0 -130
- package/dist/chunks/create-stateful-transport-JWC2EFYL.ts.map +0 -7
- package/dist/chunks/cwd-policy-NB6XZ5GF.ts +0 -17
- package/dist/chunks/cwd-policy-NB6XZ5GF.ts.map +0 -7
- package/dist/chunks/delegation-contract-LA56I5DT.ts +0 -24
- package/dist/chunks/delegation-contract-LA56I5DT.ts.map +0 -7
- package/dist/chunks/discovery-MIFB2U4Y.ts +0 -12
- package/dist/chunks/discovery-MIFB2U4Y.ts.map +0 -7
- package/dist/chunks/execution-Q2JZLKJA.ts +0 -4037
- package/dist/chunks/execution-Q2JZLKJA.ts.map +0 -7
- package/dist/chunks/in-process-transport-HJ6TZXC3.ts +0 -39
- package/dist/chunks/in-process-transport-HJ6TZXC3.ts.map +0 -7
- package/dist/chunks/inspect-UH2TKH6E.ts +0 -634
- package/dist/chunks/inspect-UH2TKH6E.ts.map +0 -7
- package/dist/chunks/peer-communication-KL36Z6CO.ts +0 -294
- package/dist/chunks/peer-communication-KL36Z6CO.ts.map +0 -7
- package/dist/chunks/persistence-UY3PY6E5.ts +0 -348
- package/dist/chunks/persistence-UY3PY6E5.ts.map +0 -7
- package/dist/chunks/registry-BT54L6CY.ts +0 -1445
- package/dist/chunks/registry-BT54L6CY.ts.map +0 -7
- package/dist/chunks/retained-semantic-state-GM75FZGE.ts +0 -81
- package/dist/chunks/retained-semantic-state-GM75FZGE.ts.map +0 -7
- package/dist/chunks/rpc-transport-7R7DVCEB.ts +0 -1222
- package/dist/chunks/rpc-transport-7R7DVCEB.ts.map +0 -7
- package/dist/chunks/runtime-policy-5YELCOVC.ts +0 -15
- package/dist/chunks/runtime-policy-5YELCOVC.ts.map +0 -7
- package/dist/chunks/semantic-snapshot-WPJ3UAFW.ts +0 -19
- package/dist/chunks/semantic-snapshot-WPJ3UAFW.ts.map +0 -7
- package/dist/chunks/spawn-idempotency-BNHOSMZW.ts +0 -13
- package/dist/chunks/spawn-idempotency-BNHOSMZW.ts.map +0 -7
- package/dist/chunks/stateful-lifecycle-JAR6K55H.ts +0 -15
- package/dist/chunks/stateful-lifecycle-JAR6K55H.ts.map +0 -7
- package/dist/chunks/subprocess-transport-VHZJRBTW.ts +0 -160
- package/dist/chunks/subprocess-transport-VHZJRBTW.ts.map +0 -7
- package/dist/chunks/usage-recording-store-CW3EH2SZ.ts +0 -164
- package/dist/chunks/usage-recording-store-CW3EH2SZ.ts.map +0 -7
- package/dist/chunks/workspace-WDKLN72J.ts +0 -11
- package/dist/chunks/workspace-WDKLN72J.ts.map +0 -7
- package/docs/async-runtime-protocol.md +0 -84
- package/docs/implementation-notes/pi-subagents-capability-matrix.md +0 -62
- package/docs/implementation-notes/pi-subagents-current-direction.md +0 -99
- package/docs/implementation-notes/pi-subagents-rpc-v1.md +0 -153
- package/docs/pi-subagents-diagrams.md +0 -183
- package/src/adaptive-scheduler.ts +0 -224
- package/src/admission-benchmark.ts +0 -95
- package/src/admission-policy.ts +0 -78
- package/src/agent-projection.ts +0 -53
- package/src/agents/built-ins.ts +0 -71
- package/src/agents/catalog.ts +0 -241
- package/src/agents/discovery.ts +0 -265
- package/src/agents/types.ts +0 -100
- package/src/agents.ts +0 -51
- package/src/async-subagent-benchmark.ts +0 -532
- package/src/auto-transport.ts +0 -121
- package/src/blocking-status.ts +0 -63
- package/src/cached-module-loader.ts +0 -18
- package/src/capabilities.ts +0 -145
- package/src/capability-grant.ts +0 -115
- package/src/capability-router.ts +0 -107
- package/src/child-peer-bridge.ts +0 -124
- package/src/child-peer-tools.ts +0 -132
- package/src/completion-delivery.ts +0 -409
- package/src/completion-render.ts +0 -190
- package/src/completion-requirement.ts +0 -479
- package/src/completion-routing.ts +0 -24
- package/src/config-registration.ts +0 -114
- package/src/config-status.ts +0 -217
- package/src/config-ui.ts +0 -983
- package/src/consult-policy.ts +0 -15
- package/src/consult-registration.ts +0 -125
- package/src/consult-render.ts +0 -194
- package/src/consult-resources.ts +0 -51
- package/src/consult-tool.ts +0 -95
- package/src/consult.ts +0 -566
- package/src/context.ts +0 -126
- package/src/create-stateful-transport.ts +0 -157
- package/src/cwd-policy.ts +0 -183
- package/src/delegation-contract.ts +0 -463
- package/src/execution/budget.ts +0 -56
- package/src/execution/runtime-policy.ts +0 -19
- package/src/execution-plan.ts +0 -322
- package/src/execution-ui.ts +0 -259
- package/src/execution.ts +0 -1679
- package/src/in-process-transport.ts +0 -941
- package/src/inspect-registration.ts +0 -64
- package/src/inspect-render.ts +0 -334
- package/src/inspect-tool.ts +0 -45
- package/src/inspect.ts +0 -769
- package/src/integration-controller.ts +0 -98
- package/src/limits.ts +0 -75
- package/src/orchestration-metrics.ts +0 -116
- package/src/outcome.ts +0 -61
- package/src/panel-child-group.ts +0 -35
- package/src/panel-contract.ts +0 -343
- package/src/panel-evidence.ts +0 -59
- package/src/panel-execution.ts +0 -769
- package/src/panel-failure.ts +0 -56
- package/src/panel-planning.ts +0 -175
- package/src/panel-presets.ts +0 -3
- package/src/panel-prompts.ts +0 -132
- package/src/panel-reconciliation.ts +0 -57
- package/src/panel-render.ts +0 -103
- package/src/parallel-limit-ui.ts +0 -113
- package/src/params.ts +0 -278
- package/src/peer-communication.ts +0 -352
- package/src/peer-transport.ts +0 -49
- package/src/persistence.ts +0 -522
- package/src/pi-args.ts +0 -43
- package/src/pi-invocation.ts +0 -168
- package/src/process-control.ts +0 -43
- package/src/prompt-resources.ts +0 -38
- package/src/prompt-source-safety.ts +0 -48
- package/src/protocol.ts +0 -76
- package/src/registry-types.ts +0 -204
- package/src/registry.ts +0 -1698
- package/src/render-common.ts +0 -252
- package/src/render.ts +0 -704
- package/src/result-contract.ts +0 -431
- package/src/retained-semantic-state.ts +0 -100
- package/src/rpc-timeout-finalization.ts +0 -207
- package/src/rpc-transport-metadata.ts +0 -65
- package/src/rpc-transport.ts +0 -1035
- package/src/rpc-turn-capture.ts +0 -217
- package/src/runner-outcome.ts +0 -31
- package/src/runner-result.ts +0 -55
- package/src/runner-types.ts +0 -102
- package/src/runner-usage.ts +0 -48
- package/src/runner.ts +0 -866
- package/src/safe-text.ts +0 -67
- package/src/semantic-snapshot.ts +0 -214
- package/src/session-guidance-contract.ts +0 -399
- package/src/settings/inspection.ts +0 -302
- package/src/settings/schema.ts +0 -195
- package/src/settings-reader.ts +0 -215
- package/src/settings.ts +0 -452
- package/src/spawn-idempotency.ts +0 -68
- package/src/stateful-agent-view.ts +0 -85
- package/src/stateful-config.ts +0 -13
- package/src/stateful-guidance.ts +0 -29
- package/src/stateful-lifecycle.ts +0 -74
- package/src/stateful-limit-ui.ts +0 -249
- package/src/stateful-limits.ts +0 -96
- package/src/stateful-prompt.ts +0 -49
- package/src/stateful-registration.ts +0 -1320
- package/src/stateful-render.ts +0 -318
- package/src/stateful-safety.ts +0 -47
- package/src/stateful-tool-params.ts +0 -215
- package/src/stateful.ts +0 -12
- package/src/subagent-details.ts +0 -43
- package/src/subagents-extension.ts +0 -355
- package/src/subprocess-transport.ts +0 -145
- package/src/supervision.ts +0 -104
- package/src/task-path.ts +0 -65
- package/src/timeout-checkpoint.ts +0 -305
- package/src/timeout-finalization.ts +0 -75
- package/src/tool-schema-compatibility.ts +0 -73
- package/src/transport-types.ts +0 -74
- package/src/transport-ui.ts +0 -135
- package/src/transport.ts +0 -43
- package/src/turn-budget.ts +0 -109
- package/src/usage-format.ts +0 -42
- package/src/usage-recording-config.ts +0 -13
- package/src/usage-recording-store.ts +0 -183
- package/src/usage-recording.ts +0 -478
- package/src/verification-harness.ts +0 -516
- package/src/verification-policy.ts +0 -67
- package/src/verification-receipt.ts +0 -275
- package/src/verified-execution-benchmark.ts +0 -86
- package/src/verified-execution-contract.ts +0 -191
- package/src/verified-execution-schema.ts +0 -32
- package/src/work-item-ledger.ts +0 -1404
- package/src/work-item-persistence.ts +0 -254
- package/src/workflow-completion-controller.ts +0 -397
- package/src/workflow-planning.ts +0 -172
- package/src/workflow-tree-identity.ts +0 -289
- package/src/workflow-ui.ts +0 -69
- package/src/workflow-verification.ts +0 -296
- package/src/workspace.ts +0 -174
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Design principles
|
|
2
|
+
|
|
3
|
+
These principles define Pi Subagents rather than requirements for every Pi subagent implementation.
|
|
4
|
+
|
|
5
|
+
## Context isolation is the purpose
|
|
6
|
+
|
|
7
|
+
A subagent starts as a fresh Pi process.
|
|
8
|
+
|
|
9
|
+
It does not inherit the main agent's conversation history.
|
|
10
|
+
|
|
11
|
+
That isolation is not a limitation—it is the point.
|
|
12
|
+
|
|
13
|
+
Fresh does not mean empty.
|
|
14
|
+
|
|
15
|
+
The subagent still receives Pi's standard system prompt, the main agent's effective model, applicable project context, its selected tools, and one explicit task.
|
|
16
|
+
|
|
17
|
+
The task defines the child's specialization, while the selected tools define its authority.
|
|
18
|
+
|
|
19
|
+
Give each subagent a self-contained task containing only the context needed to complete that task.
|
|
20
|
+
|
|
21
|
+
If work depends on substantial history from the current conversation, continue in the current thread instead of copying that history into a new subagent.
|
|
22
|
+
|
|
23
|
+
## Simplicity over feature breadth
|
|
24
|
+
|
|
25
|
+
Keep the architecture explicit, minimal, and easy to understand.
|
|
26
|
+
|
|
27
|
+
Feature breadth alone does not justify more machinery.
|
|
28
|
+
|
|
29
|
+
Prefer a small system whose behavior can be understood end to end over a feature-complete system whose behavior is obscured by layers of abstraction and orchestration.
|
|
30
|
+
|
|
31
|
+
Every capability must earn its place: its value must outweigh both the complexity it introduces and the maintenance burden it leaves behind.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Per-child IPC transport idea
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
This document records an unverified implementation idea for later evaluation.
|
|
6
|
+
|
|
7
|
+
The current loopback TCP broker with private inherited-pipe credential bootstrap remains the implemented and authoritative transport.
|
|
8
|
+
|
|
9
|
+
Do not treat this document as an approved migration plan or compatibility guarantee.
|
|
10
|
+
|
|
11
|
+
## Motivation
|
|
12
|
+
|
|
13
|
+
The TCP broker provides authenticated cross-process messaging but requires a server, ephemeral port, per-job tokens, private credential bootstrap, socket lifecycle management, NDJSON framing, connection deadlines, and long-poll handling.
|
|
14
|
+
|
|
15
|
+
A private communication channel created with each child process may preserve the messaging contract with fewer transport concepts and less lifecycle code.
|
|
16
|
+
|
|
17
|
+
## Proposed direction
|
|
18
|
+
|
|
19
|
+
Spawn each child with a private Node.js child-process IPC channel:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
spawn(command, args, {
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Bind the channel to the job represented by that child process rather than accepting a job identifier or authentication token from the child.
|
|
28
|
+
|
|
29
|
+
Send request and response events in either direction over the same persistent channel.
|
|
30
|
+
|
|
31
|
+
Keep request state in a small session-owned registry in the parent and keep child wait promises in the child bridge.
|
|
32
|
+
|
|
33
|
+
A possible exchange is:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
sender -> parent: send(recipient, message)
|
|
37
|
+
parent -> sender: accepted(requestId)
|
|
38
|
+
responder -> parent: send(requestId, message)
|
|
39
|
+
parent -> requester: response(requestId, message)
|
|
40
|
+
child requester -> parent: wait(requestId)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The exact wire messages and ownership boundaries remain undecided.
|
|
44
|
+
|
|
45
|
+
## Complexity that may be removed
|
|
46
|
+
|
|
47
|
+
A successful IPC design may remove:
|
|
48
|
+
|
|
49
|
+
- The loopback TCP server and ephemeral port.
|
|
50
|
+
- Per-job broker tokens and their private bootstrap pipe.
|
|
51
|
+
- Socket connection and connection-limit management.
|
|
52
|
+
- Custom NDJSON request and response framing.
|
|
53
|
+
- Connection and frame deadlines.
|
|
54
|
+
- Long-poll socket ownership.
|
|
55
|
+
- Cross-job token validation.
|
|
56
|
+
|
|
57
|
+
## Behavior that must remain
|
|
58
|
+
|
|
59
|
+
Any replacement must preserve:
|
|
60
|
+
|
|
61
|
+
- A fresh child Pi process with no inherited main-agent conversation history.
|
|
62
|
+
- Context-specific provider-visible `subagent_send` definitions for main and child processes.
|
|
63
|
+
- The child `subagent_wait` tool for child-originated requests.
|
|
64
|
+
- Plain-text requests and responses in either direction.
|
|
65
|
+
- At most four unresolved requests per job across both directions.
|
|
66
|
+
- The 48 KiB and 1,992-line message limits that reserve space for protocol envelopes.
|
|
67
|
+
- First-response-wins semantics.
|
|
68
|
+
- Retryable waits after timeout.
|
|
69
|
+
- Caller cancellation that stops only the current wait.
|
|
70
|
+
- One-shot replay of a child response that arrives immediately before a main-agent job wait.
|
|
71
|
+
- Interruption of active child response waits after a main-agent request is accepted, without consuming the original child requests.
|
|
72
|
+
- Immediate rejection of pending waits after job cancellation, child exit, session replacement, reload, or shutdown.
|
|
73
|
+
- Retention-limited state and terminal-safe cleanup.
|
|
74
|
+
- Untrusted-content handling and terminal sanitization.
|
|
75
|
+
- No peer messaging, retained child conversations, or nested subagents.
|
|
76
|
+
|
|
77
|
+
## Compatibility spike
|
|
78
|
+
|
|
79
|
+
Before selecting IPC, build the smallest possible spike that proves bidirectional messages, cancellation, child exit, and repeated cleanup through the actual Pi launch path.
|
|
80
|
+
|
|
81
|
+
Exercise the spike with npm-installed Node.js Pi, Bun execution, and the Pi standalone executable where available.
|
|
82
|
+
|
|
83
|
+
Include Linux and macOS, and include Windows before claiming Windows compatibility.
|
|
84
|
+
|
|
85
|
+
Verify that the IPC channel does not interfere with Pi RPC framing, subprocess termination, detached process-group cleanup, or extension loading.
|
|
86
|
+
|
|
87
|
+
Reject the IPC design if any supported runtime cannot expose a reliable private channel without runtime-specific branches or substantial fallback code.
|
|
88
|
+
|
|
89
|
+
## Fallback
|
|
90
|
+
|
|
91
|
+
If child-process IPC is not portable enough, evaluate dedicated inherited file descriptors such as fd 3 and fd 4.
|
|
92
|
+
|
|
93
|
+
Inherited pipes can still remove TCP addressing and authentication, but they require explicit frame-size limits and stream lifecycle handling.
|
|
94
|
+
|
|
95
|
+
HTTP loopback may simplify framing incrementally, but it retains the server, port, token, and network lifecycle and therefore offers a smaller reduction.
|
|
96
|
+
|
|
97
|
+
Do not use filesystem polling or add an ad hoc messaging protocol to Pi's RPC stdin and stdout unless evidence shows that its additional coupling and cleanup remain simpler than the current transport.
|
|
98
|
+
|
|
99
|
+
## Decision criteria
|
|
100
|
+
|
|
101
|
+
Adopt a replacement only if it measurably reduces implementation and lifecycle complexity while preserving current behavior and supported runtime compatibility.
|
|
102
|
+
|
|
103
|
+
Prefer the current TCP broker if the replacement requires runtime-specific branches, multiple transport fallbacks, or weaker isolation.
|
package/docs/tools.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Pi Subagents tools
|
|
2
|
+
|
|
3
|
+
## `subagent_spawn`
|
|
4
|
+
|
|
5
|
+
| Parameter | Type | Required | Constraint / default |
|
|
6
|
+
| --- | --- | --- | --- |
|
|
7
|
+
| `task` | `string` | Yes | Self-contained task, up to 50 KiB of UTF-8 text. |
|
|
8
|
+
| `tools` | `string[]` | No | Up to 64 names from `read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, and `ls`; defaults to `read`, `grep`, `find`, and `ls`. |
|
|
9
|
+
| `thinkingLevel` | `string` | No | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`; defaults to the main agent's effective thinking level. |
|
|
10
|
+
| `timeout` | `number` | No | Seconds; `> 0` through `2,147,483.647`; no default timeout. |
|
|
11
|
+
|
|
12
|
+
Starts one task-specialized subagent job with the selected tool capabilities and returns its job ID immediately.
|
|
13
|
+
|
|
14
|
+
The runtime always adds `subagent_send` and `subagent_wait` to the selected tools.
|
|
15
|
+
|
|
16
|
+
The child inherits the main agent's effective provider and model at spawn time.
|
|
17
|
+
|
|
18
|
+
Providers registered by a parent extension throw before the job is queued because children disable unrelated extensions.
|
|
19
|
+
|
|
20
|
+
A process-local runtime API key, including a parent-only `--api-key` value, also throws before queuing.
|
|
21
|
+
|
|
22
|
+
Use Pi's stored credentials or environment credentials that the child process can read.
|
|
23
|
+
|
|
24
|
+
Unavailable or extension-only tool names throw before the job is queued.
|
|
25
|
+
|
|
26
|
+
Throws without launching a child when the session broker is unavailable.
|
|
27
|
+
|
|
28
|
+
## `subagent_inspect`
|
|
29
|
+
|
|
30
|
+
No parameters.
|
|
31
|
+
|
|
32
|
+
## `subagent_cancel`
|
|
33
|
+
|
|
34
|
+
| Parameter | Type | Required | Constraint / default |
|
|
35
|
+
| --- | --- | --- | --- |
|
|
36
|
+
| `jobId` | `string` | Yes | Job ID returned by `subagent_spawn`. |
|
|
37
|
+
|
|
38
|
+
## `subagent_wait`
|
|
39
|
+
|
|
40
|
+
### Main agent
|
|
41
|
+
|
|
42
|
+
| Parameter | Type | Required | Constraint / default |
|
|
43
|
+
| --- | --- | --- | --- |
|
|
44
|
+
| `jobId` | `string` | Yes | Job ID to wait for. |
|
|
45
|
+
| `timeout` | `number` | No | Seconds; `> 0` through `2,147,483.647`; no default and does not cancel the job. |
|
|
46
|
+
|
|
47
|
+
Returns `{ jobId, state, timedOut: false, interrupted: true, reason: "subagent_message" }` without cancelling the job when a child request or response arrives.
|
|
48
|
+
|
|
49
|
+
### Subagent
|
|
50
|
+
|
|
51
|
+
| Parameter | Type | Required | Constraint / default |
|
|
52
|
+
| --- | --- | --- | --- |
|
|
53
|
+
| `requestId` | `string` | Yes | Request ID returned by a child-originated `subagent_send`. |
|
|
54
|
+
| `timeout` | `number` | No | Seconds; `> 0` through `2,147,483.647`; no default and does not cancel the request. |
|
|
55
|
+
|
|
56
|
+
Returns the main agent's response as plain text.
|
|
57
|
+
|
|
58
|
+
A timeout, caller cancellation, or incoming main-agent request throws and stops only that wait, so the child may wait for the same request again.
|
|
59
|
+
|
|
60
|
+
The runtime interrupts an active child wait only after Pi RPC accepts the incoming main request for steering.
|
|
61
|
+
|
|
62
|
+
## `subagent_send`
|
|
63
|
+
|
|
64
|
+
Main and child processes receive separate provider-visible definitions for their own context.
|
|
65
|
+
|
|
66
|
+
### Main agent
|
|
67
|
+
|
|
68
|
+
| Parameter | Type | Required | Constraint / default |
|
|
69
|
+
| --- | --- | --- | --- |
|
|
70
|
+
| `recipient` | `string` | Conditional | Active job ID for a new request. |
|
|
71
|
+
| `requestId` | `string` | Conditional | Pending child request to answer. |
|
|
72
|
+
| `message` | `string` | Yes | Plain-text request or response, up to 48 KiB of UTF-8 text and 1,992 lines. |
|
|
73
|
+
|
|
74
|
+
Provide exactly one of `recipient` or `requestId`.
|
|
75
|
+
|
|
76
|
+
A new request provides an active queued or running job ID as `recipient` and omits `requestId`.
|
|
77
|
+
|
|
78
|
+
A response provides `requestId` and omits `recipient`.
|
|
79
|
+
|
|
80
|
+
### Subagent
|
|
81
|
+
|
|
82
|
+
| Parameter | Type | Required | Constraint / default |
|
|
83
|
+
| --- | --- | --- | --- |
|
|
84
|
+
| `requestId` | `string` | No | Pending main-agent request to answer; omit to start a new request to main. |
|
|
85
|
+
| `message` | `string` | Yes | Plain-text request or response, up to 48 KiB of UTF-8 text and 1,992 lines. |
|
|
86
|
+
|
|
87
|
+
A new request omits `requestId` and returns a request ID immediately for an optional `subagent_wait` call.
|
|
88
|
+
|
|
89
|
+
A response provides the pending main-agent `requestId`.
|
|
90
|
+
|
|
91
|
+
A main-originated request waits for the child RPC prompt to be accepted and then uses Pi steering to reach the running child.
|
|
92
|
+
|
|
93
|
+
After steering is queued, the runtime interrupts active child response waits without consuming their original requests.
|
|
94
|
+
|
|
95
|
+
Caller cancellation before RPC delivery starts rolls the request back.
|
|
96
|
+
|
|
97
|
+
Once RPC delivery starts, cancellation stops only the caller's wait; the request may still arrive and remains answerable until delivery fails or the job terminates.
|
|
98
|
+
|
|
99
|
+
A child response arrives asynchronously in the main session and interrupts the next active main-agent `subagent_wait`, including when the response arrived immediately before the wait started.
|
|
100
|
+
|
|
101
|
+
The first accepted response wins, and repeated responses acknowledge the existing response without replacing it.
|
|
102
|
+
|
|
103
|
+
Each job may have up to four unresolved or answered-but-not-consumed requests across both directions.
|
|
104
|
+
|
|
105
|
+
Requests and responses are limited to 1,992 lines so their protocol envelopes fit Pi's 2,000-line model-text bound.
|
|
106
|
+
|
|
107
|
+
Terminal jobs, unknown requests, cross-job responses, responses from the request originator, and stale session credentials throw.
|
|
108
|
+
|
|
109
|
+
A successful call returns `{ requestId, accepted, duplicate }`.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-subagents",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Subagent jobs with asynchronous main-agent messaging for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"pi",
|
|
12
12
|
"subagents",
|
|
13
13
|
"agents",
|
|
14
|
-
"
|
|
14
|
+
"delegation"
|
|
15
15
|
],
|
|
16
16
|
"files": [
|
|
17
17
|
"src",
|
|
@@ -25,37 +25,29 @@
|
|
|
25
25
|
"./dist/index.ts"
|
|
26
26
|
]
|
|
27
27
|
},
|
|
28
|
-
"piExtension": {
|
|
29
|
-
"lifecycle": "stable"
|
|
30
|
-
},
|
|
31
28
|
"scripts": {
|
|
32
29
|
"build": "node scripts/build-runtime.mjs",
|
|
33
30
|
"check": "npm run build && biome check . && npm run typecheck",
|
|
34
31
|
"format": "biome check --write .",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"prepack": "npm run build"
|
|
37
34
|
},
|
|
38
35
|
"peerDependencies": {
|
|
39
|
-
"@earendil-works/pi-agent-core": "*",
|
|
40
36
|
"@earendil-works/pi-ai": "*",
|
|
41
37
|
"@earendil-works/pi-coding-agent": "*",
|
|
42
38
|
"@earendil-works/pi-tui": "*",
|
|
43
39
|
"typebox": "*"
|
|
44
40
|
},
|
|
45
41
|
"devDependencies": {
|
|
46
|
-
"@biomejs/biome": "2.5.
|
|
47
|
-
"@earendil-works/pi-
|
|
48
|
-
"@earendil-works/pi-
|
|
49
|
-
"@earendil-works/pi-
|
|
50
|
-
"@
|
|
42
|
+
"@biomejs/biome": "2.5.11",
|
|
43
|
+
"@earendil-works/pi-ai": "0.84.4",
|
|
44
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
45
|
+
"@earendil-works/pi-tui": "0.84.4",
|
|
46
|
+
"@types/node": "26.4.0",
|
|
51
47
|
"esbuild": "0.28.2",
|
|
52
|
-
"typebox": "1.3.
|
|
48
|
+
"typebox": "1.3.20",
|
|
53
49
|
"typescript": "7.0.2"
|
|
54
50
|
},
|
|
55
|
-
"dependencies": {
|
|
56
|
-
"@narumitw/pi-tui-kit": "^0.59.0",
|
|
57
|
-
"proper-lockfile": "^4.1.2"
|
|
58
|
-
},
|
|
59
51
|
"repository": {
|
|
60
52
|
"type": "git",
|
|
61
53
|
"url": "https://github.com/narumiruna/pi-extensions",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { BrokerCredentials } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const BROKER_CREDENTIAL_FD = 3;
|
|
5
|
+
export const BROKER_CREDENTIAL_FD_ENV = "PI_SUBAGENT_BROKER_FD";
|
|
6
|
+
|
|
7
|
+
const MAX_CREDENTIAL_BYTES = 1_024;
|
|
8
|
+
|
|
9
|
+
export function brokerCredentialEnvironment(): NodeJS.ProcessEnv {
|
|
10
|
+
return { [BROKER_CREDENTIAL_FD_ENV]: String(BROKER_CREDENTIAL_FD) };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function serializeBrokerCredentials(credentials: BrokerCredentials): string {
|
|
14
|
+
return JSON.stringify(credentials);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function captureBrokerCredentials(
|
|
18
|
+
readCredentials: () => string = readCredentialPipe,
|
|
19
|
+
): BrokerCredentials | undefined {
|
|
20
|
+
const descriptor = process.env[BROKER_CREDENTIAL_FD_ENV];
|
|
21
|
+
delete process.env[BROKER_CREDENTIAL_FD_ENV];
|
|
22
|
+
if (descriptor === undefined) return undefined;
|
|
23
|
+
if (descriptor !== String(BROKER_CREDENTIAL_FD)) {
|
|
24
|
+
throw new Error("Invalid pi-subagents broker credential descriptor.");
|
|
25
|
+
}
|
|
26
|
+
let serialized: string;
|
|
27
|
+
try {
|
|
28
|
+
serialized = readCredentials();
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error("Unable to read pi-subagents broker credentials.");
|
|
31
|
+
}
|
|
32
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_CREDENTIAL_BYTES) {
|
|
33
|
+
throw new Error("Invalid pi-subagents broker credentials.");
|
|
34
|
+
}
|
|
35
|
+
let value: unknown;
|
|
36
|
+
try {
|
|
37
|
+
value = JSON.parse(serialized);
|
|
38
|
+
} catch {
|
|
39
|
+
throw new Error("Invalid pi-subagents broker credentials.");
|
|
40
|
+
}
|
|
41
|
+
if (!isBrokerCredentials(value)) {
|
|
42
|
+
throw new Error("Invalid pi-subagents broker credentials.");
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readCredentialPipe(): string {
|
|
48
|
+
try {
|
|
49
|
+
return fs.readFileSync(BROKER_CREDENTIAL_FD, "utf8");
|
|
50
|
+
} finally {
|
|
51
|
+
try {
|
|
52
|
+
fs.closeSync(BROKER_CREDENTIAL_FD);
|
|
53
|
+
} catch {
|
|
54
|
+
// The descriptor may already be closed after a failed read.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isBrokerCredentials(value: unknown): value is BrokerCredentials {
|
|
60
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
61
|
+
const candidate = value as Record<string, unknown>;
|
|
62
|
+
return (
|
|
63
|
+
Object.keys(candidate).length === 3 &&
|
|
64
|
+
candidate.host === "127.0.0.1" &&
|
|
65
|
+
Number.isSafeInteger(candidate.port) &&
|
|
66
|
+
(candidate.port as number) >= 1 &&
|
|
67
|
+
(candidate.port as number) <= 65_535 &&
|
|
68
|
+
typeof candidate.token === "string" &&
|
|
69
|
+
/^[a-f0-9]{64}$/u.test(candidate.token)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { captureBrokerCredentials } from "./broker-credentials.js";
|
|
4
|
+
import {
|
|
5
|
+
type ChildCommunicationClient,
|
|
6
|
+
createChildCommunicationExtension,
|
|
7
|
+
} from "./child-communication-tools.js";
|
|
8
|
+
import { MAX_FRAME_BYTES, MAX_IDENTIFIER_LENGTH } from "./message-broker.js";
|
|
9
|
+
import type { BrokerCredentials } from "./types.js";
|
|
10
|
+
|
|
11
|
+
const CONNECT_TIMEOUT_MS = 2_000;
|
|
12
|
+
const SEND_RESPONSE_TIMEOUT_MS = 5_000;
|
|
13
|
+
|
|
14
|
+
const captured = captureBrokerCredentials();
|
|
15
|
+
|
|
16
|
+
const childCommunicationBridge: ExtensionFactory = captured
|
|
17
|
+
? createChildCommunicationExtension(createBrokerClient(captured))
|
|
18
|
+
: () => undefined;
|
|
19
|
+
|
|
20
|
+
export default childCommunicationBridge;
|
|
21
|
+
|
|
22
|
+
export function createBrokerClient(credentials: BrokerCredentials): ChildCommunicationClient {
|
|
23
|
+
return {
|
|
24
|
+
async send(params, signal) {
|
|
25
|
+
const response = await requestBroker(
|
|
26
|
+
credentials,
|
|
27
|
+
{ type: "send", token: credentials.token, ...params },
|
|
28
|
+
signal,
|
|
29
|
+
SEND_RESPONSE_TIMEOUT_MS,
|
|
30
|
+
);
|
|
31
|
+
if (response.ok !== true) throw brokerError(response);
|
|
32
|
+
if (
|
|
33
|
+
typeof response.requestId !== "string" ||
|
|
34
|
+
!response.requestId ||
|
|
35
|
+
response.requestId.length > MAX_IDENTIFIER_LENGTH ||
|
|
36
|
+
typeof response.accepted !== "boolean" ||
|
|
37
|
+
typeof response.duplicate !== "boolean"
|
|
38
|
+
) {
|
|
39
|
+
throw new Error("Subagent broker returned an invalid send acknowledgement.");
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
requestId: response.requestId,
|
|
43
|
+
accepted: response.accepted,
|
|
44
|
+
duplicate: response.duplicate,
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
async wait(requestId, timeoutMs, signal) {
|
|
48
|
+
const response = await requestBroker(
|
|
49
|
+
credentials,
|
|
50
|
+
{
|
|
51
|
+
type: "wait",
|
|
52
|
+
token: credentials.token,
|
|
53
|
+
requestId,
|
|
54
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
55
|
+
},
|
|
56
|
+
signal,
|
|
57
|
+
);
|
|
58
|
+
if (response.ok !== true) throw brokerError(response);
|
|
59
|
+
if (typeof response.response !== "string") {
|
|
60
|
+
throw new Error("Subagent broker returned an invalid plain-text response.");
|
|
61
|
+
}
|
|
62
|
+
return response.response;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function requestBroker(
|
|
68
|
+
credentials: BrokerCredentials,
|
|
69
|
+
request: Record<string, unknown>,
|
|
70
|
+
signal?: AbortSignal,
|
|
71
|
+
responseTimeoutMs?: number,
|
|
72
|
+
): Promise<Record<string, unknown>> {
|
|
73
|
+
if (signal?.aborted) return Promise.reject(abortError("Subagent broker request was cancelled."));
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const socket = net.createConnection({ host: credentials.host, port: credentials.port });
|
|
76
|
+
let response = Buffer.alloc(0);
|
|
77
|
+
let settled = false;
|
|
78
|
+
let responseTimer: NodeJS.Timeout | undefined;
|
|
79
|
+
const connectTimer = setTimeout(
|
|
80
|
+
() => finish(new Error("Subagent broker connection timed out.")),
|
|
81
|
+
CONNECT_TIMEOUT_MS,
|
|
82
|
+
);
|
|
83
|
+
connectTimer.unref();
|
|
84
|
+
const onAbort = () => finish(abortError("Subagent broker request was cancelled."));
|
|
85
|
+
const finish = (error?: Error, value?: Record<string, unknown>) => {
|
|
86
|
+
if (settled) return;
|
|
87
|
+
settled = true;
|
|
88
|
+
clearTimeout(connectTimer);
|
|
89
|
+
if (responseTimer) clearTimeout(responseTimer);
|
|
90
|
+
signal?.removeEventListener("abort", onAbort);
|
|
91
|
+
socket.destroy();
|
|
92
|
+
if (error) reject(error);
|
|
93
|
+
else resolve(value ?? {});
|
|
94
|
+
};
|
|
95
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
96
|
+
socket.once("connect", () => {
|
|
97
|
+
clearTimeout(connectTimer);
|
|
98
|
+
if (responseTimeoutMs !== undefined) {
|
|
99
|
+
responseTimer = setTimeout(
|
|
100
|
+
() => finish(new Error("Subagent broker response timed out.")),
|
|
101
|
+
responseTimeoutMs,
|
|
102
|
+
);
|
|
103
|
+
responseTimer.unref();
|
|
104
|
+
}
|
|
105
|
+
socket.write(`${JSON.stringify(request)}\n`);
|
|
106
|
+
});
|
|
107
|
+
socket.on("data", (chunk: Buffer) => {
|
|
108
|
+
if (settled) return;
|
|
109
|
+
response = Buffer.concat([response, chunk]);
|
|
110
|
+
if (response.byteLength > MAX_FRAME_BYTES) {
|
|
111
|
+
finish(new Error("Subagent broker response exceeded its size limit."));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const newline = response.indexOf(0x0a);
|
|
115
|
+
if (newline < 0) return;
|
|
116
|
+
const trailing = response
|
|
117
|
+
.subarray(newline + 1)
|
|
118
|
+
.toString("utf8")
|
|
119
|
+
.trim();
|
|
120
|
+
if (trailing) {
|
|
121
|
+
finish(new Error("Subagent broker returned more than one response frame."));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(response.subarray(0, newline).toString("utf8")) as unknown;
|
|
126
|
+
if (!isRecord(parsed)) throw new Error();
|
|
127
|
+
finish(undefined, parsed);
|
|
128
|
+
} catch {
|
|
129
|
+
finish(new Error("Subagent broker returned malformed JSON."));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
socket.once("error", (error) => finish(error));
|
|
133
|
+
socket.once("close", () => {
|
|
134
|
+
if (!settled) finish(new Error("Subagent broker closed without a response."));
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function brokerError(response: Record<string, unknown>): Error {
|
|
140
|
+
return new Error(
|
|
141
|
+
typeof response.error === "string" ? response.error : "Subagent broker rejected the request.",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function abortError(message: string): Error {
|
|
146
|
+
const error = new Error(message);
|
|
147
|
+
error.name = "AbortError";
|
|
148
|
+
return error;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
152
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
153
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
import {
|
|
4
|
+
type BrokerSendAcknowledgement,
|
|
5
|
+
MAX_IDENTIFIER_LENGTH,
|
|
6
|
+
MAX_MESSAGE_BYTES,
|
|
7
|
+
sanitizeTerminalText,
|
|
8
|
+
validateMessage,
|
|
9
|
+
} from "./message-broker.js";
|
|
10
|
+
|
|
11
|
+
export const CHILD_COMMUNICATION_TOOL_NAMES = ["subagent_send", "subagent_wait"] as const;
|
|
12
|
+
|
|
13
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
14
|
+
const MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;
|
|
15
|
+
|
|
16
|
+
const SendParameters = Type.Object(
|
|
17
|
+
{
|
|
18
|
+
requestId: Type.Optional(
|
|
19
|
+
Type.String({
|
|
20
|
+
description: "Pending main-agent request to answer. Omit when starting a new request.",
|
|
21
|
+
minLength: 1,
|
|
22
|
+
maxLength: MAX_IDENTIFIER_LENGTH,
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
message: Type.String({
|
|
26
|
+
description: "Plain-text request or response. Maximum 48 KiB of UTF-8 text and 1,992 lines.",
|
|
27
|
+
minLength: 1,
|
|
28
|
+
maxLength: MAX_MESSAGE_BYTES,
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
{ additionalProperties: false },
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const WaitParameters = Type.Object(
|
|
35
|
+
{
|
|
36
|
+
requestId: Type.String({
|
|
37
|
+
description: "Request ID returned by subagent_send.",
|
|
38
|
+
minLength: 1,
|
|
39
|
+
maxLength: MAX_IDENTIFIER_LENGTH,
|
|
40
|
+
}),
|
|
41
|
+
timeout: Type.Optional(
|
|
42
|
+
Type.Number({ description: "Timeout in seconds (optional, no default timeout)" }),
|
|
43
|
+
),
|
|
44
|
+
},
|
|
45
|
+
{ additionalProperties: false },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
type ChildBrokerSendArguments =
|
|
49
|
+
| { recipient: "main"; message: string }
|
|
50
|
+
| { requestId: string; message: string };
|
|
51
|
+
|
|
52
|
+
type WaitArguments = Static<typeof WaitParameters>;
|
|
53
|
+
|
|
54
|
+
export interface ChildCommunicationClient {
|
|
55
|
+
send(params: ChildBrokerSendArguments, signal?: AbortSignal): Promise<BrokerSendAcknowledgement>;
|
|
56
|
+
wait(requestId: string, timeoutMs: number | undefined, signal?: AbortSignal): Promise<string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createChildCommunicationExtension(
|
|
60
|
+
client: ChildCommunicationClient,
|
|
61
|
+
): ExtensionFactory {
|
|
62
|
+
return (pi) => {
|
|
63
|
+
pi.registerTool({
|
|
64
|
+
name: "subagent_send",
|
|
65
|
+
label: "Subagent · Send to Main",
|
|
66
|
+
description:
|
|
67
|
+
"Use subagent_send to send one request to the main agent or answer one pending main-agent request. Omit requestId to start a request. Provide requestId to answer that request.",
|
|
68
|
+
promptSnippet: "Use subagent_send to send or answer one main-agent message",
|
|
69
|
+
parameters: SendParameters,
|
|
70
|
+
async execute(_toolCallId, params, signal) {
|
|
71
|
+
validateMessage(params.message, "Subagent message");
|
|
72
|
+
const requestId = optionalIdentifier(params.requestId, "requestId");
|
|
73
|
+
const acknowledgement = await client.send(
|
|
74
|
+
requestId === undefined
|
|
75
|
+
? { recipient: "main", message: params.message }
|
|
76
|
+
: { requestId, message: params.message },
|
|
77
|
+
signal,
|
|
78
|
+
);
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: "text" as const, text: JSON.stringify(acknowledgement) }],
|
|
81
|
+
details: acknowledgement,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
pi.registerTool({
|
|
87
|
+
name: "subagent_wait",
|
|
88
|
+
label: "Subagent · Wait for Main Agent",
|
|
89
|
+
description:
|
|
90
|
+
"Use subagent_wait with a request ID from subagent_send to wait for the main agent's plain-text response. A timeout, caller cancellation, or incoming main-agent request stops only this wait and does not cancel the original request. Retry the wait when its response is still needed.",
|
|
91
|
+
promptSnippet: "Use subagent_wait to receive a requested main-agent response",
|
|
92
|
+
parameters: WaitParameters,
|
|
93
|
+
prepareArguments: prepareWaitArguments,
|
|
94
|
+
async execute(_toolCallId, params, signal) {
|
|
95
|
+
const requestId = requiredIdentifier(params.requestId, "requestId");
|
|
96
|
+
const response = await client.wait(requestId, resolveTimeoutMs(params.timeout), signal);
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: "text" as const, text: sanitizeTerminalText(response) }],
|
|
99
|
+
details: { requestId },
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveTimeoutMs(timeout: number | undefined): number | undefined {
|
|
107
|
+
if (timeout === undefined) return undefined;
|
|
108
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
109
|
+
throw new Error("Invalid timeout: must be a finite number of seconds");
|
|
110
|
+
}
|
|
111
|
+
const timeoutMs = timeout * 1000;
|
|
112
|
+
if (timeoutMs > MAX_TIMEOUT_MS) {
|
|
113
|
+
throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
|
|
114
|
+
}
|
|
115
|
+
return timeoutMs;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function prepareWaitArguments(args: unknown): WaitArguments {
|
|
119
|
+
if (!args || typeof args !== "object") return args as WaitArguments;
|
|
120
|
+
if (!Object.hasOwn(args, "timeoutMs")) return args as WaitArguments;
|
|
121
|
+
const record = args as Record<string, unknown>;
|
|
122
|
+
if (typeof record.timeoutMs !== "number") return record as WaitArguments;
|
|
123
|
+
const { timeoutMs, ...prepared } = record;
|
|
124
|
+
if (prepared.timeout === undefined) {
|
|
125
|
+
return { ...prepared, timeout: timeoutMs / 1000 } as WaitArguments;
|
|
126
|
+
}
|
|
127
|
+
return prepared as WaitArguments;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function optionalIdentifier(value: unknown, field: string): string | undefined {
|
|
131
|
+
return value === undefined ? undefined : requiredIdentifier(value, field);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function requiredIdentifier(value: unknown, field: string): string {
|
|
135
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`Subagent ${field} is required.`);
|
|
136
|
+
const identifier = value.trim();
|
|
137
|
+
if (
|
|
138
|
+
identifier.length > MAX_IDENTIFIER_LENGTH ||
|
|
139
|
+
[...identifier].some((character) => {
|
|
140
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
141
|
+
return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
142
|
+
})
|
|
143
|
+
) {
|
|
144
|
+
throw new Error(`Subagent ${field} is invalid.`);
|
|
145
|
+
}
|
|
146
|
+
return identifier;
|
|
147
|
+
}
|