@camstack/agent 1.0.3 → 1.0.5

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/dist/cli.js CHANGED
@@ -27,8 +27,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var os3 = __toESM(require("os"));
28
28
 
29
29
  // src/agent-bootstrap.ts
30
- var fs4 = __toESM(require("fs"));
31
- var path4 = __toESM(require("path"));
30
+ var fs6 = __toESM(require("fs"));
31
+ var path6 = __toESM(require("path"));
32
32
 
33
33
  // src/agent-http.ts
34
34
  var import_fastify = __toESM(require("fastify"));
@@ -219,8 +219,8 @@ async function startAgentHttpServer(getBroker, config) {
219
219
 
220
220
  // src/agent-bootstrap.ts
221
221
  var import_system3 = require("@camstack/system");
222
- var import_types = require("@camstack/types");
223
222
  var import_types2 = require("@camstack/types");
223
+ var import_types3 = require("@camstack/types");
224
224
 
225
225
  // src/agent-config.ts
226
226
  var fs2 = __toESM(require("fs"));
@@ -288,10 +288,87 @@ function loadAgentConfig(configPath, dataDirOverride) {
288
288
 
289
289
  // src/agent-service.ts
290
290
  var os2 = __toESM(require("os"));
291
- var fs3 = __toESM(require("fs"));
292
- var path3 = __toESM(require("path"));
291
+ var fs4 = __toESM(require("fs"));
292
+ var path4 = __toESM(require("path"));
293
293
  var import_moleculer = require("moleculer");
294
294
  var import_system = require("@camstack/system");
295
+
296
+ // src/agent-deploy-swap.ts
297
+ var fs3 = __toESM(require("fs"));
298
+ var path3 = __toESM(require("path"));
299
+ var import_node_crypto = require("crypto");
300
+ function rmrf(target) {
301
+ fs3.rmSync(target, { recursive: true, force: true });
302
+ }
303
+ function moveDir(from, to) {
304
+ try {
305
+ fs3.renameSync(from, to);
306
+ } catch (err) {
307
+ const code = err.code;
308
+ if (code === "EXDEV") {
309
+ fs3.cpSync(from, to, { recursive: true });
310
+ rmrf(from);
311
+ } else {
312
+ throw err;
313
+ }
314
+ }
315
+ }
316
+ async function applyDeployedBundle(input) {
317
+ const { addonsDir, addonId, bundle, extract, logger } = input;
318
+ fs3.mkdirSync(addonsDir, { recursive: true });
319
+ const liveDir = path3.join(addonsDir, addonId);
320
+ const liveParent = path3.dirname(liveDir);
321
+ const liveBase = path3.basename(liveDir);
322
+ fs3.mkdirSync(liveParent, { recursive: true });
323
+ const token = (0, import_node_crypto.randomBytes)(6).toString("hex");
324
+ const nextDir = path3.join(liveParent, `.${liveBase}.next.${token}`);
325
+ const backupDir = path3.join(liveParent, `.${liveBase}.backup.${token}`);
326
+ fs3.mkdirSync(nextDir, { recursive: true });
327
+ try {
328
+ await extract(bundle, nextDir);
329
+ } catch (err) {
330
+ rmrf(nextDir);
331
+ throw err;
332
+ }
333
+ const hadLive = fs3.existsSync(liveDir);
334
+ if (hadLive) {
335
+ fs3.renameSync(liveDir, backupDir);
336
+ }
337
+ try {
338
+ moveDir(nextDir, liveDir);
339
+ } catch (swapErr) {
340
+ if (hadLive) {
341
+ try {
342
+ if (fs3.existsSync(liveDir)) rmrf(liveDir);
343
+ fs3.renameSync(backupDir, liveDir);
344
+ } catch (restoreErr) {
345
+ logger.error(
346
+ `agent deploy swap: restore of "${addonId}" failed after a failed swap \u2014 manual recovery may be needed`,
347
+ {
348
+ meta: {
349
+ backupDir,
350
+ error: restoreErr instanceof Error ? restoreErr.message : String(restoreErr)
351
+ }
352
+ }
353
+ );
354
+ }
355
+ }
356
+ rmrf(nextDir);
357
+ throw swapErr;
358
+ }
359
+ if (hadLive) rmrf(backupDir);
360
+ return { addonDir: liveDir };
361
+ }
362
+
363
+ // src/agent-service.ts
364
+ var deploySwapLogger = {
365
+ info: (msg) => console.log(`[Agent] ${msg}`),
366
+ warn: (msg) => console.warn(`[Agent] ${msg}`),
367
+ error: (msg) => console.error(`[Agent] ${msg}`),
368
+ debug: (msg) => console.debug(`[Agent] ${msg}`),
369
+ child: () => deploySwapLogger,
370
+ withTags: () => deploySwapLogger
371
+ };
295
372
  function getLocalIps() {
296
373
  const interfaces = os2.networkInterfaces();
297
374
  const ips = [];
@@ -304,11 +381,23 @@ function getLocalIps() {
304
381
  }
305
382
  return ips;
306
383
  }
384
+ async function withTimeout(p, ms, fallback) {
385
+ let timer;
386
+ const timeout = new Promise((resolve5) => {
387
+ timer = setTimeout(() => resolve5(fallback), ms);
388
+ });
389
+ try {
390
+ return await Promise.race([p, timeout]);
391
+ } finally {
392
+ if (timer) clearTimeout(timer);
393
+ }
394
+ }
395
+ var METRICS_SNAPSHOT_TIMEOUT_MS = 2e3;
307
396
  function readHubAddressFromConfig(configPath) {
308
397
  if (!configPath) return null;
309
398
  try {
310
- if (!fs3.existsSync(configPath)) return null;
311
- const raw = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
399
+ if (!fs4.existsSync(configPath)) return null;
400
+ const raw = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
312
401
  return typeof raw.hubAddress === "string" && raw.hubAddress.length > 0 ? raw.hubAddress : null;
313
402
  } catch {
314
403
  return null;
@@ -316,9 +405,9 @@ function readHubAddressFromConfig(configPath) {
316
405
  }
317
406
  function readDeployedAddonIds(addonDir) {
318
407
  try {
319
- const manifestPath = path3.join(addonDir, "package.json");
320
- if (!fs3.existsSync(manifestPath)) return [];
321
- const raw = JSON.parse(fs3.readFileSync(manifestPath, "utf-8"));
408
+ const manifestPath = path4.join(addonDir, "package.json");
409
+ if (!fs4.existsSync(manifestPath)) return [];
410
+ const raw = JSON.parse(fs4.readFileSync(manifestPath, "utf-8"));
322
411
  const entries = raw.camstack?.addons ?? [];
323
412
  const ids = [];
324
413
  for (const entry of entries) {
@@ -350,7 +439,11 @@ function createAgentService(deps) {
350
439
  let memoryPercent = 0;
351
440
  const metrics = deps.getMetricsProvider?.();
352
441
  if (metrics) {
353
- const snapshot = await metrics.getCached();
442
+ const snapshot = await withTimeout(
443
+ metrics.getCached(),
444
+ METRICS_SNAPSHOT_TIMEOUT_MS,
445
+ null
446
+ );
354
447
  if (snapshot) {
355
448
  cpuPercent = snapshot.cpu.total;
356
449
  memoryPercent = snapshot.memory.percent;
@@ -373,7 +466,9 @@ function createAgentService(deps) {
373
466
  addons: [...deps.loadedAddons.values()].map((a) => ({
374
467
  id: a.id,
375
468
  status: a.status,
376
- version: a.version,
469
+ // Report the PACKAGE version (falls back to the declaration
470
+ // version) so the hub's per-node update check is accurate.
471
+ version: a.packageVersion ?? a.version,
377
472
  packageName: a.packageName
378
473
  }))
379
474
  };
@@ -387,7 +482,11 @@ function createAgentService(deps) {
387
482
  const metrics = deps.getMetricsProvider?.();
388
483
  if (metrics) {
389
484
  try {
390
- const snapshot = await metrics.getCached();
485
+ const snapshot = await withTimeout(
486
+ metrics.getCached(),
487
+ METRICS_SNAPSHOT_TIMEOUT_MS,
488
+ null
489
+ );
391
490
  if (snapshot) {
392
491
  cpuPercent = snapshot.cpu.total;
393
492
  memoryPercent = snapshot.memory.percent;
@@ -437,17 +536,17 @@ function createAgentService(deps) {
437
536
  deps.agentName = newName.trim();
438
537
  broker.logger.info(`Agent renamed: "${oldName}" \u2192 "${deps.agentName}"`);
439
538
  try {
440
- const configFile = path3.resolve(deps.configPath);
539
+ const configFile = path4.resolve(deps.configPath);
441
540
  let raw = {};
442
- if (fs3.existsSync(configFile)) {
541
+ if (fs4.existsSync(configFile)) {
443
542
  try {
444
- raw = JSON.parse(fs3.readFileSync(configFile, "utf-8"));
543
+ raw = JSON.parse(fs4.readFileSync(configFile, "utf-8"));
445
544
  } catch {
446
545
  }
447
546
  }
448
547
  raw.name = deps.agentName;
449
- fs3.mkdirSync(path3.dirname(configFile), { recursive: true });
450
- fs3.writeFileSync(configFile, JSON.stringify(raw, null, 2), "utf-8");
548
+ fs4.mkdirSync(path4.dirname(configFile), { recursive: true });
549
+ fs4.writeFileSync(configFile, JSON.stringify(raw, null, 2), "utf-8");
451
550
  broker.logger.info(`Agent name persisted to ${configFile}`);
452
551
  } catch (err) {
453
552
  broker.logger.warn(
@@ -467,20 +566,31 @@ function createAgentService(deps) {
467
566
  handler: async (ctx) => {
468
567
  const { params } = ctx;
469
568
  const { addonId, bundle } = params;
470
- const addonDir = path3.join(deps.addonsDir, addonId);
471
- fs3.mkdirSync(deps.addonsDir, { recursive: true });
472
- const bundlePath = path3.join(deps.addonsDir, `${addonId}.tgz`);
473
569
  const bufferData = typeof bundle === "string" ? Buffer.from(bundle, "base64") : bundle;
474
- fs3.writeFileSync(bundlePath, bufferData);
475
- const { execFileSync } = await import("child_process");
476
- if (fs3.existsSync(addonDir)) {
477
- fs3.rmSync(addonDir, { recursive: true, force: true });
478
- }
479
- fs3.mkdirSync(addonDir, { recursive: true });
480
- execFileSync("tar", ["-xzf", bundlePath, "-C", addonDir, "--strip-components=1"], {
481
- timeout: 3e4
570
+ const { execFile } = await import("child_process");
571
+ const { promisify } = await import("util");
572
+ const execFileAsync = promisify(execFile);
573
+ const extract = async (tgz, destDir) => {
574
+ const tgzPath = path4.join(destDir, "..", `.${path4.basename(destDir)}.tgz`);
575
+ fs4.writeFileSync(tgzPath, tgz);
576
+ try {
577
+ await execFileAsync("tar", ["-xzf", tgzPath, "-C", destDir, "--strip-components=1"], {
578
+ timeout: 6e4
579
+ });
580
+ } finally {
581
+ try {
582
+ fs4.unlinkSync(tgzPath);
583
+ } catch {
584
+ }
585
+ }
586
+ };
587
+ const { addonDir } = await applyDeployedBundle({
588
+ addonsDir: deps.addonsDir,
589
+ addonId,
590
+ bundle: bufferData,
591
+ extract,
592
+ logger: deploySwapLogger
482
593
  });
483
- fs3.unlinkSync(bundlePath);
484
594
  for (const declId of readDeployedAddonIds(addonDir)) {
485
595
  deps.loadedAddons.delete(declId);
486
596
  }
@@ -527,15 +637,15 @@ function createAgentService(deps) {
527
637
  } catch {
528
638
  }
529
639
  deps.loadedAddons.delete(addonId);
530
- const addonDir = path3.join(deps.addonsDir, addonId);
531
- if (fs3.existsSync(addonDir)) {
532
- fs3.rmSync(addonDir, { recursive: true, force: true });
640
+ const addonDir = path4.join(deps.addonsDir, addonId);
641
+ if (fs4.existsSync(addonDir)) {
642
+ fs4.rmSync(addonDir, { recursive: true, force: true });
533
643
  }
534
644
  const pkgName = entry?.packageName;
535
645
  if (pkgName && pkgName !== addonId) {
536
- const pkgDir = path3.join(deps.addonsDir, pkgName);
537
- if (fs3.existsSync(pkgDir)) {
538
- fs3.rmSync(pkgDir, { recursive: true, force: true });
646
+ const pkgDir = path4.join(deps.addonsDir, pkgName);
647
+ if (fs4.existsSync(pkgDir)) {
648
+ fs4.rmSync(pkgDir, { recursive: true, force: true });
539
649
  }
540
650
  }
541
651
  broker.logger.info(`$agent.undeploy: ${addonId} disposed (instance + service + folder)`);
@@ -589,6 +699,84 @@ function createAgentService(deps) {
589
699
  };
590
700
  }
591
701
 
702
+ // src/agent-group-runner.ts
703
+ var fs5 = __toESM(require("fs"));
704
+ var path5 = __toESM(require("path"));
705
+ var import_types = require("@camstack/types");
706
+ function readPackageManifestAddons(dir) {
707
+ try {
708
+ const pkgPath = path5.join(dir, "package.json");
709
+ if (!fs5.existsSync(pkgPath)) return null;
710
+ const raw = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
711
+ const packageName = typeof raw.name === "string" ? raw.name : "";
712
+ const packageVersion = typeof raw.version === "string" ? raw.version : "0.0.0";
713
+ const entries = Array.isArray(raw.camstack?.addons) ? raw.camstack.addons : [];
714
+ const declarations = [];
715
+ for (const entry of entries) {
716
+ if (entry !== null && typeof entry === "object" && typeof entry.id === "string") {
717
+ declarations.push(entry);
718
+ }
719
+ }
720
+ if (!packageName || declarations.length === 0) return null;
721
+ return { packageName, packageVersion, declarations };
722
+ } catch {
723
+ return null;
724
+ }
725
+ }
726
+ function isGroupRunnerAddon(decl) {
727
+ return decl.execution !== void 0 && (0, import_types.resolveAddonPlacement)(decl) !== "hub-only";
728
+ }
729
+ function registerGroupRunnerProviders(deps, addons) {
730
+ for (const a of addons) {
731
+ for (const cap of a.capabilities) {
732
+ const capName = typeof cap === "string" ? cap : cap.name;
733
+ const proxy = new Proxy(
734
+ {},
735
+ {
736
+ get(_target, prop) {
737
+ if (prop === "then" || typeof prop === "symbol") return void 0;
738
+ return (params) => deps.broker.call(`${a.addonId}.${capName}.${prop}`, params);
739
+ }
740
+ }
741
+ );
742
+ deps.capabilityRegistry.registerProvider(capName, a.addonId, proxy);
743
+ }
744
+ deps.loadedAddons.set(a.addonId, {
745
+ id: a.addonId,
746
+ status: "running",
747
+ version: a.version,
748
+ packageName: a.packageName,
749
+ packageVersion: a.packageVersion
750
+ });
751
+ }
752
+ }
753
+ async function ensureGroupRunner(deps, groupId, addons) {
754
+ try {
755
+ const restart = await deps.broker.call("$process.restart", { name: groupId });
756
+ if (!restart.success) {
757
+ if (restart.reason !== void 0 && restart.reason !== "not found") {
758
+ deps.logger.warn(
759
+ `group "${groupId}" restart returned "${restart.reason}" \u2014 spawning a fresh runner`
760
+ );
761
+ }
762
+ await deps.broker.call("$process.spawnRunner", {
763
+ runnerId: groupId,
764
+ addons: addons.map((a) => ({ addonId: a.addonId, addonDir: a.addonDir }))
765
+ });
766
+ }
767
+ registerGroupRunnerProviders(deps, addons);
768
+ deps.logger.info(
769
+ `group "${groupId}" live with ${addons.length} addon(s): ${addons.map((a) => a.addonId).join(", ")}`
770
+ );
771
+ } catch (err) {
772
+ const msg = err instanceof Error ? err.stack ?? err.message : String(err);
773
+ deps.logger.error(`failed to ensure group "${groupId}": ${msg}`);
774
+ for (const a of addons) {
775
+ deps.loadedAddons.set(a.addonId, { id: a.addonId, status: "error" });
776
+ }
777
+ }
778
+ }
779
+
592
780
  // src/agent-cap-dispatch-service.ts
593
781
  var import_system2 = require("@camstack/system");
594
782
  function narrowParams(raw) {
@@ -670,7 +858,7 @@ async function startAgent(configPath) {
670
858
  const bundledDir = process.env["CAMSTACK_BUNDLED_ADDONS_DIR"];
671
859
  let workspaceDir = null;
672
860
  let resolvedSource = explicitSource;
673
- if (bundledDir && fs4.existsSync(bundledDir)) {
861
+ if (bundledDir && fs6.existsSync(bundledDir)) {
674
862
  workspaceDir = bundledDir;
675
863
  resolvedSource = "local";
676
864
  console.log(`[Agent] Using bundled addons from ${bundledDir}`);
@@ -789,17 +977,17 @@ async function startAgent(configPath) {
789
977
  };
790
978
  const capabilityRegistry = new import_system3.CapabilityRegistry(consoleLogger);
791
979
  const agentCapabilities = [
792
- import_types2.storageCapability,
793
- import_types2.storageProviderCapability,
794
- import_types2.settingsStoreCapability,
795
- import_types2.logDestinationCapability,
796
- import_types2.metricsProviderCapability,
797
- import_types2.decoderCapability,
798
- import_types2.motionDetectionCapability,
799
- import_types2.pipelineExecutorCapability,
800
- import_types2.pipelineRunnerCapability,
801
- import_types2.audioAnalyzerCapability,
802
- import_types2.platformProbeCapability
980
+ import_types3.storageCapability,
981
+ import_types3.storageProviderCapability,
982
+ import_types3.settingsStoreCapability,
983
+ import_types3.logDestinationCapability,
984
+ import_types3.metricsProviderCapability,
985
+ import_types3.decoderCapability,
986
+ import_types3.motionDetectionCapability,
987
+ import_types3.pipelineExecutorCapability,
988
+ import_types3.pipelineRunnerCapability,
989
+ import_types3.audioAnalyzerCapability,
990
+ import_types3.platformProbeCapability
803
991
  ];
804
992
  for (const cap of agentCapabilities) {
805
993
  capabilityRegistry.declareCapability(cap);
@@ -998,7 +1186,7 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
998
1186
  try {
999
1187
  await loader.loadFromAddonDir(dir);
1000
1188
  } catch (err) {
1001
- console.warn(`[Agent] Failed to scan ${dir}: ${(0, import_types2.errMsg)(err)}`);
1189
+ console.warn(`[Agent] Failed to scan ${dir}: ${(0, import_types3.errMsg)(err)}`);
1002
1190
  }
1003
1191
  }
1004
1192
  for (const infra of AGENT_INFRA) {
@@ -1027,10 +1215,15 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
1027
1215
  storageProvider,
1028
1216
  addonConfig: { rootPath: config.dataDir },
1029
1217
  createLogger: loggerFactory,
1030
- capabilityRegistry: registry
1218
+ capabilityRegistry: registry,
1219
+ // Feed the storage-orchestrator its first-boot seed declarations
1220
+ // (addons-data:default, models:default, …). Without this the agent's
1221
+ // sqlite-settings aborts boot: "No default storage location
1222
+ // configured for type addons-data" → fatal crash-loop.
1223
+ listStorageLocationDeclarations: () => loader.listStorageLocationDeclarations()
1031
1224
  }
1032
1225
  );
1033
- const initResult = (0, import_types.normalizeAddonInitResult)(await instance.initialize(context));
1226
+ const initResult = (0, import_types2.normalizeAddonInitResult)(await instance.initialize(context));
1034
1227
  for (const reg of initResult?.providers ?? []) {
1035
1228
  const capName = reg.capability.name;
1036
1229
  registry.registerProvider(capName, addonId, reg.provider);
@@ -1041,11 +1234,12 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
1041
1234
  status: "running",
1042
1235
  version: addon.declaration.version,
1043
1236
  packageName: addon.packageName,
1237
+ packageVersion: addon.packageVersion,
1044
1238
  addon: instance
1045
1239
  });
1046
1240
  console.log(`[Agent] Core addon "${addonId}" initialized`);
1047
1241
  } catch (err) {
1048
- const msg = (0, import_types2.errMsg)(err);
1242
+ const msg = (0, import_types3.errMsg)(err);
1049
1243
  console.error(`[Agent] Failed to initialize core addon "${addonId}": ${msg}`);
1050
1244
  if (infra.required) {
1051
1245
  throw new Error(`Required infrastructure addon "${addonId}" failed: ${msg}`, { cause: err });
@@ -1063,21 +1257,22 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
1063
1257
  try {
1064
1258
  await loader.loadFromAddonDir(dir);
1065
1259
  } catch (err) {
1066
- console.warn(`[Agent] Skipping ${dir}: ${(0, import_types2.errMsg)(err)}`);
1260
+ console.warn(`[Agent] Skipping ${dir}: ${(0, import_types3.errMsg)(err)}`);
1067
1261
  continue;
1068
1262
  }
1069
1263
  dirToLoader.set(dir, loader);
1070
1264
  for (const registered of loader.listAddons()) {
1071
1265
  if (loadedAddons.has(registered.declaration.id)) continue;
1072
1266
  if (registered.declaration.execution === void 0) continue;
1073
- const placement = (0, import_types.resolveAddonPlacement)(registered.declaration);
1267
+ const placement = (0, import_types2.resolveAddonPlacement)(registered.declaration);
1074
1268
  if (placement === "hub-only") continue;
1075
1269
  allGroupCandidates.push({
1076
- groupId: (0, import_types.resolveRunnerId)(registered.declaration, registered.declaration.id),
1270
+ groupId: (0, import_types2.resolveRunnerId)(registered.declaration, registered.declaration.id),
1077
1271
  addonId: registered.declaration.id,
1078
1272
  addonDir: dir,
1079
1273
  version: registered.declaration.version ?? "0.0.0",
1080
1274
  packageName: registered.packageName,
1275
+ packageVersion: registered.packageVersion,
1081
1276
  capabilities: registered.declaration.capabilities ?? []
1082
1277
  });
1083
1278
  }
@@ -1095,27 +1290,7 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
1095
1290
  runnerId: groupId,
1096
1291
  addons: addons.map((a) => ({ addonId: a.addonId, addonDir: a.addonDir }))
1097
1292
  });
1098
- for (const a of addons) {
1099
- for (const cap of a.capabilities) {
1100
- const capName = typeof cap === "string" ? cap : cap.name;
1101
- const proxy = new Proxy(
1102
- {},
1103
- {
1104
- get(_target, prop) {
1105
- if (prop === "then" || typeof prop === "symbol") return void 0;
1106
- return (params) => broker.call(`${a.addonId}.${capName}.${prop}`, params);
1107
- }
1108
- }
1109
- );
1110
- capabilityRegistry.registerProvider(capName, a.addonId, proxy);
1111
- }
1112
- loadedAddons.set(a.addonId, {
1113
- id: a.addonId,
1114
- status: "running",
1115
- version: a.version,
1116
- packageName: a.packageName
1117
- });
1118
- }
1293
+ registerGroupRunnerProviders({ broker, capabilityRegistry, loadedAddons }, addons);
1119
1294
  console.log(
1120
1295
  `[Agent] Group "${groupId}" spawned with ${addons.length} addon(s): ${addons.map((a) => a.addonId).join(", ")}`
1121
1296
  );
@@ -1134,7 +1309,7 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
1134
1309
  for (const registered of loader.listAddons()) {
1135
1310
  const addonId = registered.declaration.id;
1136
1311
  if (loadedAddons.has(addonId)) continue;
1137
- if (!(0, import_types.isDeployableToAgent)(registered.declaration)) continue;
1312
+ if (!(0, import_types2.isDeployableToAgent)(registered.declaration)) continue;
1138
1313
  console.warn(
1139
1314
  `[Agent] Addon "${addonId}" is deployable but missing from any spawned group \u2014 verify package.json execution field`
1140
1315
  );
@@ -1142,9 +1317,66 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
1142
1317
  }
1143
1318
  }
1144
1319
  async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, storageProvider, loggerFactory, capabilityRegistry) {
1145
- if (!fs4.existsSync(addonsDir)) return;
1146
- const loader = new import_system3.AddonLoader();
1147
- await loader.loadFromDirectory(addonsDir);
1320
+ if (!fs6.existsSync(addonsDir)) return;
1321
+ const packageDirs = resolveAddonPackageDirs(addonsDir);
1322
+ const groupCandidates = [];
1323
+ const inProcessDirs = /* @__PURE__ */ new Set();
1324
+ for (const dir of packageDirs) {
1325
+ const manifest = readPackageManifestAddons(dir);
1326
+ if (!manifest) continue;
1327
+ for (const decl of manifest.declarations) {
1328
+ const addonId = decl.id;
1329
+ if (loadedAddons.has(addonId)) continue;
1330
+ if (!(0, import_types2.isDeployableToAgent)(decl)) continue;
1331
+ if (isGroupRunnerAddon(decl)) {
1332
+ groupCandidates.push({
1333
+ groupId: (0, import_types2.resolveRunnerId)(decl, addonId),
1334
+ addonId,
1335
+ addonDir: dir,
1336
+ version: decl.version ?? "0.0.0",
1337
+ packageName: manifest.packageName,
1338
+ packageVersion: manifest.packageVersion,
1339
+ capabilities: decl.capabilities ?? []
1340
+ });
1341
+ } else {
1342
+ inProcessDirs.add(dir);
1343
+ }
1344
+ }
1345
+ }
1346
+ if (inProcessDirs.size > 0) {
1347
+ await loadInProcessDeployedAddons(
1348
+ broker,
1349
+ [...inProcessDirs],
1350
+ dataDir,
1351
+ loadedAddons,
1352
+ storageProvider,
1353
+ loggerFactory,
1354
+ capabilityRegistry
1355
+ );
1356
+ }
1357
+ if (groupCandidates.length === 0) return;
1358
+ if (!capabilityRegistry) {
1359
+ console.warn(
1360
+ `[Agent] ${groupCandidates.length} deployed group addon(s) skipped \u2014 no capabilityRegistry passed`
1361
+ );
1362
+ return;
1363
+ }
1364
+ const grouped = /* @__PURE__ */ new Map();
1365
+ for (const c of groupCandidates) {
1366
+ const arr = grouped.get(c.groupId) ?? [];
1367
+ arr.push(c);
1368
+ grouped.set(c.groupId, arr);
1369
+ }
1370
+ const deployLogger = loggerFactory("agent-group-runner");
1371
+ for (const [groupId, addons] of grouped) {
1372
+ await ensureGroupRunner(
1373
+ { broker, capabilityRegistry, loadedAddons, logger: deployLogger },
1374
+ groupId,
1375
+ addons
1376
+ );
1377
+ }
1378
+ }
1379
+ async function loadInProcessDeployedAddons(broker, dirs, dataDir, loadedAddons, storageProvider, loggerFactory, capabilityRegistry) {
1148
1380
  const modelsDir = storageProvider ? await storageProvider.resolve({ location: "models", relativePath: "" }).catch(() => void 0) : void 0;
1149
1381
  const contextOptions = {
1150
1382
  storageProvider,
@@ -1152,45 +1384,56 @@ async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, stor
1152
1384
  createLogger: loggerFactory,
1153
1385
  capabilityRegistry
1154
1386
  };
1155
- for (const registered of loader.listAddons()) {
1156
- const addonId = registered.declaration.id;
1157
- if (loadedAddons.has(addonId)) continue;
1158
- if (!(0, import_types.isDeployableToAgent)(registered.declaration)) continue;
1387
+ for (const dir of dirs) {
1388
+ const loader = new import_system3.AddonLoader();
1159
1389
  try {
1160
- const instance = new registered.addonClass();
1161
- const context = await (0, import_system3.createAddonContext)(
1162
- broker,
1163
- registered.declaration,
1164
- dataDir,
1165
- contextOptions
1166
- );
1167
- await instance.initialize(context);
1168
- const serviceSchema = (0, import_system3.createAddonService)(instance, registered.declaration);
1169
- broker.createService(serviceSchema);
1170
- loadedAddons.set(addonId, {
1171
- id: addonId,
1172
- status: "running",
1173
- version: registered.declaration.version,
1174
- packageName: registered.packageName,
1175
- addon: instance
1176
- });
1177
- console.log(`[Agent] Deployed addon "${addonId}" loaded`);
1390
+ await loader.loadFromAddonDir(dir);
1178
1391
  } catch (err) {
1179
- const msg = (0, import_types2.errMsg)(err);
1180
- console.error(`[Agent] Failed to load deployed addon "${addonId}": ${msg}`);
1181
- loadedAddons.set(addonId, { id: addonId, status: "error" });
1392
+ console.warn(`[Agent] Skipping ${dir}: ${(0, import_types3.errMsg)(err)}`);
1393
+ continue;
1394
+ }
1395
+ for (const registered of loader.listAddons()) {
1396
+ const addonId = registered.declaration.id;
1397
+ if (loadedAddons.has(addonId)) continue;
1398
+ if (!(0, import_types2.isDeployableToAgent)(registered.declaration)) continue;
1399
+ if (isGroupRunnerAddon(registered.declaration)) continue;
1400
+ try {
1401
+ const instance = new registered.addonClass();
1402
+ const context = await (0, import_system3.createAddonContext)(
1403
+ broker,
1404
+ registered.declaration,
1405
+ dataDir,
1406
+ contextOptions
1407
+ );
1408
+ await instance.initialize(context);
1409
+ const serviceSchema = (0, import_system3.createAddonService)(instance, registered.declaration);
1410
+ broker.createService(serviceSchema);
1411
+ loadedAddons.set(addonId, {
1412
+ id: addonId,
1413
+ status: "running",
1414
+ version: registered.declaration.version,
1415
+ packageName: registered.packageName,
1416
+ packageVersion: registered.packageVersion,
1417
+ addon: instance
1418
+ });
1419
+ console.log(`[Agent] Deployed addon "${addonId}" loaded in-process`);
1420
+ } catch (err) {
1421
+ const msg = (0, import_types3.errMsg)(err);
1422
+ console.error(`[Agent] Failed to load deployed addon "${addonId}": ${msg}`);
1423
+ loadedAddons.set(addonId, { id: addonId, status: "error" });
1424
+ }
1182
1425
  }
1183
1426
  }
1184
1427
  }
1185
1428
  function readAgentVersion() {
1186
1429
  const candidates = [
1187
- path4.resolve(__dirname, "..", "package.json"),
1188
- path4.resolve(__dirname, "..", "..", "package.json")
1430
+ path6.resolve(__dirname, "..", "package.json"),
1431
+ path6.resolve(__dirname, "..", "..", "package.json")
1189
1432
  ];
1190
1433
  for (const candidate of candidates) {
1191
1434
  try {
1192
- if (!fs4.existsSync(candidate)) continue;
1193
- const raw = JSON.parse(fs4.readFileSync(candidate, "utf-8"));
1435
+ if (!fs6.existsSync(candidate)) continue;
1436
+ const raw = JSON.parse(fs6.readFileSync(candidate, "utf-8"));
1194
1437
  if (raw.name === "@camstack/agent" && typeof raw.version === "string") return raw.version;
1195
1438
  } catch {
1196
1439
  }
@@ -1199,24 +1442,24 @@ function readAgentVersion() {
1199
1442
  }
1200
1443
  function isDir(p) {
1201
1444
  try {
1202
- return fs4.statSync(p).isDirectory();
1445
+ return fs6.statSync(p).isDirectory();
1203
1446
  } catch {
1204
1447
  return false;
1205
1448
  }
1206
1449
  }
1207
1450
  function resolveAddonPackageDirs(addonsDir) {
1208
1451
  const dirs = [];
1209
- if (!fs4.existsSync(addonsDir)) return dirs;
1210
- for (const name of fs4.readdirSync(addonsDir)) {
1211
- const full = path4.join(addonsDir, name);
1452
+ if (!fs6.existsSync(addonsDir)) return dirs;
1453
+ for (const name of fs6.readdirSync(addonsDir)) {
1454
+ const full = path6.join(addonsDir, name);
1212
1455
  if (name.startsWith("@") && isDir(full)) {
1213
- for (const sub of fs4.readdirSync(full)) {
1214
- const subFull = path4.join(full, sub);
1215
- if (isDir(subFull) && fs4.existsSync(path4.join(subFull, "package.json"))) {
1456
+ for (const sub of fs6.readdirSync(full)) {
1457
+ const subFull = path6.join(full, sub);
1458
+ if (isDir(subFull) && fs6.existsSync(path6.join(subFull, "package.json"))) {
1216
1459
  dirs.push(subFull);
1217
1460
  }
1218
1461
  }
1219
- } else if (isDir(full) && fs4.existsSync(path4.join(full, "package.json"))) {
1462
+ } else if (isDir(full) && fs6.existsSync(path6.join(full, "package.json"))) {
1220
1463
  dirs.push(full);
1221
1464
  }
1222
1465
  }