@caupulican/pi-adaptative 0.81.19 → 0.81.21

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 (38) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +17 -0
  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/models/context-sizing.d.ts.map +1 -1
  13. package/dist/core/models/context-sizing.js +18 -26
  14. package/dist/core/models/context-sizing.js.map +1 -1
  15. package/dist/core/models/local-runtime.d.ts +57 -4
  16. package/dist/core/models/local-runtime.d.ts.map +1 -1
  17. package/dist/core/models/local-runtime.js +307 -30
  18. package/dist/core/models/local-runtime.js.map +1 -1
  19. package/dist/core/models/perf-profile.d.ts +3 -0
  20. package/dist/core/models/perf-profile.d.ts.map +1 -1
  21. package/dist/core/models/perf-profile.js +42 -13
  22. package/dist/core/models/perf-profile.js.map +1 -1
  23. package/dist/core/models/runtime-arbiter.d.ts +45 -0
  24. package/dist/core/models/runtime-arbiter.d.ts.map +1 -1
  25. package/dist/core/models/runtime-arbiter.js +115 -0
  26. package/dist/core/models/runtime-arbiter.js.map +1 -1
  27. package/dist/modes/interactive/local-model-commands.d.ts.map +1 -1
  28. package/dist/modes/interactive/local-model-commands.js +24 -6
  29. package/dist/modes/interactive/local-model-commands.js.map +1 -1
  30. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  31. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  32. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  33. package/examples/extensions/sandbox/package-lock.json +2 -2
  34. package/examples/extensions/sandbox/package.json +1 -1
  35. package/examples/extensions/with-deps/package-lock.json +2 -2
  36. package/examples/extensions/with-deps/package.json +1 -1
  37. package/npm-shrinkwrap.json +12 -12
  38. 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";
@@ -54,7 +54,19 @@ const TRANSFORMERS_RUNTIME_DIR_NAME = "hf-transformers";
54
54
  const TRANSFORMERS_VENV_DIR_NAME = "venv";
55
55
  const TRANSFORMERS_SERVER_RELATIVE_PATH = join("runtimes", "hf-transformers-openai-server.py");
56
56
  const TRANSFORMERS_PINNED_PACKAGES = ["transformers==5.13.0", "huggingface-hub==1.22.0", "safetensors==0.8.0"];
