@synadia-ai/agent-fabric-pi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@synadia-ai/agent-fabric-pi` are recorded here. The
4
+ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
5
+ package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] — unreleased
8
+
9
+ ### Added
10
+
11
+ - The extension module for the PI channel plugin, named in `SYNADIA_PI_EXTENSIONS`: `fabricTracing()` on the plugin's service and agent-tools client.
12
+ - The `X-Synadia-Thread-ID` and `X-Synadia-Root-ID` headers on PI's own model calls, with each served request's scope bound at PI's events.
13
+ - The `409` loop guard in front of PI's handler.
14
+ - The edge subject setting: `options.edgeSubject`, `SYNADIA_FABRIC_EDGE_SUBJECT`, or `TRACE.edges`.
15
+ - ScratchPad: the shared skill with the sp command line for darwin and linux, its variables for `scripts/sp`, references in and out, and grant requests.
16
+
17
+ [0.1.0]: https://github.com/synadia-ai/agent-fabric-sdk/tree/main/plugins/pi
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for describing the origin of the Work and
141
+ reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Support. While redistributing the Work or
166
+ Derivative Works thereof, You may choose to offer, and charge a fee
167
+ for, acceptance of support, warranty, indemnity, or other liability
168
+ obligations and/or rights consistent with this License. However, in
169
+ accepting such obligations, You may act only on Your own behalf and
170
+ on Your sole responsibility, not on behalf of any other Contributor,
171
+ and only if You agree to indemnify, defend, and hold each Contributor
172
+ harmless for any liability incurred by, or claims asserted against,
173
+ such Contributor by reason of your accepting any such warranty or
174
+ additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Synadia Communications
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
200
+ implied. See the License for the specific language governing permissions
201
+ and limitations under the License.
package/README.md ADDED
@@ -0,0 +1,281 @@
1
+ # @synadia-ai/agent-fabric-pi
2
+
3
+ The Synadia Agent Fabric add-on for the PI channel plugin
4
+ (`@synadia-ai/nats-pi-channel`, `agents/pi` in
5
+ [`synadia-agents`](https://github.com/synadia-ai/synadia-agents)). One
6
+ extension module in the shape the plugin loads: named in
7
+ `SYNADIA_PI_EXTENSIONS`, it puts the fabric package's tracing on the
8
+ plugin's service and on the client its agent tools prompt through, the
9
+ fabric's two headers on PI's own model calls, and the `409` loop guard in
10
+ front of PI's handler. It also ships ScratchPad's skill with the sp command
11
+ line, so PI's model reads, publishes and shares ScratchPad content through
12
+ its bash tool. Without it the plugin is the plain plugin. The contract it
13
+ fills is the plugin's `agents/EXTENSIONS.md`; the spec is
14
+ [`docs/plugins.md`](https://github.com/synadia-ai/agent-fabric-sdk/blob/main/docs/plugins.md) §2, §3.1, §3.2 and §9.
15
+
16
+ ## What it does
17
+
18
+ The factory, the module's default export, calls `fabricTracing()` once and
19
+ hands the plugin its three hooks, so PI is on the wire exactly what an
20
+ SDK-built agent is:
21
+
22
+ - **On the service, per prompt:** the envelope's `thread_id` and `root_id`
23
+ are adopted, or a root minted when neither is there; a half pair or a
24
+ malformed id is refused with `400` before the handler runs; the handler
25
+ runs inside the trace scope.
26
+ - **On the client PI's agent tools prompt through, per prompt:** the child
27
+ thread is minted and the pair put into the envelope, and the signed `edge`
28
+ record is published on `TRACE.edges` before the prompt goes out, with the
29
+ model's tool-call ID, which the plugin passes to `execute()`. A delegation
30
+ PI's model makes hangs under its tool call in the console.
31
+ - **On the heartbeat:** `records_published` and `records_dropped`.
32
+
33
+ The harness piece is the scope. PI's loop does not run in the handler's
34
+ async context, so the add-on keeps each served request's scope by the
35
+ plugin's request id and binds it where the plugin says the request is being
36
+ worked on:
37
+
38
+ | Event | What the add-on does |
39
+ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
40
+ | `promptAccepted(request)` | keeps `activeTrace()`, the scope tracing bound for this request, by `request.id` |
41
+ | `aroundInject(request, run)` | runs `run` inside `bindActiveTrace(scope, …)`, which replaces the ambient context: the plugin injects the next queued prompt from the previous turn's settle hook, whose context is the previous request's |
42
+ | `providerHeaders(request)` | returns `X-Synadia-Thread-ID` and `X-Synadia-Root-ID` for the request, so the proxy files PI's own model calls on the served thread; each call counts against the scope, so an edge PI's model spawns later says how many calls came before it, retries included |
43
+ | `aroundToolCall(request, tool, run)` | binds the request's scope around `run`, so the edge of a `prompt_agent` PI's model calls is filed under the served thread even if PI's tool runner did not inherit the injected context |
44
+ | `promptEnded(request)` | forgets the request |
45
+
46
+ A request the add-on never saw (no scope kept) is left alone: no headers,
47
+ `run` called as it is.
48
+
49
+ **The `409` loop guard.** PI serves one turn at a time and cannot serve a
50
+ prompt while its model waits on a call to another agent, so a prompt of a
51
+ tree PI is waiting in is refused at once with `409` instead of queueing
52
+ behind the wait it is part of. PI waits from the moment its model sends a
53
+ prompt until every call the served request has open has settled: a
54
+ blocking `prompt_agent` until it returns, a call started with `wait: false`
55
+ for as long as it is open. With no call open, a prompt of the tree is
56
+ admitted. The guard is two more interceptors and a wrapper: its request
57
+ interceptor, after tracing's, keeps the served request by the scope tracing
58
+ bound for the handler's duration and refuses an arriving prompt whose root
59
+ a served request is waiting in, before `next()`, with
60
+ `RequestRejectedError(409, …)`; its prompt interceptor, on the plugin's
61
+ client, runs in the tool call's context and marks that request as waiting;
62
+ its `aroundToolCall`, inside the add-on's, lifts the mark when a tool
63
+ result counts no open call and no other agent-tool call of the request
64
+ runs. A call that fails or expires while the model runs no agent tool keeps
65
+ the mark until the next agent-tool result or the served prompt's end. The
66
+ root is read from the scope tracing bound, never from the envelope. Stated
67
+ honestly: this also refuses a legitimate second prompt of the same tree
68
+ that arrives while PI waits in it, such as a sibling delegation issued with
69
+ `wait: false` by the same coordinator; PI would have queued it anyway, and
70
+ the guard turns the queue into a refusal the caller sees at once.
71
+
72
+ **What it does not do.** No `served` record: PI's calls carry the headers,
73
+ so they need none, and PI writes nothing to `TRACE.edges` but the edges of
74
+ its own delegations. No `stopping`: an edge is published before the prompt
75
+ it describes and counted as it goes, so there is nothing to flush.
76
+
77
+ ## Naming it
78
+
79
+ Installed where PI runs, `npm install @synadia-ai/agent-fabric-pi`, and
80
+ named to the plugin by package name when that directory is the plugin's or
81
+ an ancestor of it, by absolute path otherwise (a package directory with
82
+ `dist/` built):
83
+
84
+ ```sh
85
+ SYNADIA_PI_EXTENSIONS=/opt/fabric/node_modules/@synadia-ai/agent-fabric-pi pi …
86
+ ```
87
+
88
+ or in `nats-channel.json`, where an entry may carry options:
89
+
90
+ ```json
91
+ {
92
+ "extensions": [
93
+ { "module": "@synadia-ai/agent-fabric-pi", "options": { "edgeSubject": "TRACE.edges" } }
94
+ ]
95
+ }
96
+ ```
97
+
98
+ `SYNADIA_PI_EXTENSIONS` wins over `SYNADIA_AGENT_EXTENSIONS`, which wins
99
+ over the config field. The plugin's connect line and `/nats-status` list
100
+ `extensions=agent-fabric` when the module is loaded.
101
+
102
+ ## The one setting
103
+
104
+ The edge subject: the entry's `options.edgeSubject`, else the variable
105
+ `SYNADIA_FABRIC_EDGE_SUBJECT`, else `TRACE.edges`. `null` in the options is
106
+ propagate-only: lineage forwarded, no record published, no counts on the
107
+ heartbeat. A subject that can never be published to (empty, an empty token,
108
+ whitespace, a wildcard, not a string) stops the factory; the plugin logs
109
+ `extension "…" not loaded: …` and starts plain, so the mistake is visible at
110
+ start. There is no `tracing` setting: the add-on's presence is the switch.
111
+
112
+ ## ScratchPad
113
+
114
+ The package carries ScratchPad's skill, adapted for an agent on the fabric,
115
+ in `skill/scratchpad/`: `SKILL.md`, `scripts/sp`, and `bin/` with the sp
116
+ command line for darwin and linux on arm64 and amd64. It is the one skill
117
+ every add-on ships, kept in `../scratchpad-skill` and installed here by
118
+ `npm run build:sp`. The model runs
119
+ `scripts/sp` through PI's bash tool; the harness's tools stay as the
120
+ operator configured them, and PI asks no permission for any command.
121
+
122
+ **The operator's step.** Start PI with the skill and the bash tool on:
123
+
124
+ ```sh
125
+ pi --mode rpc … \
126
+ -e <plugin>/extensions/nats-channel.ts \
127
+ --skill <add-on>/skill/scratchpad \
128
+ --tools bash,discover_agents,prompt_agent,answer_agent
129
+ ```
130
+
131
+ `--skill` loads the directory even under `--no-skills`. PI lists the skill
132
+ to the model only when its `bash` or `read` tool is on, so
133
+ `--no-builtin-tools` alone hides it; `--tools` naming `bash` and the
134
+ plugin's tools keeps them. Then name a volume for PI's identity, reserved
135
+ by the ScratchPad operator: the extension entry's `options.scratchpad`, or
136
+ the variable.
137
+
138
+ ```json
139
+ { "module": "@synadia-ai/agent-fabric-pi", "options": { "scratchpad": { "volume": "PiVol" } } }
140
+ ```
141
+
142
+ **Where the settings come from.** Once the service is up, the add-on
143
+ resolves each from the entry's `options.scratchpad` (`volume`, `dir`, `url`,
144
+ `creds`), else the variable (`SCRATCHPAD_VOLUME`, `SCRATCHPAD_DIR`,
145
+ `NATS_URL`, `NATS_CREDS`), else, for the URL and the credentials file, the
146
+ plugin's own NATS context (`NATS_CONTEXT`, or `context` in
147
+ `nats-channel.json`), which sp cannot read itself. The workspace defaults to
148
+ `scratchpad/` in the plugin's state directory (`~/.pi/agent/scratchpad`),
149
+ created if missing; keep it across restarts. Without a volume ScratchPad is
150
+ off; without a URL and a credentials file it is off with a warning.
151
+
152
+ **What `scripts/sp` gets.** The add-on sets these in PI's process, and PI's
153
+ bash tool passes them to every command (measured on PI 0.85.1):
154
+
155
+ | Variable | Set | `scripts/sp` adds |
156
+ | ------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------ |
157
+ | `SP_NATS_URL`, `SP_NATS_CREDS` | at start | `--url`, `--creds`, every call |
158
+ | `SCRATCHPAD_DIR` | at start | `--dir`, every call |
159
+ | `SCRATCHPAD_VOLUME` | at start | `--volume`, on `init` only: a read refuses a volume other than the reference's |
160
+ | `SP_CREATOR` | at start: PI's prompt address | `--creator`, on `publish-file` |
161
+ | `SP_THREAD_ID`, `SP_ROOT_ID` | per served prompt, from its hand-over to PI's loop to its end | `--thread-id`, `--root-id`, every call |
162
+ | `SP_CALLER_ACCOUNT`, `SP_CALLER_USER` | the same, for a verified caller | `--grantee-account`, `--grantee-user`, on `grant` and after `publish-file` |
163
+ | `SP_GRANTS_FILE` | at start: `<workspace>.grants` | nothing: it records there each grant sp confirmed, for grant requests |
164
+
165
+ The connection has names of its own, not `NATS_URL` and `NATS_CREDS`, which
166
+ a plugin may read as its own connection. The model passes none of these
167
+ flags; `scripts/sp` refuses them, and refuses a `grant` when no served
168
+ prompt has a verified caller, so PI grants read on what it publishes to the
169
+ agent that prompted it and to no other. A `publish-file` during a served
170
+ prompt with a verified caller also makes that grant, as an SDK-built agent
171
+ grants the caller its reply: the result carries `granted_to_caller`, or
172
+ `caller_grant_error`. Models skip a separate grant step often enough that
173
+ the publication makes it (`docs/plugins.md` §9.12).
174
+ On a platform sp is not built for, `scripts/sp` says so, in sp's own error
175
+ shape with exit 2. `SCRATCHPAD_BIN` names another sp binary.
176
+
177
+ **References in and out.** With ScratchPad on, the add-on registers PI as
178
+ reading references (`scratchpad_refs_ok: "true"` through the extension's
179
+ `metadata`), so discovery shows `reads_scratchpad_references: true` and a
180
+ caller's `prompt_agent` hands them to PI. And the references PI's model puts
181
+ into its own `prompt_agent` go through the fabric package's ScratchPad
182
+ extension, as an SDK-built agent's do: a receiver that reads references is
183
+ granted read, first-hand, right before the prompt goes out; any other gets
184
+ the text without them and the call's result says how many were removed; a
185
+ reference PI was neither given nor made is refused. A reference into PI's
186
+ own volume counts as made by PI, since its model publishes with
187
+ `scripts/sp`, which the extension does not see. The extension's own grants
188
+ run the packaged sp directly, with the settings above.
189
+
190
+ **Grant requests.** A caller PI handed a reference to may hand it on
191
+ through its own agent tools; its ScratchPad extension then asks the creator,
192
+ PI, to grant the receiver read (the grant request, `grant-request.ts` in
193
+ the fabric package). The add-on answers on PI's service, by the rule every
194
+ SDK-built agent answers by: the reference must point into PI's volume, the
195
+ requester must hold a live read grant PI made it first-hand, the grantee
196
+ must be another agent, and the grant is one path, read only, ten minutes at
197
+ most, recorded as made on request, which entitles no one to ask again. The
198
+ grants PI's model makes go through `scripts/sp`, which the add-on does not
199
+ see, so `scripts/sp` records each one sp confirms in `SP_GRANTS_FILE`
200
+ (`<workspace>.grants`), and the add-on counts a live one as first-hand: it
201
+ went to the agent whose prompt PI was serving.
202
+
203
+ ## Identity
204
+
205
+ Records are signed, so the plugin needs `senderIdentity: "signed"` with a
206
+ NATS context carrying PI's own credentials. With identity off the add-on
207
+ warns once at `started`, the fabric package warns once more when the first
208
+ record is due, every record owed counts as dropped on the heartbeat, and
209
+ PI's model calls still file under the caller's thread, since the headers
210
+ need no signature.
211
+
212
+ ## What the console shows
213
+
214
+ PI's model calls on the child thread the caller's edge created, PI the
215
+ executor, its depth `full`; the caller's `asked` row names PI. A delegation
216
+ PI's model makes is a grandchild under its tool call, PI's edge signed and
217
+ verified. No outcome or duration from a pair: PI publishes none, and the
218
+ thread's calls carry its timing.
219
+
220
+ ## Public API
221
+
222
+ | | |
223
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
224
+ | `default`, `fabricExtension(ctx, env?)` | the factory of the contract: resolves ScratchPad, then builds the extension; `env` is where the variables are read and ScratchPad's are set, `process.env` by default |
225
+ | `createFabricExtension(ctx, env?, scratchPad?)` | the same, synchronous, from settings already resolved |
226
+ | `resolveEdgeSubject(options, env?)` | the setting's precedence, as above |
227
+ | `SCRATCHPAD_OPTION`, `SP_VARS`, `ScratchPadHandOn`, `spBinary(env)` | ScratchPad's option key, the variables `scripts/sp` reads, the tool extension, the packaged sp's path |
228
+ | `EXTENSION_NAME`, `EDGE_SUBJECT_OPTION`, `EDGE_SUBJECT_VAR`, `IDENTITY_OFF_WARNING` | the constants |
229
+ | `loopGuard()`, `LOOP_GUARD_CODE`, `loopGuardDescription(rootId)` | the guard on its own: `requestInterceptor`, `promptInterceptor`, `aroundToolCall(scope, run)`, `serving(rootId)`, `waiting(rootId)` |
230
+ | `AgentExtensionContext`, `AgentExtension`, `HarnessEvents`, `ServedRequest`, `Outcome`, `AgentExtensionHandles`, `AgentExtensionSettings`, `Harness`, `AgentExtensionFactory` | the plugin's contract, declared structurally so this package imports nothing of the plugin or of PI |
231
+
232
+ The setting's precedence, the loop guard and the contract's types live in
233
+ `@synadia-ai/agent-fabric`, shared by every add-on under `plugins/`; this
234
+ package re-exports them, the contract's `PluginHarness` as `Harness`.
235
+
236
+ ## Development
237
+
238
+ Built like the fabric package (tsup, ESM and CJS, vitest, eslint,
239
+ prettier). It links `@synadia-ai/agent-fabric` at `file:../../typescript`,
240
+ built first, and the two protocol SDK packages by the same `file:` links
241
+ the fabric package uses; its `package-lock.json` is committed, and a clean clone
242
+ installs with `npm ci`. The tests: unit, a fake of the plugin driving the events
243
+ (`test/support/fake-plugin.ts`); integration, the factory's hooks on a real
244
+ `AgentService` and `Agents` over a local `nats-server` in operator mode,
245
+ the edges on the wire validated against `test-fixtures/schemas/edge.json`.
246
+
247
+ `npm run build:sp` installs the skill from `../scratchpad-skill` into
248
+ `skill/scratchpad`. It downloads the four published binaries pinned in
249
+ `../scratchpad-skill/release.json` and verifies their archive and executable
250
+ hashes with Node 20+. `npm pack` runs it too (`prepack`). `skill/` is
251
+ gitignored. No Go compiler or service checkout is required. The
252
+ [binary guide](https://github.com/synadia-ai/agent-fabric-sdk/blob/main/docs/scratchpad-binaries.md) covers authenticated
253
+ downloads, offline archives, cache checks, and the included licenses.
254
+ `test/unit/scripts-sp.test.ts` drives the shared `scripts/sp` with fake
255
+ binaries and a fake `uname`, and runs the real binary for this machine
256
+ through the installed copy when it is built.
257
+
258
+ ```sh
259
+ (cd ../../typescript && npm install && npm run build)
260
+ npm install
261
+ npm run build:sp
262
+ npm run typecheck && npm run lint && npm run format:check
263
+ npm test
264
+ npm run build
265
+ ```
266
+
267
+ ## License
268
+
269
+ Apache-2.0, like the fabric package. See [LICENSE](https://github.com/synadia-ai/agent-fabric-sdk/blob/main/plugins/pi/LICENSE).
270
+
271
+ Short artifact references
272
+ -------------------------
273
+
274
+ The shared wrapper and grant records accept `sp-r1:` short tokens and existing `sp-ref:` tokens.
275
+ Publication uses the short form when the service and the selected client support it.
276
+ Short-token reads and resolution require a compatible `sp` binary selected through
277
+ `SCRATCHPAD_BIN`. The bundled CLI release is pinned independently; this change
278
+ does not advance that pin.
279
+ Copy the returned token unchanged into prompts and replies.
280
+ The token survives client restarts; each content read still requires current authorization.
281
+ See the [native client contract](https://github.com/synadia-ai/agent-fabric-sdk/blob/main/docs/scratchpad-native.md) for recovery and capability fallback.
package/dist/index.cjs ADDED
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var agentFabric = require('@synadia-ai/agent-fabric');
6
+ var url = require('url');
7
+
8
+ // node_modules/tsup/assets/cjs_shims.js
9
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
10
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
11
+ var SKILL_DIR = url.fileURLToPath(new URL("../skill/scratchpad", importMetaUrl));
12
+ function spBinary(env) {
13
+ return agentFabric.spBinary(env, SKILL_DIR);
14
+ }
15
+ function spPort(settings, creator, env) {
16
+ return agentFabric.spPort(settings, creator, env, SKILL_DIR);
17
+ }
18
+
19
+ // src/extension.ts
20
+ var EXTENSION_NAME = "agent-fabric";
21
+ var IDENTITY_OFF_WARNING = `${EXTENSION_NAME}: senderIdentity is off; trace records are signed, so none will be published and each counts as dropped on the heartbeat. Set senderIdentity to signed.`;
22
+ async function fabricExtension(ctx, env = process.env) {
23
+ return createFabricExtension(ctx, env, await agentFabric.resolveScratchPad(ctx, env));
24
+ }
25
+ function createFabricExtension(ctx, env = process.env, scratchPad) {
26
+ const edgeSubject = agentFabric.resolveEdgeSubject(ctx.options, env);
27
+ const tracing = agentFabric.fabricTracing({ edgeSubject });
28
+ const guard = agentFabric.loopGuard();
29
+ const log = ctx.logger;
30
+ const scopes = /* @__PURE__ */ new Map();
31
+ const scopeOf = (request) => request === void 0 ? void 0 : scopes.get(request.id);
32
+ let warnedIdentityOff = false;
33
+ const scratchPadOn = scratchPad !== void 0;
34
+ const promptEnv = new agentFabric.ServedPromptEnv(env);
35
+ if (scratchPad !== void 0) agentFabric.exportScratchPad(scratchPad, env);
36
+ const handOn = scratchPad !== void 0 ? new agentFabric.ScratchPadHandOn() : void 0;
37
+ return {
38
+ name: EXTENSION_NAME,
39
+ // The service: tracing first, so the guard runs inside the scope tracing
40
+ // bound and a malformed pair is refused with 400 before the guard
41
+ // looks; the plugin appends its own (the tools' scope) after these.
42
+ // The ScratchPad extension's, last, remembers the references each
43
+ // served prompt brings, which PI may hand on.
44
+ requestInterceptors: [
45
+ tracing.requestInterceptor,
46
+ guard.requestInterceptor,
47
+ ...handOn !== void 0 ? [handOn.requestInterceptor] : []
48
+ ],
49
+ // The client PI's agent tools prompt through: the edge and the pair,
50
+ // then the waiting mark. Both read the ambient scope, so the order
51
+ // between them does not matter.
52
+ promptInterceptors: [tracing.promptInterceptor, guard.promptInterceptor],
53
+ heartbeatExtras: tracing.heartbeatExtras,
54
+ ...handOn !== void 0 ? {
55
+ // References in PI's `prompt_agent`: granted to a receiver that
56
+ // reads them, removed with a note for any other.
57
+ toolExtensions: [handOn],
58
+ // So a caller's `prompt_agent` hands references to PI.
59
+ metadata: { [agentFabric.READS_REFERENCES_KEY]: "true" }
60
+ } : {},
61
+ // No `stopping`: an
62
+ // edge record is published before the prompt it describes and counted
63
+ // as it goes, so there is nothing to flush when the plugin stops.
64
+ started({ agents, service }) {
65
+ if (ctx.settings.senderIdentity !== "signed" && !warnedIdentityOff) {
66
+ warnedIdentityOff = true;
67
+ log.warn(IDENTITY_OFF_WARNING);
68
+ }
69
+ log.info(
70
+ `${EXTENSION_NAME}: tracing ${service.subject.prompt}` + (edgeSubject === null ? " (propagate-only: lineage forwarded, no edge record published)" : `, edge records on ${edgeSubject}`)
71
+ );
72
+ if (scratchPad !== void 0 && handOn !== void 0) {
73
+ env[agentFabric.SP_VARS.creator] = service.subject.prompt;
74
+ const port = spPort(scratchPad, service.subject.prompt, env);
75
+ if (port !== void 0) handOn.start({ scratchPad: port, agents, service, logger: log });
76
+ else
77
+ log.warn(
78
+ `${EXTENSION_NAME}: no sp for this platform (set SCRATCHPAD_BIN); references in prompt_agent are not handed on, and grant requests not answered`
79
+ );
80
+ log.info(
81
+ `${EXTENSION_NAME}: ScratchPad volume ${scratchPad.volume}` + (scratchPad.dir !== void 0 ? `, workspace ${scratchPad.dir}` : ", no workspace") + ", for the skill's scripts/sp"
82
+ );
83
+ }
84
+ },
85
+ events: {
86
+ promptAccepted(request) {
87
+ const scope = agentFabric.activeTrace();
88
+ if (scope !== void 0) scopes.set(request.id, scope);
89
+ },
90
+ promptEnded(request) {
91
+ scopes.delete(request.id);
92
+ promptEnv.leave(request);
93
+ },
94
+ aroundInject(request, run) {
95
+ const scope = scopes.get(request.id);
96
+ if (scratchPadOn) promptEnv.enter(request, scope);
97
+ return scope === void 0 ? run() : agentFabric.bindActiveTrace(scope, run);
98
+ },
99
+ providerHeaders(request) {
100
+ const scope = scopes.get(request.id);
101
+ return scope === void 0 ? void 0 : agentFabric.bindActiveTrace(scope, agentFabric.traceHeaders);
102
+ },
103
+ aroundToolCall(request, _toolName, run) {
104
+ const scope = scopeOf(request);
105
+ return scope === void 0 ? run() : agentFabric.bindActiveTrace(scope, () => guard.aroundToolCall(scope, run));
106
+ }
107
+ }
108
+ };
109
+ }
110
+
111
+ Object.defineProperty(exports, "EDGE_SUBJECT_OPTION", {
112
+ enumerable: true,
113
+ get: function () { return agentFabric.EDGE_SUBJECT_OPTION; }
114
+ });
115
+ Object.defineProperty(exports, "EDGE_SUBJECT_VAR", {
116
+ enumerable: true,
117
+ get: function () { return agentFabric.EDGE_SUBJECT_VAR; }
118
+ });
119
+ Object.defineProperty(exports, "LOOP_GUARD_CODE", {
120
+ enumerable: true,
121
+ get: function () { return agentFabric.LOOP_GUARD_CODE; }
122
+ });
123
+ Object.defineProperty(exports, "SCRATCHPAD_OPTION", {
124
+ enumerable: true,
125
+ get: function () { return agentFabric.SCRATCHPAD_OPTION; }
126
+ });
127
+ Object.defineProperty(exports, "SP_VARS", {
128
+ enumerable: true,
129
+ get: function () { return agentFabric.SP_VARS; }
130
+ });
131
+ Object.defineProperty(exports, "ScratchPadHandOn", {
132
+ enumerable: true,
133
+ get: function () { return agentFabric.ScratchPadHandOn; }
134
+ });
135
+ Object.defineProperty(exports, "loopGuard", {
136
+ enumerable: true,
137
+ get: function () { return agentFabric.loopGuard; }
138
+ });
139
+ Object.defineProperty(exports, "loopGuardDescription", {
140
+ enumerable: true,
141
+ get: function () { return agentFabric.loopGuardDescription; }
142
+ });
143
+ Object.defineProperty(exports, "resolveEdgeSubject", {
144
+ enumerable: true,
145
+ get: function () { return agentFabric.resolveEdgeSubject; }
146
+ });
147
+ exports.EXTENSION_NAME = EXTENSION_NAME;
148
+ exports.IDENTITY_OFF_WARNING = IDENTITY_OFF_WARNING;
149
+ exports.createFabricExtension = createFabricExtension;
150
+ exports.default = fabricExtension;
151
+ exports.fabricExtension = fabricExtension;
152
+ exports.spBinary = spBinary;
153
+ //# sourceMappingURL=index.cjs.map
154
+ //# sourceMappingURL=index.cjs.map