@caupulican/pi-adaptative 0.81.20 → 0.81.22

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +21 -4
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/doctor.d.ts.map +1 -1
  6. package/dist/core/doctor.js +4 -1
  7. package/dist/core/doctor.js.map +1 -1
  8. package/dist/core/local-runtime-controller.d.ts +2 -0
  9. package/dist/core/local-runtime-controller.d.ts.map +1 -1
  10. package/dist/core/local-runtime-controller.js +92 -6
  11. package/dist/core/local-runtime-controller.js.map +1 -1
  12. package/dist/core/model-router/intent-classifier.d.ts.map +1 -1
  13. package/dist/core/model-router/intent-classifier.js +1 -1
  14. package/dist/core/model-router/intent-classifier.js.map +1 -1
  15. package/dist/core/model-router/route-judge.d.ts.map +1 -1
  16. package/dist/core/model-router/route-judge.js +10 -6
  17. package/dist/core/model-router/route-judge.js.map +1 -1
  18. package/dist/core/model-router/tool-escalation.d.ts.map +1 -1
  19. package/dist/core/model-router/tool-escalation.js +2 -1
  20. package/dist/core/model-router/tool-escalation.js.map +1 -1
  21. package/dist/core/model-router-controller.d.ts.map +1 -1
  22. package/dist/core/model-router-controller.js +27 -2
  23. package/dist/core/model-router-controller.js.map +1 -1
  24. package/dist/core/models/context-sizing.d.ts.map +1 -1
  25. package/dist/core/models/context-sizing.js +18 -26
  26. package/dist/core/models/context-sizing.js.map +1 -1
  27. package/dist/core/models/local-runtime.d.ts +56 -4
  28. package/dist/core/models/local-runtime.d.ts.map +1 -1
  29. package/dist/core/models/local-runtime.js +286 -19
  30. package/dist/core/models/local-runtime.js.map +1 -1
  31. package/dist/core/models/perf-profile.d.ts +3 -0
  32. package/dist/core/models/perf-profile.d.ts.map +1 -1
  33. package/dist/core/models/perf-profile.js +42 -13
  34. package/dist/core/models/perf-profile.js.map +1 -1
  35. package/dist/core/models/runtime-arbiter.d.ts +45 -0
  36. package/dist/core/models/runtime-arbiter.d.ts.map +1 -1
  37. package/dist/core/models/runtime-arbiter.js +115 -0
  38. package/dist/core/models/runtime-arbiter.js.map +1 -1
  39. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  40. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  41. package/dist/modes/interactive/interactive-mode.js +25 -1
  42. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  43. package/dist/modes/interactive/local-model-commands.d.ts.map +1 -1
  44. package/dist/modes/interactive/local-model-commands.js +24 -6
  45. package/dist/modes/interactive/local-model-commands.js.map +1 -1
  46. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  47. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  48. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  49. package/examples/extensions/sandbox/package-lock.json +2 -2
  50. package/examples/extensions/sandbox/package.json +1 -1
  51. package/examples/extensions/with-deps/package-lock.json +2 -2
  52. package/examples/extensions/with-deps/package.json +1 -1
  53. package/npm-shrinkwrap.json +12 -12
  54. package/package.json +4 -4
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
- import { createWriteStream, existsSync, mkdirSync, rmSync } from "node:fs";
2
+ import { copyFileSync, createWriteStream, existsSync, linkSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, } from "node:fs";
3
3
  import { homedir, arch as osArch, platform as osPlatform } from "node:os";
4
- import { delimiter, join } from "node:path";
4
+ import { delimiter, dirname, join, relative } from "node:path";
5
5
  import { pipeline } from "node:stream/promises";
6
6
  import * as nodeZlib from "node:zlib";
7
7
  import { getBundledResourcesDir } from "../../config.js";
