loopengine 0.1.28 → 0.1.30
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# LoopEngine
|
|
2
2
|
|
|
3
|
-
[](https://github.com/loopengine-co/loopengine/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
5
|
LoopEngine is a runtime for building AI agents: a persona, a set of tools,
|
|
6
6
|
and permission rules, run through a transparent ReAct loop. No chain DSL,
|
|
@@ -52,6 +52,14 @@ export interface InstalledAbilityRecord {
|
|
|
52
52
|
* this rather than storing/refetching full content to diff. */
|
|
53
53
|
contentHashes: Record<string, string>;
|
|
54
54
|
}
|
|
55
|
+
/** Every ability currently installed for `agentName`, as recorded in
|
|
56
|
+
* .loopengine-abilities.json — powers the Admin UI's Abilities tab so an
|
|
57
|
+
* operator can see what's already there before installing something new
|
|
58
|
+
* (and get a clear collision error, via installAbility itself, rather
|
|
59
|
+
* than guessing blind). */
|
|
60
|
+
export declare function listInstalledAbilities(agentName: string): Array<{
|
|
61
|
+
name: string;
|
|
62
|
+
} & InstalledAbilityRecord>;
|
|
55
63
|
/** `npm pack <spec>` + tar extract — generalized from
|
|
56
64
|
* create-loopengine's own `fetchPublishedTemplateDir` (that function
|
|
57
65
|
* only ever fetches the hardcoded `create-loopengine@<version>`; this
|
|
@@ -117,6 +117,34 @@ function matchesInstalledToolName(abilityName, manifestToolFile, installedToolNa
|
|
|
117
117
|
const bareToolName = basename(manifestToolFile, '.ts');
|
|
118
118
|
return bareToolName === installedToolName || namespacedToolName(abilityName, bareToolName) === installedToolName;
|
|
119
119
|
}
|
|
120
|
+
/** Two abilities can each declare a rule with the same bare `name` too —
|
|
121
|
+
* unlike a genuine `actauth` scope/tool/decision collision (which would
|
|
122
|
+
* be a real policy conflict worth refusing over), a bare *name* clash is
|
|
123
|
+
* just an addressing problem: `actauth`'s own Gate.evaluate()/
|
|
124
|
+
* RuleSet.resolve() never reads a rule's `name` at all (see
|
|
125
|
+
* models.js's own ruleSpecificity/scopeKey — resolution is entirely
|
|
126
|
+
* tool+scope+when), so two identically-named rules would never actually
|
|
127
|
+
* behave ambiguously at decision time. `name` only matters as
|
|
128
|
+
* addActauthRule's own lookup key (and the Admin UI's Actauth tab
|
|
129
|
+
* :ruleName route) — the same "it's an addressing problem, not a policy
|
|
130
|
+
* one" reasoning namespacedSkillId already applies to skill ids.
|
|
131
|
+
* Namespaced under the ability's own name only when the bare name is
|
|
132
|
+
* already taken, same fallback tools/skills already get; still refuses
|
|
133
|
+
* if even *that's* somehow already taken (two same-named installs of
|
|
134
|
+
* the same ability can't happen anyway — see AbilityAlreadyInstalledError
|
|
135
|
+
* above — so this only bites on a truly pathological manual edit). */
|
|
136
|
+
function namespacedRuleName(abilityName, ruleName) {
|
|
137
|
+
return `${sanitizeForToolName(abilityName)}__${ruleName}`;
|
|
138
|
+
}
|
|
139
|
+
/** matchesInstalledToolName's own sibling for rules — upgradeAbility needs
|
|
140
|
+
* this to recompute forward from a freshly re-parsed manifest's bare rule
|
|
141
|
+
* name and compare against provenance's possibly-namespaced one, rather
|
|
142
|
+
* than comparing bare names directly (which would silently treat every
|
|
143
|
+
* namespaced rule as unmatched, reporting it 'unchanged' instead of
|
|
144
|
+
* actually diffing it against the new version). */
|
|
145
|
+
function matchesInstalledRuleName(abilityName, manifestRuleName, installedRuleName) {
|
|
146
|
+
return manifestRuleName === installedRuleName || namespacedRuleName(abilityName, manifestRuleName) === installedRuleName;
|
|
147
|
+
}
|
|
120
148
|
function provenancePath(agentName) {
|
|
121
149
|
return join(agentDir(agentName), '.loopengine-abilities.json');
|
|
122
150
|
}
|
|
@@ -126,6 +154,15 @@ function readProvenance(agentName) {
|
|
|
126
154
|
return {};
|
|
127
155
|
return JSON.parse(readFileSync(path, 'utf8'));
|
|
128
156
|
}
|
|
157
|
+
/** Every ability currently installed for `agentName`, as recorded in
|
|
158
|
+
* .loopengine-abilities.json — powers the Admin UI's Abilities tab so an
|
|
159
|
+
* operator can see what's already there before installing something new
|
|
160
|
+
* (and get a clear collision error, via installAbility itself, rather
|
|
161
|
+
* than guessing blind). */
|
|
162
|
+
export function listInstalledAbilities(agentName) {
|
|
163
|
+
const provenance = readProvenance(agentName);
|
|
164
|
+
return Object.entries(provenance).map(([name, record]) => ({ name, ...record }));
|
|
165
|
+
}
|
|
129
166
|
function writeProvenance(agentName, data) {
|
|
130
167
|
writeFileSync(provenancePath(agentName), JSON.stringify(data, null, 2) + '\n');
|
|
131
168
|
}
|
|
@@ -329,10 +366,18 @@ export async function installAbility(agentName, spec, options = {}) {
|
|
|
329
366
|
skillIds.push(installedSkillId);
|
|
330
367
|
}
|
|
331
368
|
const existingRuleNames = new Set(readActauthConfig(agentName).rules.map((r) => r.name));
|
|
369
|
+
const ruleNames = [];
|
|
332
370
|
for (const rule of rules) {
|
|
333
|
-
|
|
334
|
-
|
|
371
|
+
// The bare name if it's free; otherwise namespace under this
|
|
372
|
+
// ability's own name rather than refusing the install outright —
|
|
373
|
+
// see namespacedRuleName's own doc comment for why this is safe
|
|
374
|
+
// (a rule's `name` is an addressing key, not something `actauth`'s
|
|
375
|
+
// own resolution logic reads).
|
|
376
|
+
const installedRuleName = existingRuleNames.has(rule.name) ? namespacedRuleName(manifest.name, rule.name) : rule.name;
|
|
377
|
+
if (existingRuleNames.has(installedRuleName)) {
|
|
378
|
+
throw new AbilityCollisionError(`An actauth rule named '${installedRuleName}' already exists for '${agentName}'.`);
|
|
335
379
|
}
|
|
380
|
+
ruleNames.push(installedRuleName);
|
|
336
381
|
}
|
|
337
382
|
const contentHashes = {};
|
|
338
383
|
// Write tool files and patch tools/index.ts — pure filesystem
|
|
@@ -393,21 +438,25 @@ export async function installAbility(agentName, spec, options = {}) {
|
|
|
393
438
|
contentHashes[`skills/${skillId}/SKILL.md`] = sha256(readFileSync(skillMdPath, 'utf8'));
|
|
394
439
|
}
|
|
395
440
|
}
|
|
396
|
-
// Append actauth rules
|
|
397
|
-
|
|
398
|
-
|
|
441
|
+
// Append actauth rules — ruleNames[i] is rules[i]'s already-resolved
|
|
442
|
+
// installed name (bare, or namespaced under this ability's own name if
|
|
443
|
+
// the bare name collided — computed once, above, not recomputed here,
|
|
444
|
+
// so this loop can't disagree with the collision check that already
|
|
445
|
+
// ran against it, same reasoning the skills-copy loop above follows).
|
|
446
|
+
for (const [i, rule] of rules.entries()) {
|
|
447
|
+
addActauthRule(agentName, { ...rule, name: ruleNames[i] });
|
|
399
448
|
}
|
|
400
449
|
provenance[manifest.name] = {
|
|
401
450
|
version: manifest.version,
|
|
402
451
|
spec,
|
|
403
452
|
tools: toolNames,
|
|
404
453
|
skills: skillIds,
|
|
405
|
-
actauthRules:
|
|
454
|
+
actauthRules: ruleNames,
|
|
406
455
|
env: manifest.env ?? [],
|
|
407
456
|
contentHashes,
|
|
408
457
|
};
|
|
409
458
|
writeProvenance(agentName, provenance);
|
|
410
|
-
return { installed: [...toolNames.map((n) => `tools/${n}.ts`), ...skillIds.map((id) => `skills/${id}/`), ...
|
|
459
|
+
return { installed: [...toolNames.map((n) => `tools/${n}.ts`), ...skillIds.map((id) => `skills/${id}/`), ...ruleNames.map((n) => `actauth:${n}`)] };
|
|
411
460
|
}
|
|
412
461
|
// ---- Upgrade ----
|
|
413
462
|
// git merge-file's own exit code *is* its conflict count (0 = clean),
|
|
@@ -557,8 +606,11 @@ export async function upgradeAbility(agentName, abilityName, options = {}) {
|
|
|
557
606
|
const newRules = parseActauthRules(newDir, newManifest);
|
|
558
607
|
const currentRules = new Map(readActauthConfig(agentName).rules.map((r) => [r.name, r]));
|
|
559
608
|
for (const ruleName of record.actauthRules) {
|
|
560
|
-
|
|
561
|
-
|
|
609
|
+
// matchesInstalledRuleName, not a bare r.name === ruleName comparison
|
|
610
|
+
// — ruleName may be namespaced (see namespacedRuleName), and the
|
|
611
|
+
// freshly re-parsed old/new manifests only ever carry the bare name.
|
|
612
|
+
const oldRule = oldRules.find((r) => matchesInstalledRuleName(abilityName, r.name, ruleName));
|
|
613
|
+
const newRule = newRules.find((r) => matchesInstalledRuleName(abilityName, r.name, ruleName));
|
|
562
614
|
const current = currentRules.get(ruleName);
|
|
563
615
|
const path = `actauth:${ruleName}`;
|
|
564
616
|
if (!oldRule || !newRule || !current) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const NPM_SEARCH_TIMEOUT_MS = 10000;
|
|
2
|
+
const NPM_SEARCH_SIZE = 20;
|
|
3
|
+
export async function searchPublicAbilities(query) {
|
|
4
|
+
const text = query ? `keywords:loopengine-ability ${query}` : 'keywords:loopengine-ability';
|
|
5
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(text)}&size=${NPM_SEARCH_SIZE}`;
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
const timeout = setTimeout(() => controller.abort(), NPM_SEARCH_TIMEOUT_MS);
|
|
8
|
+
let res;
|
|
9
|
+
try {
|
|
10
|
+
res = await fetch(url, { signal: controller.signal });
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
throw new Error(`Could not reach the npm registry: ${err instanceof Error ? err.message : String(err)}`);
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
clearTimeout(timeout);
|
|
17
|
+
}
|
|
18
|
+
if (!res.ok)
|
|
19
|
+
throw new Error(`npm registry search failed (HTTP ${res.status})`);
|
|
20
|
+
const data = (await res.json());
|
|
21
|
+
return (data.objects ?? []).map((o) => ({
|
|
22
|
+
name: o.package.name,
|
|
23
|
+
description: o.package.description ?? '',
|
|
24
|
+
version: o.package.version,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
@@ -407,6 +407,8 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
407
407
|
var actauthLoadedFor = null;
|
|
408
408
|
// Same idea again, for the Environment tab's own GET .../env.
|
|
409
409
|
var envLoadedFor = null;
|
|
410
|
+
// Same idea again, for the Abilities tab's own GET .../abilities.
|
|
411
|
+
var abilitiesLoadedFor = null;
|
|
410
412
|
// The full /agents/:name/config response the currently-open agent was
|
|
411
413
|
// last rendered from — kept around so refreshActauthDependentPanels/
|
|
412
414
|
// refreshSkillsDependentPanels below can re-fetch and re-render just
|
|
@@ -1208,6 +1210,172 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
1208
1210
|
});
|
|
1209
1211
|
}
|
|
1210
1212
|
|
|
1213
|
+
// ---- Abilities tab: what's already installed for this agent, a
|
|
1214
|
+
// search box over public (npm, tagged loopengine-ability) packages,
|
|
1215
|
+
// and a plain spec field for a private ability (a git URL, a local
|
|
1216
|
+
// file:../path, or a private registry name) — both installs go
|
|
1217
|
+
// through the same POST .../abilities, since installAbility itself
|
|
1218
|
+
// already resolves any spec npm pack accepts (see
|
|
1219
|
+
// bin/ability-manager.ts's own doc comment on fetchAbilityDir).
|
|
1220
|
+
// Lazily loaded the same way Actauth/Environment are. ----
|
|
1221
|
+
|
|
1222
|
+
function renderAbilitiesTabPlaceholder() {
|
|
1223
|
+
return '<section id="abilitiesSection"><div id="abilitiesContent"><p class="hint">Loading…</p></div></section>';
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function renderInstalledAbilityRow(a) {
|
|
1227
|
+
return '<tr>' +
|
|
1228
|
+
'<td><code>' + escapeHtml(a.name) + '</code></td>' +
|
|
1229
|
+
'<td>' + escapeHtml(a.version) + '</td>' +
|
|
1230
|
+
'<td class="hint">' + escapeHtml((a.tools || []).join(', ')) + '</td>' +
|
|
1231
|
+
'</tr>';
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function renderAbilitySearchResultRow(pkg) {
|
|
1235
|
+
return '<tr>' +
|
|
1236
|
+
'<td><code>' + escapeHtml(pkg.name) + '</code> <span class="hint">' + escapeHtml(pkg.version) + '</span></td>' +
|
|
1237
|
+
'<td>' + escapeHtml(pkg.description || '') + '</td>' +
|
|
1238
|
+
'<td><button type="button" class="ability-install-btn" data-spec="' + escapeHtml(pkg.name) + '">Install</button></td>' +
|
|
1239
|
+
'</tr>';
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function renderAbilitiesConfigHtml(data) {
|
|
1243
|
+
var installed = data.abilities || [];
|
|
1244
|
+
var installedHtml = installed.length
|
|
1245
|
+
? '<table><thead><tr><th>Name</th><th>Version</th><th>Tools</th></tr></thead><tbody>' + installed.map(renderInstalledAbilityRow).join('') + '</tbody></table>'
|
|
1246
|
+
: '<p class="hint">No abilities installed for this agent yet.</p>';
|
|
1247
|
+
|
|
1248
|
+
return '<h3>Installed</h3>' + installedHtml +
|
|
1249
|
+
'<h3>Search public abilities</h3>' +
|
|
1250
|
+
'<form id="abilitySearchForm" class="add-source">' +
|
|
1251
|
+
'<input type="text" name="q" placeholder="Search by name or keyword (optional)">' +
|
|
1252
|
+
'<button type="submit">Search</button>' +
|
|
1253
|
+
'</form>' +
|
|
1254
|
+
'<div id="abilitySearchResults"></div>' +
|
|
1255
|
+
'<h3 style="margin-top:24px">Install from a spec</h3>' +
|
|
1256
|
+
'<p class="hint">A git URL, a local <code>file:../path</code>, or a private npm package name/version — for anything not published publicly.</p>' +
|
|
1257
|
+
'<form id="abilityInstallForm" class="add-source">' +
|
|
1258
|
+
'<input type="text" name="spec" placeholder="e.g. git+https://github.com/org/repo.git or file:../my-ability" required>' +
|
|
1259
|
+
'<button type="submit">Install</button>' +
|
|
1260
|
+
'</form>' +
|
|
1261
|
+
'<p id="abilityInstallStatus" class="hint"></p>';
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function abilitiesContentEl() {
|
|
1265
|
+
return detail.querySelector('#abilitiesContent');
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Shared by both the search-results "Install" button and the
|
|
1269
|
+
// spec-form submit — same endpoint either way, installAbility itself
|
|
1270
|
+
// doesn't distinguish "found via search" from "pasted directly".
|
|
1271
|
+
function installAbilitySpec(name, spec, statusEl, btn) {
|
|
1272
|
+
if (btn) { btn.disabled = true; }
|
|
1273
|
+
statusEl.className = 'hint';
|
|
1274
|
+
statusEl.textContent = 'Installing ' + spec + '…';
|
|
1275
|
+
fetch('/agents/' + encodeURIComponent(name) + '/abilities', {
|
|
1276
|
+
method: 'POST',
|
|
1277
|
+
headers: { 'content-type': 'application/json' },
|
|
1278
|
+
body: JSON.stringify({ spec: spec }),
|
|
1279
|
+
})
|
|
1280
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
1281
|
+
.then(function (result) {
|
|
1282
|
+
if (!result.ok) throw new Error(result.body.error || 'request failed');
|
|
1283
|
+
// Same "is this dev or serve" distinction the CLI's own
|
|
1284
|
+
// add-ability command prints after installing — a web install
|
|
1285
|
+
// doesn't splice into an already-running server's in-memory
|
|
1286
|
+
// registry any more than the CLI does (see installAbility's own
|
|
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.';
|
|
1289
|
+
loadAbilitiesTab(name);
|
|
1290
|
+
})
|
|
1291
|
+
.catch(function (err) {
|
|
1292
|
+
statusEl.className = 'error';
|
|
1293
|
+
statusEl.textContent = 'Could not install: ' + err.message;
|
|
1294
|
+
if (btn) { btn.disabled = false; }
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Shared by the initial (empty-query) load right after the tab opens
|
|
1299
|
+
// and the search form's own submit — the empty-query case is just
|
|
1300
|
+
// "list every published ability" (the keyword filter alone, no extra
|
|
1301
|
+
// search text), so an operator sees what's out there immediately
|
|
1302
|
+
// instead of having to search for something before anything shows up.
|
|
1303
|
+
function runAbilitySearch(name, q, content, resultsEl) {
|
|
1304
|
+
resultsEl.innerHTML = '<p class="hint">Loading…</p>';
|
|
1305
|
+
fetch('/agents/' + encodeURIComponent(name) + '/abilities/search' + (q ? '?q=' + encodeURIComponent(q) : ''))
|
|
1306
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
1307
|
+
.then(function (result) {
|
|
1308
|
+
if (!result.ok) throw new Error(result.body.error || 'search failed');
|
|
1309
|
+
var results = result.body.results || [];
|
|
1310
|
+
resultsEl.innerHTML = results.length
|
|
1311
|
+
? '<table><thead><tr><th>Package</th><th>Description</th><th></th></tr></thead><tbody>' + results.map(renderAbilitySearchResultRow).join('') + '</tbody></table>'
|
|
1312
|
+
: '<p class="hint">No public abilities found.</p>';
|
|
1313
|
+
var installBtns = resultsEl.querySelectorAll('.ability-install-btn');
|
|
1314
|
+
for (var i = 0; i < installBtns.length; i++) {
|
|
1315
|
+
installBtns[i].addEventListener('click', function (ev2) {
|
|
1316
|
+
var btn = ev2.currentTarget;
|
|
1317
|
+
installAbilitySpec(name, btn.dataset.spec, content.querySelector('#abilityInstallStatus'), btn);
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
})
|
|
1321
|
+
.catch(function (err) {
|
|
1322
|
+
resultsEl.innerHTML = '<p class="error">' + escapeHtml(err.message) + '</p>';
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function wireAbilitiesHandlers(name) {
|
|
1327
|
+
var content = abilitiesContentEl();
|
|
1328
|
+
if (!content) return;
|
|
1329
|
+
|
|
1330
|
+
var searchForm = content.querySelector('#abilitySearchForm');
|
|
1331
|
+
var resultsEl = content.querySelector('#abilitySearchResults');
|
|
1332
|
+
if (searchForm && resultsEl) {
|
|
1333
|
+
searchForm.addEventListener('submit', function (ev) {
|
|
1334
|
+
ev.preventDefault();
|
|
1335
|
+
var q = new FormData(searchForm).get('q');
|
|
1336
|
+
runAbilitySearch(name, q, content, resultsEl);
|
|
1337
|
+
});
|
|
1338
|
+
// List every published ability immediately, not just after the
|
|
1339
|
+
// operator explicitly searches for something.
|
|
1340
|
+
runAbilitySearch(name, '', content, resultsEl);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
var installForm = content.querySelector('#abilityInstallForm');
|
|
1344
|
+
if (installForm) {
|
|
1345
|
+
installForm.addEventListener('submit', function (ev) {
|
|
1346
|
+
ev.preventDefault();
|
|
1347
|
+
var spec = new FormData(installForm).get('spec');
|
|
1348
|
+
var statusEl = content.querySelector('#abilityInstallStatus');
|
|
1349
|
+
var btn = installForm.querySelector('button[type="submit"]');
|
|
1350
|
+
installAbilitySpec(name, spec, statusEl, btn);
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function applyAbilitiesConfig(name, data) {
|
|
1356
|
+
var content = abilitiesContentEl();
|
|
1357
|
+
if (!content) return;
|
|
1358
|
+
content.innerHTML = renderAbilitiesConfigHtml(data);
|
|
1359
|
+
wireAbilitiesHandlers(name);
|
|
1360
|
+
abilitiesLoadedFor = name;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function loadAbilitiesTab(name) {
|
|
1364
|
+
var content = abilitiesContentEl();
|
|
1365
|
+
if (!content) return;
|
|
1366
|
+
content.innerHTML = '<p class="hint">Loading…</p>';
|
|
1367
|
+
fetch('/agents/' + encodeURIComponent(name) + '/abilities')
|
|
1368
|
+
.then(function (r) { return r.json(); })
|
|
1369
|
+
.then(function (data) {
|
|
1370
|
+
if (currentName !== name) return;
|
|
1371
|
+
applyAbilitiesConfig(name, data);
|
|
1372
|
+
})
|
|
1373
|
+
.catch(function (err) {
|
|
1374
|
+
if (currentName !== name) return;
|
|
1375
|
+
content.innerHTML = '<p class="error">Could not load abilities: ' + escapeHtml(err.message) + '</p>';
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1211
1379
|
// ---- Skills tab: editable — add/edit/delete SKILL.md files via
|
|
1212
1380
|
// skills-admin.ts, restricted (see that file's own doc comment) to
|
|
1213
1381
|
// flat, non-nested skill ids. Unlike the Tools tab's Gateway Tools
|
|
@@ -1995,6 +2163,9 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
1995
2163
|
if (tab === 'env' && envLoadedFor !== currentName) {
|
|
1996
2164
|
loadEnvTab(currentName);
|
|
1997
2165
|
}
|
|
2166
|
+
if (tab === 'abilities' && abilitiesLoadedFor !== currentName) {
|
|
2167
|
+
loadAbilitiesTab(currentName);
|
|
2168
|
+
}
|
|
1998
2169
|
}
|
|
1999
2170
|
|
|
2000
2171
|
// Refreshes just Overview's own summary — used after anything that
|
|
@@ -2070,13 +2241,15 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
2070
2241
|
'<button class="tab" data-tab="skills">Skills</button>' +
|
|
2071
2242
|
'<button class="tab" data-tab="tools">Tools</button>' +
|
|
2072
2243
|
'<button class="tab" data-tab="actauth">ActAuth</button>' +
|
|
2244
|
+
'<button class="tab" data-tab="abilities">Abilities</button>' +
|
|
2073
2245
|
'<button class="tab" data-tab="env">Environment</button>' +
|
|
2074
2246
|
'</div>' +
|
|
2075
2247
|
'<div class="tab-panel" data-tab-panel="overview">' + renderOverviewHtml(cfg) + '</div>' +
|
|
2076
2248
|
'<div class="tab-panel" data-tab-panel="skills">' + renderSkillsTabHtml(cfg) + '</div>' +
|
|
2077
2249
|
'<div class="tab-panel" data-tab-panel="tools">' + renderToolsTabHtml(cfg) + '</div>' +
|
|
2078
2250
|
'<div class="tab-panel" data-tab-panel="actauth">' + renderActauthTabPlaceholder() + '</div>' +
|
|
2079
|
-
'<div class="tab-panel" data-tab-panel="env">' + renderEnvTabPlaceholder() + '</div>'
|
|
2251
|
+
'<div class="tab-panel" data-tab-panel="env">' + renderEnvTabPlaceholder() + '</div>' +
|
|
2252
|
+
'<div class="tab-panel" data-tab-panel="abilities">' + renderAbilitiesTabPlaceholder() + '</div>';
|
|
2080
2253
|
|
|
2081
2254
|
var buttons = detail.querySelectorAll('.tabs button');
|
|
2082
2255
|
for (var i = 0; i < buttons.length; i++) {
|
|
@@ -2101,6 +2274,7 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
2101
2274
|
gatewayLoadedFor = null;
|
|
2102
2275
|
actauthLoadedFor = null;
|
|
2103
2276
|
envLoadedFor = null;
|
|
2277
|
+
abilitiesLoadedFor = null;
|
|
2104
2278
|
var items = agentList.querySelectorAll('li');
|
|
2105
2279
|
for (var i = 0; i < items.length; i++) {
|
|
2106
2280
|
items[i].classList.toggle('active', items[i].dataset.name === name);
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopengine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://loopengine.co",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/loopengine-co/loopengine.git"
|
|
11
11
|
},
|
|
12
12
|
"main": "dist/index.js",
|
|
13
13
|
"types": "dist/index.d.ts",
|