@sagentlab/navarch-runtime 0.1.79 → 0.1.81

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 CHANGED
@@ -941,3 +941,40 @@ over the project's session model for the selected adapter only; switching agent
941
941
  types does not reuse another adapter's pin. Reconnect without `--model` to return
942
942
  to project-controlled model selection. Older identities without a model keep
943
943
  using the control-plane execution policy.
944
+
945
+
946
+ ### Pi on your own machine
947
+
948
+ Select **Pi (local)** in Connect an agent, or set `NAVARCH_AGENT=pi-local`
949
+ and `NAVARCH_RUNTIMES=pi-local`. Use a current Pi CLI with native MCP support
950
+ and Node.js 22.19 or newer. Host mode requires `NAVARCH_WORKTREE_GUARD=off`:
951
+ Pi's shell tools do not enforce the runtime's worktree boundary. The UI's
952
+ connect and supervise commands include this opt-out. Use a dedicated worker.
953
+ Docker is not advertised for this adapter yet.
954
+
955
+ The initial model is `qwen3.8-flash-next-iq2_xs`, served by a local
956
+ OpenAI-compatible endpoint at `http://127.0.0.1:8080/v1`. Start that model server
957
+ separately. Set `NAVARCH_PI_LOCAL_BASE_URL` in the worker environment for a
958
+ different endpoint (for example, Ollama's `http://127.0.0.1:11434/v1`).
959
+ `NAVARCH_PI_BIN` overrides the executable; `NAVARCH_PI_EXTRA_ARGS` adds CLI
960
+ arguments. `--model` at connect pins this machine's model as for other agents.
961
+
962
+ Bare model IDs first resolve a unique matching model in the operator's catalog;
963
+ ambiguous matches require an explicit `provider/model`. Without a match, they
964
+ use the generated `local` provider. Setting `NAVARCH_PI_LOCAL_BASE_URL` explicitly
965
+ selects that generated provider. Exact `provider/model`
966
+ references use the machine's existing Pi catalog and credentials from
967
+ `~/.pi/agent/models.json` and `auth.json` (or `PI_CODING_AGENT_DIR`). Run
968
+ `pi --list-models` to inspect that catalog. The UI puts the local default first
969
+ and labels public cloud suggestions by provider; suggestions do not imply
970
+ credentials or access. Custom model IDs remain editable.
971
+
972
+ Per-session configuration lives in a temporary agent directory, including
973
+ Navarch's authenticated `mcp.json`. Pi runs without persisted sessions,
974
+ automatically loaded extensions, skills, or project configuration. Temporary
975
+ configuration is removed at exit. Hosted Pi keeps its separate `pi` runtime,
976
+ DeepSeek provider, and billing policy; local sessions use `pi-local` and the
977
+ project's `pi_local_model` policy.
978
+
979
+ Pi CLI and model configuration reference:
980
+ https://github.com/earendil-works/pi/tree/main/packages/coding-agent/docs
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runQoderAdapter = exports.qoderAdapter = exports.runAcpAdapter = exports.acpAdapter = exports.runPiAdapter = exports.piAdapter = exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
3
+ exports.runQoderAdapter = exports.qoderAdapter = exports.runAcpAdapter = exports.acpAdapter = exports.runPiAdapter = exports.piAdapter = exports.runLocalPiAdapter = exports.localPiAdapter = exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
4
4
  exports.selectAdapter = selectAdapter;
5
5
  const claude_cjs_1 = require("./claude.cjs");
6
6
  Object.defineProperty(exports, "claudeCodeAdapter", { enumerable: true, get: function () { return claude_cjs_1.claudeCodeAdapter; } });
@@ -14,6 +14,9 @@ Object.defineProperty(exports, "runGeminiAdapter", { enumerable: true, get: func
14
14
  const opencode_cjs_1 = require("./opencode.cjs");
15
15
  Object.defineProperty(exports, "openCodeAdapter", { enumerable: true, get: function () { return opencode_cjs_1.openCodeAdapter; } });
16
16
  Object.defineProperty(exports, "runOpenCodeAdapter", { enumerable: true, get: function () { return opencode_cjs_1.runOpenCodeAdapter; } });
17
+ const pi_local_cjs_1 = require("./pi-local.cjs");
18
+ Object.defineProperty(exports, "localPiAdapter", { enumerable: true, get: function () { return pi_local_cjs_1.localPiAdapter; } });
19
+ Object.defineProperty(exports, "runLocalPiAdapter", { enumerable: true, get: function () { return pi_local_cjs_1.runLocalPiAdapter; } });
17
20
  const pi_cjs_1 = require("./pi.cjs");
18
21
  Object.defineProperty(exports, "piAdapter", { enumerable: true, get: function () { return pi_cjs_1.piAdapter; } });
19
22
  Object.defineProperty(exports, "runPiAdapter", { enumerable: true, get: function () { return pi_cjs_1.runPiAdapter; } });
@@ -44,6 +47,8 @@ function selectAdapter(agentType) {
44
47
  return qoder_cjs_1.qoderAdapter;
45
48
  case "claude-code":
46
49
  return claude_cjs_1.claudeCodeAdapter;
50
+ case "pi-local":
51
+ return pi_local_cjs_1.localPiAdapter;
47
52
  case "pi":
48
53
  // Managed hosted runtime (#885): reachable only on a managed runner
49
54
  // executing a `runtime: "pi"` claim — config.cts never produces "pi"
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.localPiAdapter = void 0;
4
+ exports.runLocalPiAdapter = runLocalPiAdapter;
5
+ const pi_cjs_1 = require("./pi.cjs");
6
+ /** BYO Pi: native MCP, operator provider catalog, and a local OpenAI-compatible default. */
7
+ async function runLocalPiAdapter(options) {
8
+ if (options.dockerExec)
9
+ throw new Error("Pi (local) requires host mode");
10
+ if (options.signal?.aborted)
11
+ return { exitCode: null, timedOut: false, killedByLeaseLoss: true, stdout: "", stderr: "" };
12
+ const model = options.model ?? "qwen3.8-flash-next-iq2_xs";
13
+ const args = ["--mode", "json", "--no-session", "--no-extensions", "--no-skills", "--no-approve", ...options.extraArgs,
14
+ "--model", model.includes("/") ? model : `local/${model}`];
15
+ if (options.reasoningEffort)
16
+ args.push("--thinking", options.reasoningEffort === "max" ? "xhigh" : options.reasoningEffort);
17
+ args.push("-p", options.prompt);
18
+ return (0, pi_cjs_1.attachPiOutput)(await (0, pi_cjs_1.runOnHost)(options, args, true));
19
+ }
20
+ exports.localPiAdapter = { agentType: "pi-local", run: runLocalPiAdapter };
@@ -8,7 +8,9 @@ exports.buildHostedPiBudgetExtension = buildHostedPiBudgetExtension;
8
8
  exports.resolveHostedPiModel = resolveHostedPiModel;
9
9
  exports.buildPiProviderConfig = buildPiProviderConfig;
10
10
  exports.parsePiJsonEvents = parsePiJsonEvents;
11
+ exports.attachPiOutput = attachPiOutput;
11
12
  exports.runPiAdapter = runPiAdapter;
13
+ exports.runOnHost = runOnHost;
12
14
  const node_child_process_1 = require("node:child_process");
13
15
  const node_fs_1 = require("node:fs");
14
16
  const node_os_1 = __importDefault(require("node:os"));
@@ -163,10 +165,20 @@ function attachPiOutput(result) {
163
165
  let costUsd;
164
166
  let sawUsage = false;
165
167
  let reportText = "";
168
+ let terminalError;
166
169
  for (const event of events) {
167
170
  const message = event.message;
171
+ if (event.type === "error") {
172
+ terminalError = typeof event.error === "string" ? event.error :
173
+ typeof event.errorMessage === "string" ? event.errorMessage : "Pi provider error";
174
+ }
168
175
  if (event.type !== "message_end" || !message || message.role !== "assistant")
169
176
  continue;
177
+ // Pi can settle an exhausted provider retry with process exit code zero.
178
+ // A later successful assistant message supersedes a recovered error.
179
+ terminalError = message.stopReason === "error" || message.errorMessage
180
+ ? typeof message.errorMessage === "string" ? message.errorMessage : "Pi provider error"
181
+ : undefined;
170
182
  const text = extractText(message.content);
171
183
  if (text.trim())
172
184
  reportText = text.trim();
@@ -191,6 +203,9 @@ function attachPiOutput(result) {
191
203
  }
192
204
  return {
193
205
  ...result,
206
+ ...(terminalError && result.exitCode === 0 && !result.timedOut && !result.killedByLeaseLoss
207
+ ? { exitCode: 1, stderr: `${result.stderr}${result.stderr ? "\n" : ""}Pi provider failed: ${terminalError}` }
208
+ : {}),
194
209
  ...(sawUsage
195
210
  ? {
196
211
  // AdapterResult semantics: tokensIn folds every input category
@@ -245,18 +260,61 @@ async function runPiAdapter(options) {
245
260
  : await runOnHost(options, args);
246
261
  return attachPiOutput(raw);
247
262
  }
248
- async function runOnHost(options, args) {
249
- // Isolated agent dir so the generated provider config never touches (or
250
- // reads) an operator's ~/.pi configuration/credentials.
263
+ async function runOnHost(options, args, local = false) {
264
+ // Hosted sessions never read operator auth; BYO sessions copy only provider
265
+ // config and credentials. Both paths clean up their isolated agent directory.
251
266
  const agentDir = await node_fs_1.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), "navarch-pi-agent-"));
252
- await node_fs_1.promises.writeFile(node_path_1.default.join(agentDir, "models.json"), buildPiProviderConfig(), { mode: 0o600 });
253
- const budgetExtensionPath = node_path_1.default.join(agentDir, BUDGET_EXTENSION_NAME);
254
- await node_fs_1.promises.writeFile(budgetExtensionPath, buildHostedPiBudgetExtension(), {
255
- mode: 0o600,
256
- });
257
- const env = { ...options.env, PI_CODING_AGENT_DIR: agentDir };
258
- const boundedArgs = withBudgetExtension(args, budgetExtensionPath);
259
267
  try {
268
+ if (local) {
269
+ const sourceDir = options.env.PI_CODING_AGENT_DIR ?? process.env.PI_CODING_AGENT_DIR ?? node_path_1.default.join(node_os_1.default.homedir(), ".pi", "agent");
270
+ for (const name of ["models.json", "auth.json"]) {
271
+ try {
272
+ await node_fs_1.promises.copyFile(node_path_1.default.join(sourceDir, name), node_path_1.default.join(agentDir, name));
273
+ await node_fs_1.promises.chmod(node_path_1.default.join(agentDir, name), 0o600);
274
+ }
275
+ catch (error) {
276
+ if (error.code !== "ENOENT")
277
+ throw error;
278
+ }
279
+ }
280
+ const model = options.model ?? "qwen3.8-flash-next-iq2_xs";
281
+ if (!model.includes("/")) {
282
+ const configPath = node_path_1.default.join(agentDir, "models.json");
283
+ const config = JSON.parse(await node_fs_1.promises.readFile(configPath, "utf8").catch(() => "{}"));
284
+ config.providers ??= {};
285
+ const endpointOverride = options.env.NAVARCH_PI_LOCAL_BASE_URL ?? process.env.NAVARCH_PI_LOCAL_BASE_URL;
286
+ const matches = endpointOverride ? [] : Object.entries(config.providers).filter(([, provider]) => provider.models?.some((entry) => entry.id === model)).map(([name]) => name);
287
+ if (matches.length > 1)
288
+ throw new Error(`Pi model ${model} matches multiple providers; select an explicit provider/model.`);
289
+ if (matches.length === 1) {
290
+ args = [...args];
291
+ args[args.lastIndexOf("--model") + 1] = `${matches[0]}/${model}`;
292
+ }
293
+ else {
294
+ // Without a catalog match, retain the default local endpoint behavior.
295
+ config.providers.local = {
296
+ baseUrl: endpointOverride ?? "http://127.0.0.1:8080/v1",
297
+ api: "openai-completions", apiKey: "local",
298
+ models: [{ id: model, reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } }],
299
+ };
300
+ await node_fs_1.promises.writeFile(configPath, JSON.stringify(config), { mode: 0o600 });
301
+ }
302
+ }
303
+ if (options.mcpConfigPath) {
304
+ await node_fs_1.promises.copyFile(options.mcpConfigPath, node_path_1.default.join(agentDir, "mcp.json"));
305
+ await node_fs_1.promises.chmod(node_path_1.default.join(agentDir, "mcp.json"), 0o600);
306
+ }
307
+ }
308
+ else {
309
+ await node_fs_1.promises.writeFile(node_path_1.default.join(agentDir, "models.json"), buildPiProviderConfig(), { mode: 0o600 });
310
+ }
311
+ const budgetExtensionPath = node_path_1.default.join(agentDir, BUDGET_EXTENSION_NAME);
312
+ if (!local)
313
+ await node_fs_1.promises.writeFile(budgetExtensionPath, buildHostedPiBudgetExtension(), {
314
+ mode: 0o600,
315
+ });
316
+ const env = { ...options.env, PI_CODING_AGENT_DIR: agentDir };
317
+ const boundedArgs = local ? args : withBudgetExtension(args, budgetExtensionPath);
260
318
  return await new Promise((resolve) => {
261
319
  let stdout = "";
262
320
  let stderr = "";
package/dist/config.cjs CHANGED
@@ -25,6 +25,7 @@ exports.RUNTIME_AGENT_TYPES = [
25
25
  "opencode",
26
26
  "acp",
27
27
  "qoder",
28
+ "pi-local",
28
29
  ];
29
30
  function isRuntimeAgentType(value) {
30
31
  return typeof value === "string" && exports.RUNTIME_AGENT_TYPES.includes(value);
@@ -105,7 +106,12 @@ function loadRuntimeConfig(env = process.env) {
105
106
  // fails closed there (adapters/opencode.cts). Advertising it anyway meant
106
107
  // the machine claimed OpenCode tasks it could only fail, one lease at a
107
108
  // time; keep it out of the claim instead of discovering this per session.
108
- const runnableRuntimes = requestedRuntimes.filter((runtime) => runtime !== "opencode" || !opencodeBlockedOnHost(sandboxMode, worktreeGuard));
109
+ const runnableRuntimes = requestedRuntimes.filter((runtime) => runtime === "pi-local"
110
+ ? sandboxMode === "host" && !worktreeGuard
111
+ : runtime !== "opencode" || !opencodeBlockedOnHost(sandboxMode, worktreeGuard));
112
+ if (requestedRuntimes.includes("pi-local") && runnableRuntimes.length === 0) {
113
+ throw new Error("Pi (local) requires NAVARCH_SANDBOX_MODE=host and NAVARCH_WORKTREE_GUARD=off");
114
+ }
109
115
  if (requestedRuntimes.includes("opencode") && runnableRuntimes.length === 0) {
110
116
  throw new Error(exports.OPENCODE_HOST_GUARD_MESSAGE);
111
117
  }
@@ -143,6 +149,8 @@ function loadRuntimeConfig(env = process.env) {
143
149
  opencodeExtraArgs: envList(env, "NAVARCH_OPENCODE_EXTRA_ARGS", []),
144
150
  acpBin: env.NAVARCH_ACP_BIN ?? "dsh",
145
151
  acpExtraArgs: envList(env, "NAVARCH_ACP_EXTRA_ARGS", ["--profile", "acp"]),
152
+ piBin: env.NAVARCH_PI_BIN ?? "pi",
153
+ piExtraArgs: envList(env, "NAVARCH_PI_EXTRA_ARGS", []),
146
154
  qoderBin: env.NAVARCH_QODER_BIN ?? "qoder",
147
155
  qoderExtraArgs: envList(env, "NAVARCH_QODER_EXTRA_ARGS", []),
148
156
  gitAuthorName: env.NAVARCH_GIT_AUTHOR_NAME ?? "sagentlab",
package/dist/session.cjs CHANGED
@@ -46,9 +46,10 @@ function resolveSessionExecution(config, claimed) {
46
46
  const execution = claimed.context_bundle.execution ?? {
47
47
  profile: claimed.task.execution_profile ?? "standard",
48
48
  model: runtime === "codex" ? "gpt-6-astra"
49
- : runtime === "gemini" ? "auto"
50
- : runtime === "opencode" || runtime === "acp" || runtime === "qoder" ? "default"
51
- : "claude-opus-5-5",
49
+ : runtime === "pi-local" ? "qwen3.8-flash-next-iq2_xs"
50
+ : runtime === "gemini" ? "auto"
51
+ : runtime === "opencode" || runtime === "acp" || runtime === "qoder" ? "default"
52
+ : "claude-opus-5-5",
52
53
  reasoning_effort: "medium",
53
54
  };
54
55
  return {
@@ -865,6 +866,8 @@ function adapterCommand(config, runtime) {
865
866
  return { bin: config.opencodeBin, extraArgs: config.opencodeExtraArgs };
866
867
  case "acp":
867
868
  return { bin: config.acpBin, extraArgs: config.acpExtraArgs };
869
+ case "pi-local":
870
+ return { bin: config.piBin, extraArgs: config.piExtraArgs };
868
871
  case "qoder":
869
872
  return { bin: config.qoderBin, extraArgs: config.qoderExtraArgs };
870
873
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.79",
3
+ "version": "0.1.81",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",