@synadia-ai/agent-fabric-pi 0.1.0 → 0.2.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/BUILD ADDED
@@ -0,0 +1,4 @@
1
+ plugin @synadia-ai/nats-pi-channel 0.5.6
2
+ synadia-agents 554414d
3
+ agent-fabric-sdk 00ca949
4
+ built 2026-10-11T06:27:09.359Z
package/README.md CHANGED
@@ -1,281 +1,61 @@
1
1
  # @synadia-ai/agent-fabric-pi
2
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.
3
+ pi on the Synadia Agent Fabric, in one package: the NATS channel for pi
4
+ (`@synadia-ai/nats-pi-channel`), the fabric's tracing and ScratchPad's
5
+ skill. Every pi session becomes an agent on the fabric: other agents find
6
+ it and prompt it, its model calls join the prompt's thread, and its own
7
+ prompts to other agents hang under the tool call that sent them.
15
8
 
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):
9
+ ## Install
83
10
 
84
11
  ```sh
85
- SYNADIA_PI_EXTENSIONS=/opt/fabric/node_modules/@synadia-ai/agent-fabric-pi pi …
12
+ pi install npm:@synadia-ai/agent-fabric-pi
86
13
  ```
87
14
 
88
- or in `nats-channel.json`, where an entry may carry options:
15
+ ## Connect
89
16
 
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.
17
+ The fabric's console creates the agent and hands out its NATS
18
+ credentials. pi reads the rest from its environment:
111
19
 
112
- ## ScratchPad
20
+ | Variable | What |
21
+ | --- | --- |
22
+ | `NATS_URL` | the fabric's NATS server |
23
+ | `NATS_CREDS` | the agent's credentials file: its identity |
24
+ | `NATS_SENDER_IDENTITY` | `signed`: the agent signs what it sends, which the fabric verifies |
25
+ | `SYNADIA_PI_NAME` | the agent's name, the last token of its address `agents.prompt.pi.<owner>.<name>` |
26
+ | `SYNADIA_OWNER` | the owner, the 4th token; defaults to `$USER` |
27
+ | `SCRATCHPAD_VOLUME`, `SCRATCHPAD_DIR` | optional: the agent's ScratchPad volume and its private workspace |
113
28
 
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.
29
+ `NATS_CONTEXT`, a NATS CLI context, wins over `NATS_URL` and `NATS_CREDS`.
121
30
 
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.
31
+ pi's model is pi's own setting. For the fabric to record the model calls,
32
+ point pi at the fabric's model proxy with the agent's model key, as a
33
+ custom provider in `~/.pi/agent/models.json`. The key can stay in the
34
+ environment: pi reads `"$NAME"` as the variable's value.
137
35
 
138
36
  ```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
37
+ {
38
+ "providers": {
39
+ "fabric": {
40
+ "baseUrl": "<the fabric's model proxy>/api/v1",
41
+ "api": "openai-completions",
42
+ "apiKey": "$FABRIC_VIRTUAL_KEY",
43
+ "models": [{ "id": "anthropic/claude-sonnet-5" }]
44
+ }
45
+ }
46
+ }
265
47
  ```
266
48
 
267
- ## License
49
+ Then `pi --model fabric/anthropic/claude-sonnet-5`, with `FABRIC_VIRTUAL_KEY`
50
+ set to the agent's model key. `/api/v1` is OpenRouter's path behind the
51
+ proxy; another provider has its own.
268
52
 
269
- Apache-2.0, like the fabric package. See [LICENSE](https://github.com/synadia-ai/agent-fabric-sdk/blob/main/plugins/pi/LICENSE).
53
+ ## The fabric add-on
270
54
 
271
- Short artifact references
272
- -------------------------
55
+ The package's pi extension loads the bundled add-on (`dist/fabric.js`)
56
+ unless `SYNADIA_PI_EXTENSIONS` or `SYNADIA_AGENT_EXTENSIONS` is set; set one
57
+ of them to name other extensions, or set it empty to run the plain channel.
58
+ The package's main export is the add-on.
273
59
 
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.
60
+ `BUILD` names the channel's version and the commits it was built from.
61
+ Apache-2.0.
@@ -0,0 +1,6 @@
1
+ // The NATS channel with this package's fabric add-on, unless an extension
2
+ // variable already names the extensions to load.
3
+ import { fileURLToPath } from "node:url";
4
+ if (process.env.SYNADIA_PI_EXTENSIONS === undefined && process.env.SYNADIA_AGENT_EXTENSIONS === undefined)
5
+ process.env.SYNADIA_PI_EXTENSIONS = fileURLToPath(new URL("./fabric.js", import.meta.url));
6
+ export { default } from "./nats-channel.js";