57
+ const TRANSFORMERS_REQUIRED_MODULES = ["torch", "transformers", "huggingface_hub", "safetensors"];
57
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
+ }
58
70
  async function runCommandDefault(command, args, options = {}) {
59
71
  try {
60
72
  const proc = spawnProcess(command, args, {
@@ -112,6 +124,8 @@ export class OllamaRuntime {
112
124
  _fetch;
113
125
  _spawn;
114
126
  _exists;
127
+ _linkFile;
128
+ _copyFile;
115
129
  _envPath;
116
130
  _homeDir;
117
131
  _sleep;
@@ -121,12 +135,15 @@ export class OllamaRuntime {
121
135
  _hasCommand;
122
136
  _extractArchiveOverride;
123
137
  _child;
138
+ _childModelsDir;
124
139
  constructor(args) {
125
140
  this._agentDir = args.agentDir;
126
141
  this._baseUrl = (args.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
127
142
  this._fetch = args.deps?.fetchFn ?? fetch;
128
143
  this._spawn = args.deps?.spawnFn ?? ((command, argv, options) => spawn(command, argv, options));
129
144
  this._exists = args.deps?.existsFn ?? existsSync;
145
+ this._linkFile = args.deps?.linkFile ?? linkSync;
146
+ this._copyFile = args.deps?.copyFile ?? copyFileSync;
130
147
  this._envPath = args.deps?.envPath ?? process.env.PATH ?? "";
131
148
  this._homeDir = args.deps?.homeDir ?? homedir();
132
149
  this._sleep = args.deps?.sleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -154,6 +171,35 @@ export class OllamaRuntime {
154
171
  ownedModelsDir() {
155
172
  return join(this._agentDir, "models", "ollama");
156
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
+ }
157
203
  _findBinary() {
158
204
  const piOwned = join(this._agentDir, "runtimes", "ollama", "bin", "ollama");
159
205
  if (this._exists(piOwned))
@@ -171,27 +217,115 @@ export class OllamaRuntime {
171
217
  return undefined;
172
218
  }
173
219
  async _serverUp() {
220
+ return (await this._serverModels()).up;
221
+ }
222
+ async _serverModels() {
174
223
  try {
175
224
  const response = await this._fetch(`${this._baseUrl}/api/tags`, {
176
225
  signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),
177
226
  });
178
- 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 };
179
231
  }
180
232
  catch {
181
- return false;
233
+ return { up: false, modelCount: 0 };
234
+ }
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 };
182
253
  }
254
+ return { kind: "external", path: "external/unknown", modelCount: serverModelCount };
183
255
  }
184
256
  async detect() {
185
257
  const binary = this._findBinary();
186
- 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;
187
264
  return {
188
265
  binaryPath: binary?.path,
189
266
  binarySource: binary?.source,
190
- serverUp,
267
+ serverUp: serverModels.up,
191
268
  serverUrl: this._baseUrl,
192
- managedByPi: this._child !== undefined && serverUp,
269
+ managedByPi: this._child !== undefined && serverModels.up,
193
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,
194
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
+ }
195
329
  }
196
330
  /**
197
331
  * Manual-steps fallback text — shown when the managed install below wasn't offered (headless),
@@ -332,9 +466,9 @@ export class OllamaRuntime {
332
466
  * differs between them (owned storage vs reusing the user's own). */
333
467
  async _spawnAndPoll(binary, extraEnv) {
334
468
  const host = this._baseUrl.replace(/^https?:\/\//, "");
335
- this._child = this._spawn(binary.path, ["serve"], {
336
- env: { ...process.env, OLLAMA_HOST: host, ...extraEnv },
337
- });
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 });
338
472
  this._child.unref?.();
339
473
  for (let attempt = 0; attempt < START_POLL_ATTEMPTS; attempt++) {
340
474
  if (await this._serverUp())
@@ -398,6 +532,7 @@ export class OllamaRuntime {
398
532
  // already gone
399
533
  }
400
534
  this._child = undefined;
535
+ this._childModelsDir = undefined;
401
536
  return { stopped: true };
402
537
  }
403
538
  async list() {
@@ -409,6 +544,43 @@ export class OllamaRuntime {
409
544
  .filter((model) => typeof model.name === "string")
410
545
  .map((model) => ({ name: model.name, sizeBytes: model.size ?? 0 }));
411
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
+ }
412
584
  async show(ref) {
413
585
  try {
414
586
  const response = await this._fetch(`${this._baseUrl}/api/show`, {
@@ -609,9 +781,10 @@ export class TransformersRuntime {
609
781
  }
610
782
  }
611
783
  async detect() {
784
+ const serverUp = await this.serverUp();
612
785
  return {
613
- runtimeInstalled: this._exists(this.pythonPath),
614
- serverUp: await this.serverUp(),
786
+ runtimeInstalled: this._exists(this.pythonPath) && (serverUp || (await this.runtimeDependenciesInstalled())),
787
+ serverUp,
615
788
  baseUrl: this._baseUrl,
616
789
  modelId: this._modelId,
617
790
  venvDir: this.venvDir,
@@ -620,15 +793,11 @@ export class TransformersRuntime {
620
793
  };
621
794
  }
622
795
  pythonCandidates() {
796
+ const envPython = process.env.PYTHON?.trim();
797
+ const envCandidates = envPython ? [{ command: envPython, args: [] }] : [];
623
798
  return this._platform() === "win32"
624
- ? [
625
- { command: "py", args: ["-3"] },
626
- { command: "python", args: [] },
627
- ]
628
- : [
629
- { command: "python3", args: [] },
630
- { command: "python", args: [] },
631
- ];
799
+ ? [...envCandidates, { command: "py", args: ["-3"] }, { command: "python", args: [] }]
800
+ : [...envCandidates, { command: "python", args: [] }, { command: "python3", args: [] }];
632
801
  }
633
802
  async findPython() {
634
803
  for (const candidate of this.pythonCandidates()) {
@@ -663,21 +832,127 @@ export class TransformersRuntime {
663
832
  : undefined,
664
833
  });
665
834
  }
835
+ async runtimeDependenciesInstalled() {
836
+ if (!this._exists(this.pythonPath))
837
+ return false;
838
+ const modules = JSON.stringify(TRANSFORMERS_REQUIRED_MODULES);
839
+ const result = await this.runPython([
840
+ "-c",
841
+ `import importlib.util, sys; missing = [name for name in ${modules} if importlib.util.find_spec(name) is None]; sys.exit(1 if missing else 0)`,
842
+ ]);
843
+ return result.ok;
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
+ }
666
940
  async installManaged(onProgress) {
667
941
  mkdirSync(this.runtimeDir, { recursive: true });
668
942
  mkdirSync(this.cacheDir, { recursive: true });
669
- if (!this._exists(this.pythonPath)) {
670
- const python = await this.findPython();
671
- if (!python)
672
- return { ok: false, error: "python-missing" };
673
- onProgress?.(`creating isolated Python venv at ${this.venvDir}`);
674
- const venv = await this._runCommand(python.command, [...python.args, "-m", "venv", this.venvDir]);
675
- if (!venv.ok)
676
- 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;
677
947
  }
678
948
  if (!this._exists(this.pythonPath)) {
679
- 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;
680
952
  }
953
+ const pipReady = await this.ensurePipAvailable(onProgress);
954
+ if (!pipReady.ok)
955
+ return pipReady;
681
956
  onProgress?.("upgrading pip inside pi-managed venv");
682
957
  const pip = await this.runPython(["-m", "pip", "install", "--upgrade", "pip"], onProgress);
683
958
  if (!pip.ok)
@@ -705,6 +980,8 @@ export class TransformersRuntime {
705
980
  async downloadModel(onProgress) {
706
981
  if (!this._exists(this.pythonPath))
707
982
  return { ok: false, error: "runtime-missing" };
983
+ if (!(await this.runtimeDependenciesInstalled()))
984
+ return { ok: false, error: "runtime-dependencies-missing" };
708
985
  if (!this._exists(this._serverScriptPath))
709
986
  return { ok: false, error: `server-script-missing: ${this._serverScriptPath}` };
710
987
  mkdirSync(this.cacheDir, { recursive: true });
@@ -755,7 +1032,7 @@ export class TransformersRuntime {
755
1032
  "Hugging Face Transformers runtime is not installed for this model.",
756
1033
  `Pi can install it into its own venv at ${this.venvDir} and cache weights under ${this.cacheDir}.`,
757
1034
  "Manual equivalent:",
758
- ` python3 -m venv ${this.venvDir}`,
1035
+ ` python -m venv ${this.venvDir}`,
759
1036
  ` ${this.pythonPath} -m pip install --upgrade pip`,
760
1037
  ` ${this.pythonPath} -m pip install ${TRANSFORMERS_PINNED_PACKAGES.join(" ")}`,
761
1038
  ` ${this.pythonPath} ${this.torchInstallArgs().join(" ")}`,