nemo-fabric-adapters-pi 0.4.0-beta.2 → 0.4.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 +140 -6
- package/dist/pi-sdk.d.ts +9 -0
- package/dist/pi-sdk.js +158 -60
- package/dist/relay-config.d.ts +35 -0
- package/dist/relay-config.js +513 -0
- package/dist/relay-gateway.d.ts +40 -0
- package/dist/relay-gateway.js +233 -0
- package/dist/relay.d.ts +50 -0
- package/dist/relay.js +248 -0
- package/dist/runtime.d.ts +6 -0
- package/dist/runtime.js +78 -10
- package/package.json +7 -7
- package/pi.fabric-adapter.json +23 -0
package/README.md
CHANGED
|
@@ -22,16 +22,24 @@ The adapter supports:
|
|
|
22
22
|
- Explicit local `.ts` or `.js` extension files contained by the NeMo Fabric
|
|
23
23
|
workspace
|
|
24
24
|
- Slash commands registered by those explicit extensions
|
|
25
|
+
- NeMo Relay 0.9 telemetry through a runtime-owned gateway and an explicitly
|
|
26
|
+
configured Relay Pi extension
|
|
27
|
+
- Live model-turn ATOF records for successful Relay redirects through the
|
|
28
|
+
default embedded NeMo Fabric collector. Startup `model_redirect` marks remain
|
|
29
|
+
in the configured Relay ATOF artifacts and are not included in per-invocation
|
|
30
|
+
`invoke_stream()` records
|
|
25
31
|
- Ordered plain-text invocations with a `{ "response": "..." }` terminal
|
|
26
|
-
output
|
|
32
|
+
output, Relay runtime details, and collected ATOF artifacts
|
|
27
33
|
|
|
28
34
|
Ambient Pi settings, context files, packages, extensions, skills, prompts,
|
|
29
35
|
themes, model files, credentials, and session files are disabled. Explicitly
|
|
30
|
-
configured extensions are trusted code.
|
|
36
|
+
configured extensions are trusted code. The adapter raises the compaction
|
|
37
|
+
reserve toward the selected model's maximum output while retaining at least
|
|
38
|
+
half of the context window for input.
|
|
31
39
|
|
|
32
40
|
## Install the Adapter
|
|
33
41
|
|
|
34
|
-
Pi 0.
|
|
42
|
+
Pi 0.86.x requires Node.js 22.19.0 or newer.
|
|
35
43
|
|
|
36
44
|
### Install for Consumers
|
|
37
45
|
|
|
@@ -40,7 +48,7 @@ then install the compatible Pi SDK harness version selected by that project:
|
|
|
40
48
|
|
|
41
49
|
```bash
|
|
42
50
|
npm install nemo-fabric-adapters-pi
|
|
43
|
-
npm install @earendil-works/pi-ai@^0.
|
|
51
|
+
npm install @earendil-works/pi-ai@^0.86.0 @earendil-works/pi-coding-agent@^0.86.0
|
|
44
52
|
```
|
|
45
53
|
|
|
46
54
|
The adapter declares the Pi packages as optional peers. Installing the adapter
|
|
@@ -65,6 +73,27 @@ just build-typescript
|
|
|
65
73
|
The full build installs its own dependencies, so you do not need to run
|
|
66
74
|
`just install-typescript-pi` first.
|
|
67
75
|
|
|
76
|
+
### Install NeMo Relay
|
|
77
|
+
|
|
78
|
+
Relay-enabled Pi runs require `nemo-relay>=0.9.0,<0.10.0` on `PATH`. Install it
|
|
79
|
+
separately from the npm adapter:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install "nemo-relay-cli-bin>=0.9.0,<0.10.0"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
To select a specific Relay executable instead of relying on `PATH`, set
|
|
86
|
+
`FABRIC_NEMO_RELAY_COMMAND` to its absolute path:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
FABRIC_NEMO_RELAY_COMMAND="/absolute/path/to/nemo-relay" uv run python your_app.py
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The adapter does not bundle the Relay Pi extension. Obtain the
|
|
93
|
+
[`crates/cli/assets/pi-extension`](https://github.com/NVIDIA/NeMo-Relay/tree/0.9.0/crates/cli/assets/pi-extension)
|
|
94
|
+
directory from the Relay 0.9 release and configure its path as described in the
|
|
95
|
+
next section.
|
|
96
|
+
|
|
68
97
|
## Configure the Adapter
|
|
69
98
|
|
|
70
99
|
The npm package includes its adapter descriptor as `pi.fabric-adapter.json`.
|
|
@@ -85,6 +114,95 @@ harness = HarnessConfig(adapter_id="nvidia.fabric.pi")
|
|
|
85
114
|
For a source build, set `discovery.local_paths` to
|
|
86
115
|
`adapters/typescript/pi/pi.fabric-adapter.json` instead.
|
|
87
116
|
|
|
117
|
+
## Configure NeMo Relay
|
|
118
|
+
|
|
119
|
+
Enable Relay with the standard NeMo Fabric configuration and provide the Relay
|
|
120
|
+
Pi extension as an adapter setting:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
config.runtime.artifacts = "./artifacts/pi"
|
|
124
|
+
config.enable_relay(output_dir="./artifacts/relay")
|
|
125
|
+
config.harness.settings["relay_extension_path"] = (
|
|
126
|
+
"/path/to/NeMo-Relay/crates/cli/assets/pi-extension"
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Relay requires `runtime.artifacts` so NeMo Fabric can create the runtime-owned
|
|
131
|
+
configuration passed to the adapter. The extension path can be absolute or
|
|
132
|
+
relative to `environment.workspace`. It can identify a JavaScript or TypeScript
|
|
133
|
+
file or a Pi extension package directory. Unlike user-configured Pi extensions,
|
|
134
|
+
the Relay extension does not need to remain inside `environment.workspace` when
|
|
135
|
+
an absolute path is used.
|
|
136
|
+
|
|
137
|
+
When the runtime starts, the adapter validates the Relay 0.9 CLI, writes an
|
|
138
|
+
explicit `plugins.toml`, starts a loopback gateway, and loads the extension into
|
|
139
|
+
the isolated Pi session. The result includes `relay_runtime` and
|
|
140
|
+
`relay_artifacts` in `output`. The gateway can produce ATOF, ATIF,
|
|
141
|
+
OpenTelemetry, and OpenInference output from the Relay observability
|
|
142
|
+
configuration.
|
|
143
|
+
|
|
144
|
+
The adapter supports one Relay-enabled Pi runtime per adapter process because
|
|
145
|
+
the Relay 0.9 extension receives its gateway and upstream configuration through
|
|
146
|
+
process environment variables. NeMo Fabric starts each runtime in a separate
|
|
147
|
+
adapter process, so concurrent NeMo Fabric runtimes remain isolated. Direct
|
|
148
|
+
embedders must likewise place concurrent Relay-enabled Pi runtimes in separate
|
|
149
|
+
processes.
|
|
150
|
+
|
|
151
|
+
Local ATIF trajectories are finalized only after the Pi session closes and are
|
|
152
|
+
therefore not included in `relay_artifacts`. The adapter does not wait for local
|
|
153
|
+
ATIF during invocation; it collects per-invocation artifacts such as ATOF and
|
|
154
|
+
returns the result. After runtime shutdown, retrieve the finalized file directly
|
|
155
|
+
from the ATIF output directory. For the configuration above and the default
|
|
156
|
+
filename template, it is written to
|
|
157
|
+
`./artifacts/relay/<runtime_id>/trajectory-<session_id>.atif.json`. Invocation
|
|
158
|
+
results do not prevent subsequent turns.
|
|
159
|
+
|
|
160
|
+
Session, turn, and tool telemetry does not depend on model redirection. Model
|
|
161
|
+
telemetry is available only when Relay supports the selected model API and the
|
|
162
|
+
gateway upstream matches the model endpoint. Relay records a skipped redirect
|
|
163
|
+
as a `model_redirect` mark with the reason in configured ATOF artifacts. Pi
|
|
164
|
+
emits its startup redirect marks before NeMo Fabric registers an invocation, so
|
|
165
|
+
they are not included in the per-invocation records from `invoke_stream()`.
|
|
166
|
+
|
|
167
|
+
Install the matching collector for the embedded streaming path:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
pip install "nemo-fabric[streaming]"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Start the runtime with streaming enabled to consume live model-turn ATOF
|
|
174
|
+
records for successful Relay redirects in one Pi invocation:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from nemo_fabric import Fabric
|
|
178
|
+
|
|
179
|
+
async with await Fabric().start_runtime(config, streaming=True) as runtime:
|
|
180
|
+
stream = runtime.invoke_stream(input="Review the latest patch")
|
|
181
|
+
async for record in stream:
|
|
182
|
+
print(record)
|
|
183
|
+
result = await stream.result()
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The terminal `RunResult` remains separate from the ATOF records. Fully consume
|
|
187
|
+
each stream, or call `await stream.aclose()` if iteration stops early, before
|
|
188
|
+
starting another invocation; the same runtime can then alternate
|
|
189
|
+
`invoke_stream()` and `invoke()` calls. The embedded collector serializes both
|
|
190
|
+
methods behind one Pi invocation lease. Streaming capture begins at the first
|
|
191
|
+
Pi `turn_start` and closes at `agent_settled`. If Relay output is interrupted or
|
|
192
|
+
late, the collector waits for a bounded interval, then admits the next native
|
|
193
|
+
invocation and uses Pi's cumulative turn count to discard ambiguous delayed
|
|
194
|
+
records until a higher `turn_start` arrives. This can thin the ATOF stream but
|
|
195
|
+
does not block later invocations. Increase `completion_wait_timeout` from its
|
|
196
|
+
one-second default when Relay delivery can take longer. Use the default embedded
|
|
197
|
+
collector for Pi streaming. The Pi extension does not attach NeMo Fabric
|
|
198
|
+
request IDs, so
|
|
199
|
+
`start_runtime(..., streaming=True, launch_collector=False)` cannot correlate
|
|
200
|
+
its records through an externally managed collector.
|
|
201
|
+
|
|
202
|
+
This Relay-backed path runs the adapter's ordinary `invoke` operation. It is
|
|
203
|
+
independent of native OpenAI streaming, so the adapter descriptor's
|
|
204
|
+
`capabilities.streaming` value remains `false`.
|
|
205
|
+
|
|
88
206
|
## Custom Tool Modules
|
|
89
207
|
|
|
90
208
|
The adapter accepts custom tools with `kind: "module"`. The `ref` is a
|
|
@@ -125,7 +243,23 @@ TypeScript packages, inspect the plan from the repository root:
|
|
|
125
243
|
|
|
126
244
|
Refer to the
|
|
127
245
|
[code-review example](../../../examples/code_review_agent/README.md) for the
|
|
128
|
-
live NVIDIA-backed run command. Relay
|
|
246
|
+
live NVIDIA-backed run command. For a Relay-enabled Pi run, pass the extension
|
|
247
|
+
path explicitly:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
.venv/bin/python -m examples.code_review_agent \
|
|
251
|
+
--variant pi \
|
|
252
|
+
--relay \
|
|
253
|
+
--stream \
|
|
254
|
+
--pi-relay-extension-path /path/to/NeMo-Relay/crates/cli/assets/pi-extension \
|
|
255
|
+
--input "Review calculator.py"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The command collects per-invocation model-turn ATOF records for successful Relay
|
|
259
|
+
redirects, then prints one JSON document containing `atof_records` and the
|
|
260
|
+
separate terminal `result`. Redirect-decision marks remain in configured Relay
|
|
261
|
+
ATOF artifacts; Pi's startup marks are not included in `atof_records`. MCP is
|
|
262
|
+
not currently supported.
|
|
129
263
|
|
|
130
264
|
## Dependency Rationale
|
|
131
265
|
|
|
@@ -135,7 +269,7 @@ process; maintaining a second JSON-RPC translation was rejected for the bundled
|
|
|
135
269
|
adapter. `@earendil-works/pi-ai` supplies Pi's model catalog and credential
|
|
136
270
|
store, which the coding-agent SDK expects. Both packages are optional peer
|
|
137
271
|
dependencies so deployments control the compatible harness version. Exact
|
|
138
|
-
0.
|
|
272
|
+
0.86.0 development dependencies keep repository builds and tests reproducible.
|
|
139
273
|
|
|
140
274
|
`jiti` loads explicitly configured, trusted JavaScript and TypeScript tool
|
|
141
275
|
modules. Native Node.js loading cannot execute TypeScript modules, while a
|
package/dist/pi-sdk.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AgentToolDefinition } from "nemo-fabric-adapter-contract";
|
|
3
3
|
import { type AdapterStartInput } from "nemo-fabric-adapters-common";
|
|
4
|
+
import { type PiRelayControllerFactory } from "./relay.js";
|
|
4
5
|
import type { PiSessionFactory, PiSessionHandle } from "./runtime.js";
|
|
6
|
+
export declare function withCustomBaseUrl<T extends {
|
|
7
|
+
api: string;
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
compat?: object;
|
|
10
|
+
}>(catalogModel: T, baseUrl: string | null | undefined, overrideBaseUrl?: boolean): T;
|
|
11
|
+
export declare function modelAwareCompactionReserveTokens(configuredReserveTokens: number, maxOutputTokens: number, contextWindow: number): number;
|
|
5
12
|
export declare function resolveCustomTools(workspace: string, definitions: Record<string, AgentToolDefinition>): Promise<ToolDefinition[]>;
|
|
6
13
|
export declare class PiSdkSessionFactory implements PiSessionFactory {
|
|
14
|
+
private readonly relayFactory;
|
|
15
|
+
constructor(relayFactory?: PiRelayControllerFactory);
|
|
7
16
|
create(input: AdapterStartInput): Promise<PiSessionHandle>;
|
|
8
17
|
}
|
package/dist/pi-sdk.js
CHANGED
|
@@ -7,9 +7,32 @@ import { realpath, stat } from "node:fs/promises";
|
|
|
7
7
|
import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
8
|
import { createJiti } from "jiti/static";
|
|
9
9
|
import { LifecycleError } from "nemo-fabric-adapters-common";
|
|
10
|
+
import { PiRelayFactory, } from "./relay.js";
|
|
11
|
+
export function withCustomBaseUrl(catalogModel, baseUrl, overrideBaseUrl = true) {
|
|
12
|
+
if (!baseUrl) {
|
|
13
|
+
return catalogModel;
|
|
14
|
+
}
|
|
15
|
+
const model = overrideBaseUrl ? { ...catalogModel, baseUrl } : catalogModel;
|
|
16
|
+
if (catalogModel.api !== "openai-completions") {
|
|
17
|
+
return model;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
...model,
|
|
21
|
+
// Generic OpenAI-compatible proxies may reject provider-specific
|
|
22
|
+
// reasoning_content fields when Pi replays an assistant tool call.
|
|
23
|
+
compat: { ...catalogModel.compat, requiresThinkingAsText: true },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function modelAwareCompactionReserveTokens(configuredReserveTokens, maxOutputTokens, contextWindow) {
|
|
27
|
+
const reserveTokens = Math.max(configuredReserveTokens, maxOutputTokens);
|
|
28
|
+
if (contextWindow <= 0) {
|
|
29
|
+
return reserveTokens;
|
|
30
|
+
}
|
|
31
|
+
return Math.min(reserveTokens, Math.floor(contextWindow / 2));
|
|
32
|
+
}
|
|
10
33
|
const PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
|
|
11
34
|
const TOOL_MODULE_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"]);
|
|
12
|
-
const PI_HARNESS_INSTALL_COMMAND = "npm install @earendil-works/pi-ai@^0.
|
|
35
|
+
const PI_HARNESS_INSTALL_COMMAND = "npm install @earendil-works/pi-ai@^0.86.0 @earendil-works/pi-coding-agent@^0.86.0";
|
|
13
36
|
function isMissingModuleError(error) {
|
|
14
37
|
return (typeof error === "object" &&
|
|
15
38
|
error !== null &&
|
|
@@ -163,11 +186,6 @@ export async function resolveCustomTools(workspace, definitions) {
|
|
|
163
186
|
}
|
|
164
187
|
return tools;
|
|
165
188
|
}
|
|
166
|
-
function extensionToolNames(resourceLoader) {
|
|
167
|
-
return resourceLoader
|
|
168
|
-
.getExtensions()
|
|
169
|
-
.extensions.flatMap((extension) => Array.from(extension.tools.keys()));
|
|
170
|
-
}
|
|
171
189
|
function rejectToolCollisions(customTools, configuredExtensionTools) {
|
|
172
190
|
const customNames = new Set(customTools.map((tool) => tool.name));
|
|
173
191
|
const seenExtensionNames = new Set();
|
|
@@ -180,6 +198,19 @@ function rejectToolCollisions(customTools, configuredExtensionTools) {
|
|
|
180
198
|
seenExtensionNames.add(name);
|
|
181
199
|
}
|
|
182
200
|
}
|
|
201
|
+
function relayExtensionLoadErrors(errors, extensions, relay, workspace) {
|
|
202
|
+
if (relay === undefined) {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
const loaded = extensions.some((extension) => [extension.path, extension.resolvedPath].some((path) => path !== undefined && resolve(workspace, path) === relay.extensionPath));
|
|
206
|
+
if (loaded) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
return errors.filter((error) => {
|
|
210
|
+
const path = resolve(workspace, error.path);
|
|
211
|
+
return path === relay.extensionPath || containedBy(relay.extensionPath, path);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
183
214
|
async function resolveExtensionPaths(workspace, configured) {
|
|
184
215
|
const resolved = [];
|
|
185
216
|
for (const entry of configured) {
|
|
@@ -259,17 +290,33 @@ function unsupportedSessionAction(name) {
|
|
|
259
290
|
throw new LifecycleError("pi_unsupported_session_operation", `Pi session operation ${name} is not supported`);
|
|
260
291
|
}
|
|
261
292
|
class PiSdkSessionHandle {
|
|
293
|
+
relay;
|
|
262
294
|
session;
|
|
263
295
|
state;
|
|
296
|
+
unsubscribeTurnCounter;
|
|
264
297
|
stopped = false;
|
|
265
|
-
|
|
298
|
+
cumulativeTurnCount = 0;
|
|
299
|
+
constructor(session, state, relay) {
|
|
266
300
|
this.session = session;
|
|
267
301
|
this.state = state;
|
|
302
|
+
this.relay = relay;
|
|
303
|
+
this.unsubscribeTurnCounter = this.session.subscribe((event) => {
|
|
304
|
+
if (event.type === "turn_start") {
|
|
305
|
+
this.cumulativeTurnCount += 1;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
get turnCount() {
|
|
310
|
+
return this.cumulativeTurnCount;
|
|
268
311
|
}
|
|
269
312
|
async prompt(text) {
|
|
270
313
|
let accepted = false;
|
|
314
|
+
let turnStarted = false;
|
|
271
315
|
let finalAssistant;
|
|
272
316
|
const unsubscribe = this.session.subscribe((event) => {
|
|
317
|
+
if (event.type === "turn_start") {
|
|
318
|
+
turnStarted = true;
|
|
319
|
+
}
|
|
273
320
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
274
321
|
finalAssistant = event.message;
|
|
275
322
|
}
|
|
@@ -288,6 +335,8 @@ class PiSdkSessionHandle {
|
|
|
288
335
|
}
|
|
289
336
|
return {
|
|
290
337
|
accepted,
|
|
338
|
+
turnStarted,
|
|
339
|
+
turnCount: this.cumulativeTurnCount,
|
|
291
340
|
text: finalAssistant === undefined ? undefined : promptText(finalAssistant),
|
|
292
341
|
stopReason: finalAssistant?.stopReason,
|
|
293
342
|
errorMessage: finalAssistant?.errorMessage,
|
|
@@ -312,6 +361,12 @@ class PiSdkSessionHandle {
|
|
|
312
361
|
catch (error) {
|
|
313
362
|
failure ??= error;
|
|
314
363
|
}
|
|
364
|
+
try {
|
|
365
|
+
this.unsubscribeTurnCounter();
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
failure ??= error;
|
|
369
|
+
}
|
|
315
370
|
try {
|
|
316
371
|
this.session.dispose();
|
|
317
372
|
}
|
|
@@ -324,6 +379,10 @@ class PiSdkSessionHandle {
|
|
|
324
379
|
}
|
|
325
380
|
}
|
|
326
381
|
export class PiSdkSessionFactory {
|
|
382
|
+
relayFactory;
|
|
383
|
+
constructor(relayFactory = new PiRelayFactory()) {
|
|
384
|
+
this.relayFactory = relayFactory;
|
|
385
|
+
}
|
|
327
386
|
async create(input) {
|
|
328
387
|
const systemInstruction = input.config.instructions?.system;
|
|
329
388
|
if (systemInstruction?.mode === "append") {
|
|
@@ -359,38 +418,6 @@ export class PiSdkSessionFactory {
|
|
|
359
418
|
const extensionPaths = await resolveExtensionPaths(workspace, harnessSettings(input.config).extensions);
|
|
360
419
|
const skillPaths = await resolveSkillPaths(input.baseDir, input.config.skills?.paths ?? []);
|
|
361
420
|
const customTools = await resolveCustomTools(workspace, input.config.tools?.definitions ?? {});
|
|
362
|
-
const agentDir = join(workspace, ".fabric-pi");
|
|
363
|
-
const resourceLoader = new pi.DefaultResourceLoader({
|
|
364
|
-
cwd: workspace,
|
|
365
|
-
agentDir,
|
|
366
|
-
settingsManager: settings,
|
|
367
|
-
additionalExtensionPaths: extensionPaths,
|
|
368
|
-
additionalSkillPaths: skillPaths,
|
|
369
|
-
noExtensions: true,
|
|
370
|
-
noSkills: true,
|
|
371
|
-
noPromptTemplates: true,
|
|
372
|
-
noThemes: true,
|
|
373
|
-
noContextFiles: true,
|
|
374
|
-
systemPrompt: systemInstruction?.content,
|
|
375
|
-
});
|
|
376
|
-
await resourceLoader.reload();
|
|
377
|
-
const extensionErrors = resourceLoader.getExtensions().errors;
|
|
378
|
-
if (extensionErrors.length > 0) {
|
|
379
|
-
throw new LifecycleError("pi_extension_load_failed", "One or more configured Pi extensions failed to load", {
|
|
380
|
-
metadata: { count: extensionErrors.length },
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
rejectToolCollisions(customTools, extensionToolNames(resourceLoader));
|
|
384
|
-
const skillDiagnostics = resourceLoader.getSkills().diagnostics;
|
|
385
|
-
const blockingSkillDiagnostics = skillDiagnostics.filter((diagnostic) => diagnostic.type === "error" || diagnostic.type === "collision");
|
|
386
|
-
for (const diagnostic of skillDiagnostics.filter((entry) => entry.type === "warning")) {
|
|
387
|
-
process.stderr.write(`Pi skill warning: ${diagnostic.message}\n`);
|
|
388
|
-
}
|
|
389
|
-
if (blockingSkillDiagnostics.length > 0) {
|
|
390
|
-
throw new LifecycleError("pi_skill_load_failed", "One or more configured NeMo Fabric skills failed to load", {
|
|
391
|
-
metadata: { count: blockingSkillDiagnostics.length },
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
421
|
const credentials = new pi.InMemoryCredentialStore();
|
|
395
422
|
const modelRuntime = await pi.ModelRuntime.create({
|
|
396
423
|
credentials,
|
|
@@ -399,28 +426,97 @@ export class PiSdkSessionFactory {
|
|
|
399
426
|
refreshOnCreate: false,
|
|
400
427
|
});
|
|
401
428
|
await modelRuntime.setRuntimeApiKey(selected.provider, apiKey);
|
|
429
|
+
const relayEnabled = input.runtimeContext.telemetry?.relay_enabled === true;
|
|
430
|
+
if (relayEnabled && selected.base_url) {
|
|
431
|
+
// Configure the provider before Relay loads so its provider-wide redirect
|
|
432
|
+
// sees a consistent catalog instead of one overlaid selected model.
|
|
433
|
+
modelRuntime.registerProvider(selected.provider, {
|
|
434
|
+
baseUrl: selected.base_url,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
402
437
|
const catalogModel = modelRuntime.getModel(selected.provider, selected.model);
|
|
403
438
|
if (catalogModel === undefined) {
|
|
404
439
|
throw new LifecycleError("pi_model_unknown", "The selected provider and model are not present in Pi's catalog");
|
|
405
440
|
}
|
|
406
|
-
const model =
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
agentDir,
|
|
413
|
-
model,
|
|
414
|
-
modelRuntime,
|
|
415
|
-
resourceLoader,
|
|
416
|
-
sessionManager: pi.SessionManager.inMemory(workspace),
|
|
417
|
-
settingsManager: settings,
|
|
418
|
-
customTools,
|
|
419
|
-
tools: enabled === null ? undefined : enabled,
|
|
420
|
-
excludeTools: blocked,
|
|
441
|
+
const model = withCustomBaseUrl(catalogModel, selected.base_url, !relayEnabled);
|
|
442
|
+
const compactionReserveTokens = modelAwareCompactionReserveTokens(settings.getCompactionReserveTokens(), model.maxTokens, model.contextWindow);
|
|
443
|
+
settings.applyOverrides({
|
|
444
|
+
compaction: {
|
|
445
|
+
reserveTokens: compactionReserveTokens,
|
|
446
|
+
},
|
|
421
447
|
});
|
|
422
|
-
|
|
448
|
+
let relay;
|
|
449
|
+
let handle;
|
|
423
450
|
try {
|
|
451
|
+
relay = await this.relayFactory.start(input, {
|
|
452
|
+
api: model.api,
|
|
453
|
+
baseUrl: model.baseUrl,
|
|
454
|
+
});
|
|
455
|
+
if (relay !== undefined && !extensionPaths.includes(relay.extensionPath)) {
|
|
456
|
+
extensionPaths.push(relay.extensionPath);
|
|
457
|
+
}
|
|
458
|
+
const agentDir = join(workspace, ".fabric-pi");
|
|
459
|
+
const resourceLoader = new pi.DefaultResourceLoader({
|
|
460
|
+
cwd: workspace,
|
|
461
|
+
agentDir,
|
|
462
|
+
settingsManager: settings,
|
|
463
|
+
additionalExtensionPaths: extensionPaths,
|
|
464
|
+
additionalSkillPaths: skillPaths,
|
|
465
|
+
noExtensions: true,
|
|
466
|
+
noSkills: true,
|
|
467
|
+
noPromptTemplates: true,
|
|
468
|
+
noThemes: true,
|
|
469
|
+
noContextFiles: true,
|
|
470
|
+
systemPrompt: systemInstruction?.content,
|
|
471
|
+
});
|
|
472
|
+
await resourceLoader.reload();
|
|
473
|
+
const extensionResult = resourceLoader.getExtensions();
|
|
474
|
+
rejectToolCollisions(customTools, extensionResult.extensions.flatMap((extension) => [...extension.tools.keys()]));
|
|
475
|
+
const extensionErrors = extensionResult.errors;
|
|
476
|
+
const relayErrors = relayExtensionLoadErrors(extensionErrors, extensionResult.extensions, relay, workspace);
|
|
477
|
+
if (relayErrors.length > 0) {
|
|
478
|
+
throw new LifecycleError("pi_relay_extension_load_failed", "The configured NeMo Relay Pi extension failed to load; use the Pi extension from the NeMo Relay 0.9 release", {
|
|
479
|
+
metadata: {
|
|
480
|
+
count: relayErrors.length,
|
|
481
|
+
relay_error: relayErrors.map((error) => error.error).join("; "),
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
if (extensionErrors.length > 0) {
|
|
486
|
+
throw new LifecycleError("pi_extension_load_failed", "One or more configured Pi extensions failed to load or conflict", {
|
|
487
|
+
metadata: {
|
|
488
|
+
count: extensionErrors.length,
|
|
489
|
+
extension_error: extensionErrors.map((error) => error.error).join("; "),
|
|
490
|
+
extension_paths: extensionErrors.map((error) => error.path),
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const skillDiagnostics = resourceLoader.getSkills().diagnostics;
|
|
495
|
+
const blockingSkillDiagnostics = skillDiagnostics.filter((diagnostic) => diagnostic.type === "error" || diagnostic.type === "collision");
|
|
496
|
+
for (const diagnostic of skillDiagnostics.filter((entry) => entry.type === "warning")) {
|
|
497
|
+
process.stderr.write(`Pi skill warning: ${diagnostic.message}\n`);
|
|
498
|
+
}
|
|
499
|
+
if (blockingSkillDiagnostics.length > 0) {
|
|
500
|
+
throw new LifecycleError("pi_skill_load_failed", "One or more configured NeMo Fabric skills failed to load", {
|
|
501
|
+
metadata: { count: blockingSkillDiagnostics.length },
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
const enabled = input.config.tools?.enabled;
|
|
505
|
+
const blocked = input.config.tools?.blocked ?? [];
|
|
506
|
+
const state = { shutdownRequested: false };
|
|
507
|
+
const { session } = await pi.createAgentSession({
|
|
508
|
+
cwd: workspace,
|
|
509
|
+
agentDir,
|
|
510
|
+
model,
|
|
511
|
+
modelRuntime,
|
|
512
|
+
resourceLoader,
|
|
513
|
+
sessionManager: pi.SessionManager.inMemory(workspace),
|
|
514
|
+
settingsManager: settings,
|
|
515
|
+
customTools,
|
|
516
|
+
tools: enabled === null ? undefined : enabled,
|
|
517
|
+
excludeTools: blocked,
|
|
518
|
+
});
|
|
519
|
+
handle = new PiSdkSessionHandle(session, state, relay);
|
|
424
520
|
const blockedNames = new Set(blocked);
|
|
425
521
|
const availableNames = new Set(session.getAllTools().map((tool) => tool.name));
|
|
426
522
|
const missing = (enabled ?? []).filter((name) => !blockedNames.has(name) && !availableNames.has(name));
|
|
@@ -451,15 +547,17 @@ export class PiSdkSessionFactory {
|
|
|
451
547
|
process.stderr.write("Pi extension handler failed\n");
|
|
452
548
|
},
|
|
453
549
|
});
|
|
550
|
+
// bindExtensions emits session_start; emitting it here would create a
|
|
551
|
+
// duplicate Relay session scope.
|
|
454
552
|
return handle;
|
|
455
553
|
}
|
|
456
554
|
catch (error) {
|
|
457
|
-
|
|
458
|
-
await handle.stop();
|
|
459
|
-
}
|
|
460
|
-
catch {
|
|
555
|
+
await handle?.stop().catch(() => {
|
|
461
556
|
process.stderr.write("Pi session cleanup failed after adapter startup error\n");
|
|
462
|
-
}
|
|
557
|
+
});
|
|
558
|
+
await relay?.stop().catch(() => {
|
|
559
|
+
process.stderr.write("NeMo Relay cleanup failed after Pi adapter startup error\n");
|
|
560
|
+
});
|
|
463
561
|
throw error;
|
|
464
562
|
}
|
|
465
563
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import type { AdapterStartInput } from "nemo-fabric-adapters-common";
|
|
3
|
+
export type RelayPluginConfig = Record<string, unknown>;
|
|
4
|
+
export interface RelayConfigPaths {
|
|
5
|
+
configPath: string;
|
|
6
|
+
pluginConfigPath: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RelayArtifact {
|
|
9
|
+
kind: "atof" | "atif";
|
|
10
|
+
path: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RelayAtifMatcher {
|
|
13
|
+
directory: string;
|
|
14
|
+
local: boolean;
|
|
15
|
+
pattern: RegExp;
|
|
16
|
+
recursive: boolean;
|
|
17
|
+
template: string;
|
|
18
|
+
}
|
|
19
|
+
type ReadDirectory = (directory: string, options: {
|
|
20
|
+
withFileTypes: true;
|
|
21
|
+
}) => Promise<Dirent[]>;
|
|
22
|
+
export declare function validateRelayObservabilityV3(pluginConfig: RelayPluginConfig): void;
|
|
23
|
+
export declare function normalizeRelayOutputDirs(pluginConfig: RelayPluginConfig, input: AdapterStartInput): Promise<void>;
|
|
24
|
+
export declare function loadRelayPluginConfig(input: AdapterStartInput): Promise<RelayPluginConfig>;
|
|
25
|
+
export declare function encodeToml(value: Record<string, unknown>): string;
|
|
26
|
+
export declare function writeRelayConfigs(pluginConfig: RelayPluginConfig): Promise<RelayConfigPaths>;
|
|
27
|
+
export declare function matchesRelayAtifPath(matcher: RelayAtifMatcher, path: string): boolean;
|
|
28
|
+
export declare function prepareRelayAtifMatchers(pluginConfig: RelayPluginConfig): Promise<RelayAtifMatcher[]>;
|
|
29
|
+
export declare function collectAtifArtifacts(matchers: RelayAtifMatcher[], options?: {
|
|
30
|
+
localOnly?: boolean;
|
|
31
|
+
readDirectory?: ReadDirectory;
|
|
32
|
+
strict?: boolean;
|
|
33
|
+
}): Promise<RelayArtifact[]>;
|
|
34
|
+
export declare function collectRelayArtifacts(pluginConfig: RelayPluginConfig, atifMatchers?: RelayAtifMatcher[]): Promise<RelayArtifact[]>;
|
|
35
|
+
export {};
|