loopengine 0.1.32 → 0.1.34

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.
@@ -27,6 +27,17 @@ export interface AbilityManifest {
27
27
  skills?: string[];
28
28
  actauth?: string;
29
29
  env?: AbilityEnvDecl[];
30
+ /** Real npm package names (dependencies + optionalDependencies) the
31
+ * ability's own tool files import — read off package.json, not
32
+ * declared in loopengine.ability.json itself. installAbility only
33
+ * ever copies the ability's *files* into the consuming project, never
34
+ * touches its package.json/node_modules (see installAbility's own
35
+ * doc comment for why), so a tool that imports a real package like
36
+ * `sharp` will fail at runtime with a bare "Cannot find package"
37
+ * error unless the operator separately runs `npm install` for it —
38
+ * this is what lets a caller (the Admin UI's Abilities tab) warn
39
+ * about that up front instead of leaving it as a silent trap. */
40
+ dependencies?: string[];
30
41
  }
31
42
  /** One agent's record of one installed ability — the merge base a
32
43
  * future upgrade needs, what remove needs to know is safe to delete,
@@ -112,6 +123,7 @@ export interface FetchOptions {
112
123
  * install is refused, before a single file is written. */
113
124
  export declare function installAbility(agentName: string, spec: string, options?: FetchOptions): Promise<{
114
125
  installed: string[];
126
+ dependencies: string[];
115
127
  }>;
116
128
  export interface UpgradeFileResult {
117
129
  path: string;
@@ -212,7 +212,8 @@ function readManifest(abilityDir) {
212
212
  if (!pkgJson.name || !pkgJson.version) {
213
213
  throw new AbilityManifestError(`${pkgJsonPath} must have "name" and "version".`);
214
214
  }
215
- return { ...declared, name: pkgJson.name, version: pkgJson.version };
215
+ const dependencies = [...Object.keys(pkgJson.dependencies ?? {}), ...Object.keys(pkgJson.optionalDependencies ?? {})];
216
+ return { ...declared, name: pkgJson.name, version: pkgJson.version, dependencies };
216
217
  }
217
218
  // The installing project's own dependencies.loopengine is itself a
218
219
  // range (e.g. "^0.1.10"), not a concrete installed version — checking
@@ -456,7 +457,10 @@ export async function installAbility(agentName, spec, options = {}) {
456
457
  contentHashes,
457
458
  };
458
459
  writeProvenance(agentName, provenance);
459
- return { installed: [...toolNames.map((n) => `tools/${n}.ts`), ...skillIds.map((id) => `skills/${id}/`), ...ruleNames.map((n) => `actauth:${n}`)] };
460
+ return {
461
+ installed: [...toolNames.map((n) => `tools/${n}.ts`), ...skillIds.map((id) => `skills/${id}/`), ...ruleNames.map((n) => `actauth:${n}`)],
462
+ dependencies: manifest.dependencies ?? [],
463
+ };
460
464
  }
461
465
  // ---- Upgrade ----
462
466
  // git merge-file's own exit code *is* its conflict count (0 = clean),