@@ -56,6 +56,17 @@ const TRANSFORMERS_SERVER_RELATIVE_PATH = join("runtimes", "hf-transformers-open
56
56
  const TRANSFORMERS_PINNED_PACKAGES = ["transformers==5.13.0", "huggingface-hub==1.22.0", "safetensors==0.8.0"];
57
57
  const TRANSFORMERS_REQUIRED_MODULES = ["torch", "transformers", "huggingface_hub", "safetensors"];
58
58
  const TORCH_PINNED_VERSION = "2.12.1";
59
+ function isCrossDeviceLinkError(error) {
60
+ return typeof error === "object" && error !== null && error.code === "EXDEV";
61
+ }
62
+ function fileSizeBytes(path) {
63
+ try {
64
+ return statSync(path).size;
65
+ }
66
+ catch {
67
+ return 0;
68
+ }
69
+ }
59
70
  async function runCommandDefault(command, args, options = {}) {
60
71
  try {
61
72
  const proc = spawnProcess(command, args, {
@@ -113,6 +124,8 @@ export class OllamaRuntime {
113
124
  _fetch;
114
125
  _spawn;
115
126
  _exists;
127
+ _linkFile;
128
+ _copyFile;
116
129
  _envPath;
117
130
  _homeDir;
118
131
  _sleep;
@@ -122,12 +135,15 @@ export class OllamaRuntime {
122
135
  _hasCommand;
123
136
  _extractArchiveOverride;
124
137
  _child;
138
+ _childModelsDir;
125
139
  constructor(args) {
126
140
  this._agentDir = args.agentDir;
127
141
  this._baseUrl = (args.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
128
142
  this._fetch = args.deps?.fetchFn ?? fetch;
129
143
  this._spawn = args.deps?.spawnFn ?? ((command, argv, options) => spawn(command, argv, options));
130
144
  this._exists = args.deps?.existsFn ?? existsSync;
145
+ this._linkFile = args.deps?.linkFile ?? linkSync;
146
+ this._copyFile = args.deps?.copyFile ?? copyFileSync;
131
147
  this._envPath = args.deps?.envPath ?? process.env.PATH ?? "";
132
148
  this._homeDir = args.deps?.homeDir ?? homedir();
133
149
  this._sleep = args.deps?.sleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -155,6 +171,35 @@ export class OllamaRuntime {
155
171
  ownedModelsDir() {
156
172
  return join(this._agentDir, "models", "ollama");
157
173
  }
174
+ userModelsDir() {
175
+ return join(this._homeDir, ".ollama", "models");
176
+ }
177
+ _storeSummary(kind, path) {
178
+ return { kind, path, modelCount: this._countStoreModels(path) };
179
+ }
180
+ _countStoreModels(storeDir) {
181
+ return this._walkFiles(join(storeDir, "manifests")).length;
182
+ }
183
+ _walkFiles(root) {
184
+ if (!this._exists(root))
185
+ return [];
186
+ let entries;
187
+ try {
188
+ entries = readdirSync(root, { withFileTypes: true });
189
+ }
190
+ catch {
191
+ return [];
192
+ }
193
+ const files = [];
194
+ for (const entry of entries) {
195
+ const path = join(root, entry.name);
196
+ if (entry.isDirectory())
197
+ files.push(...this._walkFiles(path));
198
+ else if (entry.isFile())
199
+ files.push(path);
200
+ }
201
+ return files;
202
+ }
158
203
  _findBinary() {
159
204
  const piOwned = join(this._agentDir, "runtimes", "ollama", "bin", "ollama");
160
205
  if (this._exists(piOwned))
@@ -172,27 +217,115 @@ export class OllamaRuntime {
172
217
  return undefined;
173
218
  }
174
219
  async _serverUp() {
220
+ return (await this._serverModels()).up;
221
+ }
222
+ async _serverModels() {
175
223
  try {
176
224
  const response = await this._fetch(`${this._baseUrl}/api/tags`, {
177
225
  signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),
178
226
  });
179
- return response.ok;
227
+ if (!response.ok)
228
+ return { up: false, modelCount: 0 };
229
+ const data = (await response.json().catch(() => ({})));
230
+ return { up: true, modelCount: Array.isArray(data.models) ? data.models.length : 0 };
180
231
  }
181
232
  catch {
182
- return false;
233
+ return { up: false, modelCount: 0 };
183
234
  }
184
235
  }
236
+ _activeStoreSummary(serverModelCount, ownedStore, userStore) {
237
+ if (this._childModelsDir) {
238
+ return {
239
+ kind: this._childModelsDir === this.ownedModelsDir() ? "pi-owned" : "user",
240
+ path: this._childModelsDir,
241
+ modelCount: serverModelCount,
242
+ };
243
+ }
244
+ if (serverModelCount > 0 &&
245
+ serverModelCount === ownedStore.modelCount &&
246
+ serverModelCount !== userStore.modelCount) {
247
+ return { ...ownedStore, modelCount: serverModelCount };
248
+ }
249
+ if (serverModelCount > 0 &&
250
+ serverModelCount === userStore.modelCount &&
251
+ serverModelCount !== ownedStore.modelCount) {
252
+ return { ...userStore, modelCount: serverModelCount };
253
+ }
254
+ return { kind: "external", path: "external/unknown", modelCount: serverModelCount };
255
+ }
185
256
  async detect() {
186
257
  const binary = this._findBinary();
187
- const serverUp = await this._serverUp();
258
+ const serverModels = await this._serverModels();
259
+ const ownedStore = this._storeSummary("pi-owned", this.ownedModelsDir());
260
+ const userStore = this._storeSummary("user", this.userModelsDir());
261
+ const activeStore = serverModels.up
262
+ ? this._activeStoreSummary(serverModels.modelCount, ownedStore, userStore)
263
+ : undefined;
188
264
  return {
189
265
  binaryPath: binary?.path,
190
266
  binarySource: binary?.source,
191
- serverUp,
267
+ serverUp: serverModels.up,
192
268
  serverUrl: this._baseUrl,
193
- managedByPi: this._child !== undefined && serverUp,
269
+ managedByPi: this._child !== undefined && serverModels.up,
194
270
  ownedModelsDir: this.ownedModelsDir(),
271
+ userModelsDir: this.userModelsDir(),
272
+ ownedStore,
273
+ userStore,
274
+ activeStore,
275
+ };
276
+ }
277
+ importUserModels() {
278
+ const sourceDir = this.userModelsDir();
279
+ const targetDir = this.ownedModelsDir();
280
+ const result = {
281
+ sourceDir,
282
+ targetDir,
283
+ manifestsImported: 0,
284
+ manifestsSkipped: 0,
285
+ blobsHardlinked: 0,
286
+ blobsCopied: 0,
287
+ blobsSkipped: 0,
288
+ bytesImported: 0,
195
289
  };
290
+ if (sourceDir === targetDir || !this._exists(sourceDir))
291
+ return result;
292
+ mkdirSync(targetDir, { recursive: true });
293
+ this._importManifestFiles(join(sourceDir, "manifests"), join(targetDir, "manifests"), result);
294
+ this._importBlobFiles(join(sourceDir, "blobs"), join(targetDir, "blobs"), result);
295
+ return result;
296
+ }
297
+ _importManifestFiles(sourceRoot, targetRoot, result) {
298
+ for (const source of this._walkFiles(sourceRoot)) {
299
+ const target = join(targetRoot, relative(sourceRoot, source));
300
+ if (this._exists(target)) {
301
+ result.manifestsSkipped++;
302
+ continue;
303
+ }
304
+ mkdirSync(dirname(target), { recursive: true });
305
+ this._copyFile(source, target);
306
+ result.manifestsImported++;
307
+ }
308
+ }
309
+ _importBlobFiles(sourceRoot, targetRoot, result) {
310
+ for (const source of this._walkFiles(sourceRoot)) {
311
+ const target = join(targetRoot, relative(sourceRoot, source));
312
+ if (this._exists(target)) {
313
+ result.blobsSkipped++;
314
+ continue;
315
+ }
316
+ mkdirSync(dirname(target), { recursive: true });
317
+ try {
318
+ this._linkFile(source, target);
319
+ result.blobsHardlinked++;
320
+ }
321
+ catch (error) {
322
+ if (!isCrossDeviceLinkError(error))
323
+ throw error;
324
+ this._copyFile(source, target);
325
+ result.blobsCopied++;
326
+ }
327
+ result.bytesImported += fileSizeBytes(target);
328
+ }
196
329
  }
197
330
  /**
198
331
  * Manual-steps fallback text — shown when the managed install below wasn't offered (headless),
@@ -333,9 +466,9 @@ export class OllamaRuntime {
333
466
  * differs between them (owned storage vs reusing the user's own). */
334
467
  async _spawnAndPoll(binary, extraEnv) {
335
468
  const host = this._baseUrl.replace(/^https?:\/\//, "");
336
- this._child = this._spawn(binary.path, ["serve"], {
337
- env: { ...process.env, OLLAMA_HOST: host, ...extraEnv },
338
- });
469
+ const env = { ...process.env, OLLAMA_HOST: host, ...extraEnv };
470
+ this._childModelsDir = env.OLLAMA_MODELS ?? this.userModelsDir();
471
+ this._child = this._spawn(binary.path, ["serve"], { env });
339
472
  this._child.unref?.();
340
473
  for (let attempt = 0; attempt < START_POLL_ATTEMPTS; attempt++) {
341
474
  if (await this._serverUp())
@@ -399,6 +532,7 @@ export class OllamaRuntime {
399
532
  // already gone
400
533
  }
401
534
  this._child = undefined;
535
+ this._childModelsDir = undefined;
402
536
  return { stopped: true };
403
537
  }
404
538
  async list() {
@@ -410,6 +544,43 @@ export class OllamaRuntime {
410
544
  .filter((model) => typeof model.name === "string")
411
545
  .map((model) => ({ name: model.name, sizeBytes: model.size ?? 0 }));
412
546
  }
547
+ async listResidentModels() {
548
+ const response = await this._fetch(`${this._baseUrl}/api/ps`, { signal: AbortSignal.timeout(10_000) });
549
+ if (!response.ok)
550
+ throw new Error(`ollama ps failed: HTTP ${response.status}`);
551
+ const data = (await response.json());
552
+ return (data.models ?? [])
553
+ .map((model) => ({ name: model.name ?? model.model, sizeBytes: model.size_vram ?? model.size ?? 0 }))
554
+ .filter((model) => typeof model.name === "string" && model.name.length > 0);
555
+ }
556
+ async ensureResident(model) {
557
+ return this._postKeepAlive(model, "30m");
558
+ }
559
+ async releaseResident(model) {
560
+ return this._postKeepAlive(model, 0);
561
+ }
562
+ async _postKeepAlive(model, keepAlive) {
563
+ try {
564
+ const response = await this._fetch(`${this._baseUrl}/api/generate`, {
565
+ method: "POST",
566
+ headers: { "content-type": "application/json" },
567
+ body: JSON.stringify({
568
+ model,
569
+ prompt: "",
570
+ stream: false,
571
+ keep_alive: keepAlive,
572
+ options: { num_predict: 0 },
573
+ }),
574
+ signal: AbortSignal.timeout(60_000),
575
+ });
576
+ if (!response.ok)
577
+ return { ok: false, error: `HTTP ${response.status}: ${await response.text().catch(() => "")}` };
578
+ return { ok: true };
579
+ }
580
+ catch (error) {
581
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
582
+ }
583
+ }
413
584
  async show(ref) {
414
585
  try {
415
586
  const response = await this._fetch(`${this._baseUrl}/api/show`, {
@@ -671,21 +842,117 @@ export class TransformersRuntime {
671
842
  ]);
672
843
  return result.ok;
673
844
  }
845
+ readPyvenvHome() {
846
+ const pyvenvCfg = join(this.venvDir, "pyvenv.cfg");
847
+ if (!this._exists(pyvenvCfg))
848
+ return undefined;
849
+ try {
850
+ const text = readFileSync(pyvenvCfg, "utf8");
851
+ const homeLine = text
852
+ .split(/\r?\n/)
853
+ .map((line) => line.trim())
854
+ .find((line) => line.toLowerCase().startsWith("home ="));
855
+ return homeLine?.slice(homeLine.indexOf("=") + 1).trim() || undefined;
856
+ }
857
+ catch {
858
+ return undefined;
859
+ }
860
+ }
861
+ async venvInterpreterCoherent() {
862
+ const pyvenvHome = this.readPyvenvHome();
863
+ if (!pyvenvHome)
864
+ return false;
865
+ const base = await this.runPython(["-c", "import sys; print(sys.base_prefix or sys.prefix)"]);
866
+ if (!base.ok)
867
+ return false;
868
+ const basePrefix = base.stdout.trim();
869
+ if (!basePrefix)
870
+ return true;
871
+ return pyvenvHome === basePrefix || pyvenvHome.startsWith(basePrefix) || basePrefix.startsWith(pyvenvHome);
872
+ }
873
+ async venvHealthy() {
874
+ const pip = await this.runPython(["-m", "pip", "--version"]);
875
+ return pip.ok && (await this.venvInterpreterCoherent());
876
+ }
877
+ linuxVenvPackageCommand() {
878
+ try {
879
+ const osRelease = readFileSync("/etc/os-release", "utf8").toLowerCase();
880
+ if (osRelease.includes("alpine"))
881
+ return "sudo apk add python3 py3-pip";
882
+ if (osRelease.includes("fedora") || osRelease.includes("rhel") || osRelease.includes("centos")) {
883
+ return "sudo dnf install python3";
884
+ }
885
+ }
886
+ catch {
887
+ // fall through to the most common Debian/Ubuntu package split.
888
+ }
889
+ return "sudo apt install python3-venv";
890
+ }
891
+ venvInstallHint() {
892
+ if (this._platform() === "darwin")
893
+ return "install Python from python.org or run: brew install python";
894
+ if (this._platform() === "win32")
895
+ return "install Python 3 from python.org, then run: py -3 -m venv <path>";
896
+ if (this._platform() === "linux")
897
+ return this.linuxVenvPackageCommand();
898
+ return "install Python with venv/ensurepip support for this platform";
899
+ }
900
+ venvFailure(error) {
901
+ return {
902
+ ok: false,
903
+ error: `venv-fail: ${sanitizeCommandOutput(error)}. Install Python venv support: ${this.venvInstallHint()}`,
904
+ };
905
+ }
906
+ async createVenv(onProgress) {
907
+ const python = await this.findPython();
908
+ if (!python)
909
+ return { ok: false, error: "python-missing" };
910
+ onProgress?.(`creating isolated Python venv at ${this.venvDir}`);
911
+ const venv = await this._runCommand(python.command, [...python.args, "-m", "venv", this.venvDir]);
912
+ if (!venv.ok)
913
+ return this.venvFailure(venv.error ?? venv.stderr);
914
+ if (!this._exists(this.pythonPath))
915
+ return this.venvFailure(`${this.pythonPath} was not created`);
916
+ return { ok: true };
917
+ }
918
+ async repairExistingVenv(onProgress) {
919
+ if (await this.venvHealthy())
920
+ return { ok: true };
921
+ onProgress?.("repairing pi-managed Transformers venv with ensurepip");
922
+ const ensurepip = await this.runPython(["-m", "ensurepip", "--upgrade"], onProgress);
923
+ if (ensurepip.ok && (await this.venvHealthy()))
924
+ return { ok: true };
925
+ onProgress?.("recreating pi-managed Transformers venv");
926
+ rmSync(this.venvDir, { recursive: true, force: true });
927
+ return this.createVenv(onProgress);
928
+ }
929
+ async ensurePipAvailable(onProgress) {
930
+ const pip = await this.runPython(["-m", "pip", "--version"]);
931
+ if (pip.ok)
932
+ return { ok: true };
933
+ onProgress?.("bootstrapping pip inside pi-managed venv");
934
+ const ensurepip = await this.runPython(["-m", "ensurepip", "--upgrade"], onProgress);
935
+ if (!ensurepip.ok)
936
+ return this.venvFailure(ensurepip.error ?? ensurepip.stderr);
937
+ const verified = await this.runPython(["-m", "pip", "--version"]);
938
+ return verified.ok ? { ok: true } : this.venvFailure(verified.error ?? verified.stderr);
939
+ }
674
940
  async installManaged(onProgress) {
675
941
  mkdirSync(this.runtimeDir, { recursive: true });
676
942
  mkdirSync(this.cacheDir, { recursive: true });
677
- if (!this._exists(this.pythonPath)) {
678
- const python = await this.findPython();
679
- if (!python)
680
- return { ok: false, error: "python-missing" };
681
- onProgress?.(`creating isolated Python venv at ${this.venvDir}`);
682
- const venv = await this._runCommand(python.command, [...python.args, "-m", "venv", this.venvDir]);
683
- if (!venv.ok)
684
- return { ok: false, error: `venv-fail: ${sanitizeCommandOutput(venv.error ?? venv.stderr)}` };
943
+ if (this._exists(this.pythonPath)) {
944
+ const repaired = await this.repairExistingVenv(onProgress);
945
+ if (!repaired.ok)
946
+ return repaired;
685
947
  }
686
948
  if (!this._exists(this.pythonPath)) {
687
- return { ok: false, error: `venv-fail: ${this.pythonPath} was not created` };
949
+ const created = await this.createVenv(onProgress);
950
+ if (!created.ok)
951
+ return created;
688
952
  }
953
+ const pipReady = await this.ensurePipAvailable(onProgress);
954
+ if (!pipReady.ok)
955
+ return pipReady;
689
956
  onProgress?.("upgrading pip inside pi-managed venv");
690
957
  const pip = await this.runPython(["-m", "pip", "install", "--upgrade", "pip"], onProgress);
691
958
  if (!pip.ok)