@@ -1285,7 +1285,19 @@ export const agentsConfigPageHtml = `<!doctype html>
1285
1285
  // doesn't splice into an already-running server's in-memory
1286
1286
  // registry any more than the CLI does (see installAbility's own
1287
1287
  // doc comment on why), so the operator needs the same nudge here.
1288
- statusEl.textContent = 'Installed: ' + result.body.installed.join(', ') + '. Already running under npx loopengine dev? It becomes active automatically. Running under serve (or nothing yet)? Restart the server to pick it up.';
1288
+ var message = 'Installed: ' + escapeHtml(result.body.installed.join(', ')) + '. Already running under npx loopengine dev? It becomes active automatically. Running under serve (or nothing yet)? Restart the server to pick it up.';
1289
+ var deps = result.body.dependencies || [];
1290
+ if (deps.length) {
1291
+ // installAbility only ever copies the ability's own files —
1292
+ // never touches package.json/node_modules — so a real npm
1293
+ // dependency the ability's own tool code imports (e.g. sharp)
1294
+ // has to be installed separately, or the tool fails at runtime
1295
+ // with an opaque "Cannot find package" the very next time this
1296
+ // agent's config is loaded. Surfaced here, loudly, instead of
1297
+ // leaving that as a silent trap discovered later.
1298
+ message += ' <strong>This ability also needs: <code>npm install ' + escapeHtml(deps.join(' ')) + '</code></strong> in your own project before it will actually work.';
1299
+ }
1300
+ statusEl.innerHTML = message;
1289
1301
  loadAbilitiesTab(name);
1290
1302
  })
1291
1303
  .catch(function (err) {
@@ -2175,9 +2187,14 @@ export const agentsConfigPageHtml = `<!doctype html>
2175
2187
  // so only Overview is left needing a re-fetch.
2176
2188
  function refreshOverviewPanel(name) {
2177
2189
  fetch('/agents/' + encodeURIComponent(name) + '/config')
2178
- .then(function (r) { return r.json(); })
2179
- .then(function (cfg) {
2190
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
2191
+ .then(function (result) {
2180
2192
  if (currentName !== name) return;
2193
+ // A non-ok body is {error: "..."}, not a real config — bail out
2194
+ // to the catch below rather than rendering it as one (same risk
2195
+ // selectAgent's own /config fetch has — see its own comment).
2196
+ if (!result.ok) throw new Error(result.body.error || 'request failed');
2197
+ var cfg = result.body;
2181
2198
  currentCfg = cfg;
2182
2199
  var overviewPanel = detail.querySelector('[data-tab-panel="overview"]');
2183
2200
  if (overviewPanel) {
@@ -2213,9 +2230,13 @@ export const agentsConfigPageHtml = `<!doctype html>
2213
2230
  // re-rendering both it and Overview from a fresh /config fetch.
2214
2231
  function refreshSkillsDependentPanels(name) {
2215
2232
  fetch('/agents/' + encodeURIComponent(name) + '/config')
2216
- .then(function (r) { return r.json(); })
2217
- .then(function (cfg) {
2233
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
2234
+ .then(function (result) {
2218
2235
  if (currentName !== name) return;
2236
+ // Same "don't render an {error: ...} body as if it were a real
2237
+ // config" guard as refreshOverviewPanel/selectAgent above.
2238
+ if (!result.ok) throw new Error(result.body.error || 'request failed');
2239
+ var cfg = result.body;
2219
2240
  currentCfg = cfg;
2220
2241
  var overviewPanel = detail.querySelector('[data-tab-panel="overview"]');
2221
2242
  var skillsPanel = skillsPanelEl();
@@ -2283,10 +2304,18 @@ export const agentsConfigPageHtml = `<!doctype html>
2283
2304
  empty.style.display = 'none';
2284
2305
  detail.style.display = 'block';
2285
2306
  fetch('/agents/' + encodeURIComponent(name) + '/config')
2286
- .then(function (r) { return r.json(); })
2287
- .then(function (cfg) {
2307
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
2308
+ .then(function (result) {
2288
2309
  if (currentName !== name) return;
2289
- renderDetail(cfg);
2310
+ // A non-ok response's body is {error: "..."}, not a real config
2311
+ // — rendering it as one would crash somewhere deep in
2312
+ // renderDetail (e.g. reading .model.provider off an object with
2313
+ // no .model at all) with a confusing, unrelated-looking message
2314
+ // instead of the actual server-side error this route already
2315
+ // went out of its way to surface clearly (see handleAgentConfig
2316
+ // Get's own doc comment).
2317
+ if (!result.ok) throw new Error(result.body.error || 'request failed');
2318
+ renderDetail(result.body);
2290
2319
  })
2291
2320
  .catch(function (err) {
2292
2321
  if (currentName !== name) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",