loopengine 0.1.15 → 0.1.24
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 +124 -299
- package/dist/bin/{package-manager.d.ts → ability-manager.d.ts} +39 -33
- package/dist/bin/{package-manager.js → ability-manager.js} +85 -74
- package/dist/bin/cli.js +22 -22
- package/dist/core/agent-config.d.ts +1 -1
- package/dist/core/gateway-tools.js +4 -4
- package/dist/core/mcpplug.d.ts +23 -0
- package/dist/core/mcpplug.js +64 -0
- package/dist/core/run-agent.js +2 -2
- package/dist/core/session-store.d.ts +1 -1
- package/dist/core/session-store.js +1 -1
- package/dist/core/sessionknit.d.ts +87 -0
- package/dist/core/sessionknit.js +189 -0
- package/dist/core/skillgarden/activation.d.ts +4 -0
- package/dist/core/skillgarden/activation.js +16 -0
- package/dist/core/skillgarden/budget.d.ts +20 -0
- package/dist/core/skillgarden/budget.js +28 -0
- package/dist/core/skillgarden/discovery.d.ts +11 -0
- package/dist/core/skillgarden/discovery.js +40 -0
- package/dist/core/skillgarden/frontmatter.d.ts +10 -0
- package/dist/core/skillgarden/frontmatter.js +35 -0
- package/dist/core/skillgarden/index.d.ts +8 -0
- package/dist/core/skillgarden/index.js +12 -0
- package/dist/core/skillgarden/loader.d.ts +31 -0
- package/dist/core/skillgarden/loader.js +70 -0
- package/dist/core/skillgarden/substitute.d.ts +4 -0
- package/dist/core/skillgarden/substitute.js +13 -0
- package/dist/core/skillgarden/types.d.ts +23 -0
- package/dist/core/skillgarden/types.js +1 -0
- package/dist/core/toollane.d.ts +46 -0
- package/dist/core/toollane.js +69 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +4 -1
- package/dist/web/agents-config-page.js +7 -203
- package/dist/web/env-admin.d.ts +6 -6
- package/dist/web/env-admin.js +11 -11
- package/dist/web/http-tool-admin.d.ts +2 -2
- package/dist/web/http-tool-admin.js +4 -4
- package/dist/web/skills-admin.js +1 -1
- package/package.json +10 -7
- package/dist/web/skillgarden-admin.d.ts +0 -24
- package/dist/web/skillgarden-admin.js +0 -76
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** A tool call is just a callable with identity — this module doesn't
|
|
2
|
+
* know or care what a "tool" is beyond that. */
|
|
3
|
+
export interface ToolCall {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
execute: () => Promise<unknown>;
|
|
7
|
+
}
|
|
8
|
+
/** Host-provided: does this specific call belong in a parallel lane?
|
|
9
|
+
* Mirrors Claude Code's own isReadOnly()/isConcurrencySafe() per-tool
|
|
10
|
+
* declarations. */
|
|
11
|
+
export type SafetyClassifier = (call: ToolCall) => boolean;
|
|
12
|
+
export interface Lane {
|
|
13
|
+
mode: 'parallel' | 'solo';
|
|
14
|
+
calls: ToolCall[];
|
|
15
|
+
}
|
|
16
|
+
export interface LaneResult {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
status: 'fulfilled' | 'rejected';
|
|
20
|
+
value?: unknown;
|
|
21
|
+
error?: unknown;
|
|
22
|
+
}
|
|
23
|
+
/** Groups a batch of tool calls into lanes: consecutive safety-declared
|
|
24
|
+
* calls merge into one parallel lane; anything else gets its own solo
|
|
25
|
+
* lane. Batch order is preserved across lanes — this never reorders
|
|
26
|
+
* calls, only decides what runs together. */
|
|
27
|
+
export declare function buildLanes(calls: ToolCall[], isSafe: SafetyClassifier): Lane[];
|
|
28
|
+
/** Runs lanes in order; calls within a parallel lane run concurrently.
|
|
29
|
+
* Every call resolves to a LaneResult regardless of success or failure —
|
|
30
|
+
* one call throwing never rejects the batch, it just carries the error
|
|
31
|
+
* in its own slot. */
|
|
32
|
+
export declare function runLanes(lanes: Lane[]): AsyncGenerator<LaneResult>;
|
|
33
|
+
export interface ToolLaneOptions {
|
|
34
|
+
isSafe: SafetyClassifier;
|
|
35
|
+
}
|
|
36
|
+
/** Ties classification, scheduling, and execution together: given a
|
|
37
|
+
* batch of approved tool calls, decide a safe execution plan and run
|
|
38
|
+
* it, streaming each call's result as it completes. */
|
|
39
|
+
export declare class ToolLane {
|
|
40
|
+
private readonly isSafe;
|
|
41
|
+
constructor(options: ToolLaneOptions);
|
|
42
|
+
/** Read-only — returns the lane plan without executing anything. */
|
|
43
|
+
plan(calls: ToolCall[]): Lane[];
|
|
44
|
+
/** Builds the plan and executes it. */
|
|
45
|
+
run(calls: ToolCall[]): AsyncGenerator<LaneResult>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Formerly the standalone `toollane` package — folded in here for the
|
|
2
|
+
// same reason mcpplug.ts was: no consumer besides loopengine itself ever
|
|
3
|
+
// actually materialized, so the separate-package/portable-to-any-host
|
|
4
|
+
// framing was aspirational rather than realized. A concurrency-safety-
|
|
5
|
+
// aware scheduler for a batch of already-approved tool calls: consecutive
|
|
6
|
+
// safety-declared calls run together in a parallel lane, anything else
|
|
7
|
+
// gets its own solo lane, order preserved, one failure isolated to its
|
|
8
|
+
// own slot rather than rejecting the whole batch.
|
|
9
|
+
/** Groups a batch of tool calls into lanes: consecutive safety-declared
|
|
10
|
+
* calls merge into one parallel lane; anything else gets its own solo
|
|
11
|
+
* lane. Batch order is preserved across lanes — this never reorders
|
|
12
|
+
* calls, only decides what runs together. */
|
|
13
|
+
export function buildLanes(calls, isSafe) {
|
|
14
|
+
const lanes = [];
|
|
15
|
+
for (const call of calls) {
|
|
16
|
+
const safe = isSafe(call);
|
|
17
|
+
const lastLane = lanes.at(-1);
|
|
18
|
+
if (safe && lastLane?.mode === 'parallel') {
|
|
19
|
+
lastLane.calls.push(call);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
lanes.push({ mode: safe ? 'parallel' : 'solo', calls: [call] });
|
|
23
|
+
}
|
|
24
|
+
return lanes;
|
|
25
|
+
}
|
|
26
|
+
async function runCall(call) {
|
|
27
|
+
try {
|
|
28
|
+
const value = await call.execute();
|
|
29
|
+
return { id: call.id, name: call.name, status: 'fulfilled', value };
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return { id: call.id, name: call.name, status: 'rejected', error };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Runs lanes in order; calls within a parallel lane run concurrently.
|
|
36
|
+
* Every call resolves to a LaneResult regardless of success or failure —
|
|
37
|
+
* one call throwing never rejects the batch, it just carries the error
|
|
38
|
+
* in its own slot. */
|
|
39
|
+
export async function* runLanes(lanes) {
|
|
40
|
+
for (const lane of lanes) {
|
|
41
|
+
if (lane.mode === 'solo') {
|
|
42
|
+
const call = lane.calls[0];
|
|
43
|
+
if (call)
|
|
44
|
+
yield await runCall(call);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const results = await Promise.all(lane.calls.map(runCall));
|
|
48
|
+
for (const result of results) {
|
|
49
|
+
yield result;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Ties classification, scheduling, and execution together: given a
|
|
54
|
+
* batch of approved tool calls, decide a safe execution plan and run
|
|
55
|
+
* it, streaming each call's result as it completes. */
|
|
56
|
+
export class ToolLane {
|
|
57
|
+
isSafe;
|
|
58
|
+
constructor(options) {
|
|
59
|
+
this.isSafe = options.isSafe;
|
|
60
|
+
}
|
|
61
|
+
/** Read-only — returns the lane plan without executing anything. */
|
|
62
|
+
plan(calls) {
|
|
63
|
+
return buildLanes(calls, this.isSafe);
|
|
64
|
+
}
|
|
65
|
+
/** Builds the plan and executes it. */
|
|
66
|
+
async *run(calls) {
|
|
67
|
+
yield* runLanes(this.plan(calls));
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export { streamMessage, streamMessageWithCallbacks, sendMessage, approveCall, de
|
|
|
6
6
|
export type { RequestOptions, SendMessageResult, PendingApprovalResult, PendingQuestionResult, PendingResult, MessageResult, StreamMessageCallbacks, } from './core/client.js';
|
|
7
7
|
export { FileSessionStore, RedisSessionStore, createSessionStore } from './core/session-store.js';
|
|
8
8
|
export type { SessionStore, SessionResult, RedisSessionStoreOptions } from './core/session-store.js';
|
|
9
|
+
export { SessionKnit, FileStorage, MemoryStorage, reconstructChain } from './core/sessionknit.js';
|
|
10
|
+
export type { Storage, SessionEntry, ResumeResult, SessionKnitOptions, ReconstructResult } from './core/sessionknit.js';
|
|
11
|
+
export { SkillGarden, parseSkillFile, discoverSkillFiles, estimateTokens as estimateSkillIndexTokens, buildBudgetedIndex, substituteArguments, matchesActivationPaths, } from './core/skillgarden/index.js';
|
|
12
|
+
export type { SkillGardenOptions, SkillFrontmatter, SkillIndexEntry, LoadedSkill, DiscoveredSkillFile, BuildIndexOptions, BudgetedIndex, } from './core/skillgarden/index.js';
|
|
9
13
|
export { VectorIndex, embed, cosineSimilarity } from './core/vector-index.js';
|
|
10
14
|
export type { Document, ScoredDocument } from './core/vector-index.js';
|
|
11
15
|
export { createAnthropicModelCall } from './core/model-calls/anthropic-model-call.js';
|
|
@@ -32,6 +36,10 @@ export { createCheckpointStore } from './core/durable-approvals.js';
|
|
|
32
36
|
export type { TurnCheckpoint, CheckpointStore, OutstandingItem } from './core/durable-approvals.js';
|
|
33
37
|
export { addGatewayTool, agentDir, disconnectComposioAccount, describeGatewayTools, listComposioConnections, listComposioTools, loadGatewayToolsFromDir, removeGatewayTool, removeGatewayToolSlug, GatewayToolExistsError, GatewayToolNotFoundError, } from './core/gateway-tools.js';
|
|
34
38
|
export type { GatewayToolEntry, GatewayToolDecision } from './core/gateway-tools.js';
|
|
39
|
+
export { connectComposioSource } from './core/mcpplug.js';
|
|
40
|
+
export type { ToolSource, ComposioSourceOptions } from './core/mcpplug.js';
|
|
41
|
+
export { ToolLane, buildLanes, runLanes } from './core/toollane.js';
|
|
42
|
+
export type { ToolLaneOptions, SafetyClassifier, ToolCall, Lane, LaneResult } from './core/toollane.js';
|
|
35
43
|
export { WebhookNotifier } from './core/http-notify-triggers/webhook.js';
|
|
36
44
|
export { listQuestions, answerQuestion as resolvePendingQuestion, findQuestion, createAskUserTool } from './core/system-tools/index.js';
|
|
37
45
|
export type { PendingQuestion } from './core/system-tools/index.js';
|
|
@@ -45,8 +53,6 @@ export { globalConfigPageHtml } from './web/global-config-page.js';
|
|
|
45
53
|
export { playgroundHtml } from './web/playground.js';
|
|
46
54
|
export { readSkill, writeSkill, deleteSkill, SkillInvalidIdError, SkillNotFoundError } from './web/skills-admin.js';
|
|
47
55
|
export type { SkillContent } from './web/skills-admin.js';
|
|
48
|
-
export { listSkillgardenCatalog, readSkillgardenCatalogEntry, addSkillgardenSkillToAgent, SkillgardenUnavailableError } from './web/skillgarden-admin.js';
|
|
49
|
-
export type { SkillgardenCatalogEntry, SkillgardenCatalogDetail } from './web/skillgarden-admin.js';
|
|
50
56
|
export { listDeclaredEnvVars, setEnvVar, EnvVarNameError } from './web/env-admin.js';
|
|
51
57
|
export type { DeclaredEnvVar } from './web/env-admin.js';
|
|
52
58
|
export { readActauthConfig, addActauthRule, updateActauthRule, removeActauthRule, setDefaultDecision, ActauthRuleExistsError, ActauthRuleNotFoundError, } from './web/actauth-admin.js';
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,8 @@ export { runAgent } from '#core/run-agent.js';
|
|
|
12
12
|
// exception.
|
|
13
13
|
export { streamMessage, streamMessageWithCallbacks, sendMessage, approveCall, denyCall, answerQuestion, getSessionHistory, } from './core/client.js';
|
|
14
14
|
export { FileSessionStore, RedisSessionStore, createSessionStore } from './core/session-store.js';
|
|
15
|
+
export { SessionKnit, FileStorage, MemoryStorage, reconstructChain } from './core/sessionknit.js';
|
|
16
|
+
export { SkillGarden, parseSkillFile, discoverSkillFiles, estimateTokens as estimateSkillIndexTokens, buildBudgetedIndex, substituteArguments, matchesActivationPaths, } from './core/skillgarden/index.js';
|
|
15
17
|
export { VectorIndex, embed, cosineSimilarity } from './core/vector-index.js';
|
|
16
18
|
export { createAnthropicModelCall } from './core/model-calls/anthropic-model-call.js';
|
|
17
19
|
export { createOpenAIModelCall } from './core/model-calls/openai-model-call.js';
|
|
@@ -33,6 +35,8 @@ export { Compactor, TruncatingSummarizer } from './core/compaction.js';
|
|
|
33
35
|
export { LLMSummarizer } from './core/llm-summarizer.js';
|
|
34
36
|
export { createCheckpointStore } from './core/durable-approvals.js';
|
|
35
37
|
export { addGatewayTool, agentDir, disconnectComposioAccount, describeGatewayTools, listComposioConnections, listComposioTools, loadGatewayToolsFromDir, removeGatewayTool, removeGatewayToolSlug, GatewayToolExistsError, GatewayToolNotFoundError, } from './core/gateway-tools.js';
|
|
38
|
+
export { connectComposioSource } from './core/mcpplug.js';
|
|
39
|
+
export { ToolLane, buildLanes, runLanes } from './core/toollane.js';
|
|
36
40
|
export { WebhookNotifier } from './core/http-notify-triggers/webhook.js';
|
|
37
41
|
// answerQuestion above (core/client.ts) is the HTTP client-side helper —
|
|
38
42
|
// this is the server-side function that actually resolves a pending
|
|
@@ -45,7 +49,6 @@ export { agentsListPageHtml } from './web/agents-list-page.js';
|
|
|
45
49
|
export { globalConfigPageHtml } from './web/global-config-page.js';
|
|
46
50
|
export { playgroundHtml } from './web/playground.js';
|
|
47
51
|
export { readSkill, writeSkill, deleteSkill, SkillInvalidIdError, SkillNotFoundError } from './web/skills-admin.js';
|
|
48
|
-
export { listSkillgardenCatalog, readSkillgardenCatalogEntry, addSkillgardenSkillToAgent, SkillgardenUnavailableError } from './web/skillgarden-admin.js';
|
|
49
52
|
export { listDeclaredEnvVars, setEnvVar, EnvVarNameError } from './web/env-admin.js';
|
|
50
53
|
export { readActauthConfig, addActauthRule, updateActauthRule, removeActauthRule, setDefaultDecision, ActauthRuleExistsError, ActauthRuleNotFoundError, } from './web/actauth-admin.js';
|
|
51
54
|
export { describeModelProviders, describeGateways } from './web/global-config.js';
|
|
@@ -367,26 +367,6 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
367
367
|
#httpToolAddFieldBtn, #httpToolAddHeaderBtn {
|
|
368
368
|
padding: 6px 12px;
|
|
369
369
|
}
|
|
370
|
-
.skillgarden-category-badge {
|
|
371
|
-
display: inline-block;
|
|
372
|
-
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
373
|
-
font-size: 10px;
|
|
374
|
-
font-weight: 600;
|
|
375
|
-
text-transform: uppercase;
|
|
376
|
-
letter-spacing: 0.03em;
|
|
377
|
-
padding: 1px 7px;
|
|
378
|
-
border-radius: 999px;
|
|
379
|
-
background: light-dark(#eef1ec, #1e2519);
|
|
380
|
-
color: light-dark(#3f6b4a, #7fbf8f);
|
|
381
|
-
margin-right: 6px;
|
|
382
|
-
}
|
|
383
|
-
#skillgardenFilterInput { margin-bottom: 8px; }
|
|
384
|
-
.skillgarden-catalog-body {
|
|
385
|
-
margin-top: 8px;
|
|
386
|
-
padding-top: 8px;
|
|
387
|
-
border-top: 1px solid light-dark(#eee, #2a2a2e);
|
|
388
|
-
font-size: 13px;
|
|
389
|
-
}
|
|
390
370
|
</style>
|
|
391
371
|
</head>
|
|
392
372
|
<body>
|
|
@@ -1130,9 +1110,9 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
1130
1110
|
});
|
|
1131
1111
|
}
|
|
1132
1112
|
|
|
1133
|
-
// ---- Environment tab: every env var
|
|
1134
|
-
// bin/
|
|
1135
|
-
// every
|
|
1113
|
+
// ---- Environment tab: every env var an ability (see ABILITIES.md,
|
|
1114
|
+
// bin/ability-manager.ts) declared it needs for this agent, across
|
|
1115
|
+
// every ability installed — status only, a value is never shown once
|
|
1136
1116
|
// set, matching env-admin.ts's own never-echo-a-secret rule. Lazily
|
|
1137
1117
|
// loaded the same way Actauth is, for the same reason (avoid a
|
|
1138
1118
|
// redundant fetch on every agent switch for a tab that isn't always
|
|
@@ -1146,7 +1126,7 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
1146
1126
|
return '<tr>' +
|
|
1147
1127
|
'<td><code>' + escapeHtml(v.name) + '</code></td>' +
|
|
1148
1128
|
'<td>' + escapeHtml(v.description || '') + '</td>' +
|
|
1149
|
-
'<td class="hint">' + escapeHtml(v.
|
|
1129
|
+
'<td class="hint">' + escapeHtml(v.abilityName) + '</td>' +
|
|
1150
1130
|
'<td>' + (v.set ? '<span class="hint">set</span>' : '<span class="error">not set</span>') + '</td>' +
|
|
1151
1131
|
'<td><form class="add-source env-var-form" data-name="' + escapeHtml(v.name) + '">' +
|
|
1152
1132
|
'<input type="' + (v.secret ? 'password' : 'text') + '" name="value" placeholder="' + (v.set ? 'unchanged unless you type a new value' : 'value') + '" required>' +
|
|
@@ -1156,9 +1136,9 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
1156
1136
|
}
|
|
1157
1137
|
|
|
1158
1138
|
function renderEnvConfigHtml(vars) {
|
|
1159
|
-
if (!vars.length) return '<p class="hint">No installed
|
|
1139
|
+
if (!vars.length) return '<p class="hint">No installed ability has declared any environment variables for this agent yet.</p>';
|
|
1160
1140
|
var rows = vars.map(renderEnvRow).join('');
|
|
1161
|
-
return '<table><thead><tr><th>Name</th><th>Description</th><th>
|
|
1141
|
+
return '<table><thead><tr><th>Name</th><th>Description</th><th>Ability</th><th>Status</th><th>Set value</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
|
1162
1142
|
}
|
|
1163
1143
|
|
|
1164
1144
|
function envContentEl() {
|
|
@@ -1270,194 +1250,18 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
1270
1250
|
'<button type="submit" id="skillSubmitBtn">Add</button>' +
|
|
1271
1251
|
'<button type="button" id="skillCancelBtn" style="display:none">Cancel</button>' +
|
|
1272
1252
|
'<div id="skillFormError" class="error"></div>' +
|
|
1273
|
-
'</form></section>'
|
|
1274
|
-
'<section><h3>Browse skillgarden catalog</h3>' +
|
|
1275
|
-
'<input type="text" id="skillgardenFilterInput" placeholder="Filter by name or category…">' +
|
|
1276
|
-
'<div id="skillgardenCatalogList"><p class="hint">Loading…</p></div>' +
|
|
1277
|
-
'</section>';
|
|
1253
|
+
'</form></section>';
|
|
1278
1254
|
}
|
|
1279
1255
|
|
|
1280
1256
|
function skillsPanelEl() {
|
|
1281
1257
|
return detail.querySelector('[data-tab-panel="skills"]');
|
|
1282
1258
|
}
|
|
1283
1259
|
|
|
1284
|
-
// ---- Skills tab's "Browse skillgarden catalog" section: a
|
|
1285
|
-
// read-only, agent-independent list of everything GET /skillgarden-
|
|
1286
|
-
// catalog reports (see web/skillgarden-admin.ts) — cached in this one
|
|
1287
|
-
// module-level variable, not per-agent like gatewayLoadedFor, since
|
|
1288
|
-
// the catalog itself never varies by agent. renderSkillsTabHtml
|
|
1289
|
-
// regenerates the whole Skills panel (including this section's own
|
|
1290
|
-
// markup) on every agent switch/refresh, so loadSkillgardenCatalogSection
|
|
1291
|
-
// re-renders from this cache instead of re-fetching every time — only
|
|
1292
|
-
// the very first time this tab is opened in the page's lifetime hits
|
|
1293
|
-
// the network at all. ----
|
|
1294
|
-
var skillgardenCatalog = null;
|
|
1295
|
-
|
|
1296
|
-
function skillgardenCatalogListEl() {
|
|
1297
|
-
var panel = skillsPanelEl();
|
|
1298
|
-
return panel ? panel.querySelector('#skillgardenCatalogList') : null;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
function renderSkillgardenCatalogEntry(entry) {
|
|
1302
|
-
return '<div class="source" data-category="' + escapeHtml(entry.category) + '" data-id="' + escapeHtml(entry.id) + '">' +
|
|
1303
|
-
'<div class="source-head">' +
|
|
1304
|
-
'<h4><span class="skillgarden-category-badge">' + escapeHtml(entry.category) + '</span>' + escapeHtml(entry.id) + '</h4>' +
|
|
1305
|
-
'<span>' +
|
|
1306
|
-
'<button type="button" class="skillgarden-view-btn" data-category="' + escapeHtml(entry.category) + '" data-id="' + escapeHtml(entry.id) + '">View</button> ' +
|
|
1307
|
-
'<button type="button" class="skillgarden-add-btn" data-category="' + escapeHtml(entry.category) + '" data-id="' + escapeHtml(entry.id) + '">Add to this agent</button>' +
|
|
1308
|
-
'</span>' +
|
|
1309
|
-
'</div>' +
|
|
1310
|
-
'<p class="hint">' + escapeHtml(entry.description) + '</p>' +
|
|
1311
|
-
'<div class="skillgarden-catalog-body" style="display:none"></div>' +
|
|
1312
|
-
'</div>';
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
function renderSkillgardenCatalogList(entries) {
|
|
1316
|
-
return entries.length ? entries.map(renderSkillgardenCatalogEntry).join('') : '<p class="hint">No skills match.</p>';
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
function applySkillgardenCatalog(name, entries) {
|
|
1320
|
-
var listEl = skillgardenCatalogListEl();
|
|
1321
|
-
if (!listEl) return;
|
|
1322
|
-
listEl.innerHTML = renderSkillgardenCatalogList(entries);
|
|
1323
|
-
wireSkillgardenCatalogHandlers(name);
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
function filteredSkillgardenCatalog(filterText) {
|
|
1327
|
-
if (!filterText) return skillgardenCatalog;
|
|
1328
|
-
var needle = filterText.toLowerCase();
|
|
1329
|
-
return skillgardenCatalog.filter(function (entry) {
|
|
1330
|
-
return entry.category.toLowerCase().indexOf(needle) !== -1 || entry.id.toLowerCase().indexOf(needle) !== -1;
|
|
1331
|
-
});
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
function loadSkillgardenCatalogSection(name) {
|
|
1335
|
-
var listEl = skillgardenCatalogListEl();
|
|
1336
|
-
if (!listEl) return;
|
|
1337
|
-
if (skillgardenCatalog) {
|
|
1338
|
-
var panel = skillsPanelEl();
|
|
1339
|
-
var filterInput = panel ? panel.querySelector('#skillgardenFilterInput') : null;
|
|
1340
|
-
applySkillgardenCatalog(name, filteredSkillgardenCatalog(filterInput ? filterInput.value.trim() : ''));
|
|
1341
|
-
return;
|
|
1342
|
-
}
|
|
1343
|
-
listEl.innerHTML = '<p class="hint">Loading…</p>';
|
|
1344
|
-
fetch('/skillgarden-catalog')
|
|
1345
|
-
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
1346
|
-
.then(function (result) {
|
|
1347
|
-
if (!result.ok) throw new Error(result.body.error || 'request failed');
|
|
1348
|
-
skillgardenCatalog = result.body;
|
|
1349
|
-
if (currentName !== name) return;
|
|
1350
|
-
applySkillgardenCatalog(name, skillgardenCatalog);
|
|
1351
|
-
})
|
|
1352
|
-
.catch(function (err) {
|
|
1353
|
-
if (currentName !== name) return;
|
|
1354
|
-
listEl.innerHTML = '<p class="error">Could not load skillgarden catalog: ' + escapeHtml(err.message) + '</p>';
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
// force=true retries as an explicit overwrite after the operator
|
|
1359
|
-
// confirms — same "ask once a plain add collides, don't ask up front"
|
|
1360
|
-
// flow the rest of this page never has to think about since nothing
|
|
1361
|
-
// else lets an operator add something whose id they don't already
|
|
1362
|
-
// control the uniqueness of themselves.
|
|
1363
|
-
function performSkillgardenAdd(name, category, id, btn, originalText, force) {
|
|
1364
|
-
fetch('/agents/' + encodeURIComponent(name) + '/skillgarden-catalog/' + encodeURIComponent(category) + '/' + encodeURIComponent(id), {
|
|
1365
|
-
method: 'POST',
|
|
1366
|
-
headers: { 'content-type': 'application/json' },
|
|
1367
|
-
body: JSON.stringify({ force: !!force }),
|
|
1368
|
-
})
|
|
1369
|
-
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
1370
|
-
.then(function (result) {
|
|
1371
|
-
if (result.ok) {
|
|
1372
|
-
refreshSkillsDependentPanels(name);
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
if (!force && /already exists/.test(result.body.error || '')) {
|
|
1376
|
-
if (confirm('A skill named "' + id + '" already exists for this agent. Overwrite it?')) {
|
|
1377
|
-
performSkillgardenAdd(name, category, id, btn, originalText, true);
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
btn.disabled = false;
|
|
1381
|
-
btn.textContent = originalText;
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
throw new Error(result.body.error || 'request failed');
|
|
1385
|
-
})
|
|
1386
|
-
.catch(function (err) {
|
|
1387
|
-
alert('Could not add "' + id + '": ' + err.message);
|
|
1388
|
-
btn.disabled = false;
|
|
1389
|
-
btn.textContent = originalText;
|
|
1390
|
-
});
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
function wireSkillgardenCatalogHandlers(name) {
|
|
1394
|
-
var listEl = skillgardenCatalogListEl();
|
|
1395
|
-
if (!listEl) return;
|
|
1396
|
-
|
|
1397
|
-
var viewButtons = listEl.querySelectorAll('.skillgarden-view-btn');
|
|
1398
|
-
for (var i = 0; i < viewButtons.length; i++) {
|
|
1399
|
-
viewButtons[i].addEventListener('click', function (ev) {
|
|
1400
|
-
var btn = ev.currentTarget;
|
|
1401
|
-
var category = btn.getAttribute('data-category');
|
|
1402
|
-
var id = btn.getAttribute('data-id');
|
|
1403
|
-
var card = btn.closest('.source');
|
|
1404
|
-
var bodyEl = card.querySelector('.skillgarden-catalog-body');
|
|
1405
|
-
if (bodyEl.style.display !== 'none') {
|
|
1406
|
-
bodyEl.style.display = 'none';
|
|
1407
|
-
btn.textContent = 'View';
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
|
-
if (bodyEl.getAttribute('data-loaded') === '1') {
|
|
1411
|
-
bodyEl.style.display = 'block';
|
|
1412
|
-
btn.textContent = 'Hide';
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
btn.disabled = true;
|
|
1416
|
-
fetch('/skillgarden-catalog/' + encodeURIComponent(category) + '/' + encodeURIComponent(id))
|
|
1417
|
-
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
1418
|
-
.then(function (result) {
|
|
1419
|
-
if (!result.ok) throw new Error(result.body.error || 'request failed');
|
|
1420
|
-
bodyEl.innerHTML = renderMarkdownPreview(result.body.body);
|
|
1421
|
-
bodyEl.setAttribute('data-loaded', '1');
|
|
1422
|
-
bodyEl.style.display = 'block';
|
|
1423
|
-
btn.textContent = 'Hide';
|
|
1424
|
-
})
|
|
1425
|
-
.catch(function (err) {
|
|
1426
|
-
bodyEl.innerHTML = '<p class="error">' + escapeHtml(err.message) + '</p>';
|
|
1427
|
-
bodyEl.style.display = 'block';
|
|
1428
|
-
})
|
|
1429
|
-
.then(function () { btn.disabled = false; });
|
|
1430
|
-
});
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
var addButtons = listEl.querySelectorAll('.skillgarden-add-btn');
|
|
1434
|
-
for (var j = 0; j < addButtons.length; j++) {
|
|
1435
|
-
addButtons[j].addEventListener('click', function (ev) {
|
|
1436
|
-
var btn = ev.currentTarget;
|
|
1437
|
-
var category = btn.getAttribute('data-category');
|
|
1438
|
-
var id = btn.getAttribute('data-id');
|
|
1439
|
-
var originalText = btn.textContent;
|
|
1440
|
-
btn.disabled = true;
|
|
1441
|
-
btn.textContent = 'Adding…';
|
|
1442
|
-
performSkillgardenAdd(name, category, id, btn, originalText, false);
|
|
1443
|
-
});
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
1260
|
function wireSkillsHandlers(name) {
|
|
1448
1261
|
var panel = skillsPanelEl();
|
|
1449
1262
|
if (!panel) return;
|
|
1450
1263
|
var editingSkillId = null;
|
|
1451
1264
|
|
|
1452
|
-
loadSkillgardenCatalogSection(name);
|
|
1453
|
-
var skillgardenFilterInput = panel.querySelector('#skillgardenFilterInput');
|
|
1454
|
-
if (skillgardenFilterInput) {
|
|
1455
|
-
skillgardenFilterInput.addEventListener('input', function () {
|
|
1456
|
-
if (!skillgardenCatalog) return;
|
|
1457
|
-
applySkillgardenCatalog(name, filteredSkillgardenCatalog(skillgardenFilterInput.value.trim()));
|
|
1458
|
-
});
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
1265
|
// Write/Preview toggle for the Body field — same idea as GitHub's own
|
|
1462
1266
|
// issue/PR editor: the textarea and the rendered preview show/hide
|
|
1463
1267
|
// each other, the textarea's value is the only source of truth (the
|
package/dist/web/env-admin.d.ts
CHANGED
|
@@ -4,13 +4,13 @@ export interface DeclaredEnvVar {
|
|
|
4
4
|
name: string;
|
|
5
5
|
description?: string;
|
|
6
6
|
secret: boolean;
|
|
7
|
-
|
|
7
|
+
abilityName: string;
|
|
8
8
|
set: boolean;
|
|
9
9
|
}
|
|
10
|
-
/** Every env var any
|
|
11
|
-
* needs, deduplicated by name —
|
|
12
|
-
* cross-
|
|
13
|
-
* *project*, not per agent, so two unrelated
|
|
10
|
+
/** Every env var any ability installed for `agentName` declared it
|
|
11
|
+
* needs, deduplicated by name — ABILITIES.md's own open question on
|
|
12
|
+
* cross-ability name collisions applies here too (one .env per
|
|
13
|
+
* *project*, not per agent, so two unrelated abilities declaring the
|
|
14
14
|
* same name can't actually be told apart; the first one seen wins the
|
|
15
15
|
* description shown). `set` is read live off `process.env`, not cached,
|
|
16
16
|
* so it reflects whatever the last `setEnvVar` call — or a plain
|
|
@@ -19,7 +19,7 @@ export declare function listDeclaredEnvVars(agentName: string): DeclaredEnvVar[]
|
|
|
19
19
|
/** Upserts `NAME=VALUE` into the project's `.env` file — preserving
|
|
20
20
|
* every other line (comments, blank lines, unrelated keys) — and
|
|
21
21
|
* applies it to *this* running process immediately via `process.env`,
|
|
22
|
-
* so a newly-installed
|
|
22
|
+
* so a newly-installed ability's tools work without a restart. Callers
|
|
23
23
|
* (the PUT route in adapters/http.ts) are responsible for refusing to
|
|
24
24
|
* call this at all when `LOOPENGINE_ADMIN_AUTH` isn't set — this
|
|
25
25
|
* function itself has no notion of HTTP auth, it just writes. */
|
package/dist/web/env-admin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Backs the Admin UI's "Environment" section — every env var
|
|
2
|
-
// (see
|
|
3
|
-
// across every
|
|
1
|
+
// Backs the Admin UI's "Environment" section — every env var an ability
|
|
2
|
+
// (see ABILITIES.md, bin/ability-manager.ts) declared as required,
|
|
3
|
+
// across every ability installed for an agent, with set/not-set status
|
|
4
4
|
// only. A value marked `secret` is never echoed back once set — same
|
|
5
5
|
// never-echo-a-secret rule web/http-tool-admin.ts's own `{{ENV_VAR}}`
|
|
6
6
|
// header handling already establishes for a tool's own secrets.
|
|
@@ -13,12 +13,12 @@ export class EnvVarNameError extends Error {
|
|
|
13
13
|
// validates a {{ENV_VAR}} header reference against.
|
|
14
14
|
const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
15
15
|
function provenancePath(agentName) {
|
|
16
|
-
return join(agentDir(agentName), '.loopengine-
|
|
16
|
+
return join(agentDir(agentName), '.loopengine-abilities.json');
|
|
17
17
|
}
|
|
18
|
-
/** Every env var any
|
|
19
|
-
* needs, deduplicated by name —
|
|
20
|
-
* cross-
|
|
21
|
-
* *project*, not per agent, so two unrelated
|
|
18
|
+
/** Every env var any ability installed for `agentName` declared it
|
|
19
|
+
* needs, deduplicated by name — ABILITIES.md's own open question on
|
|
20
|
+
* cross-ability name collisions applies here too (one .env per
|
|
21
|
+
* *project*, not per agent, so two unrelated abilities declaring the
|
|
22
22
|
* same name can't actually be told apart; the first one seen wins the
|
|
23
23
|
* description shown). `set` is read live off `process.env`, not cached,
|
|
24
24
|
* so it reflects whatever the last `setEnvVar` call — or a plain
|
|
@@ -30,7 +30,7 @@ export function listDeclaredEnvVars(agentName) {
|
|
|
30
30
|
const provenance = JSON.parse(readFileSync(path, 'utf8'));
|
|
31
31
|
const seen = new Set();
|
|
32
32
|
const result = [];
|
|
33
|
-
for (const [
|
|
33
|
+
for (const [abilityName, record] of Object.entries(provenance)) {
|
|
34
34
|
for (const decl of record.env) {
|
|
35
35
|
if (seen.has(decl.name))
|
|
36
36
|
continue;
|
|
@@ -39,7 +39,7 @@ export function listDeclaredEnvVars(agentName) {
|
|
|
39
39
|
name: decl.name,
|
|
40
40
|
description: decl.description,
|
|
41
41
|
secret: decl.secret === true,
|
|
42
|
-
|
|
42
|
+
abilityName,
|
|
43
43
|
set: process.env[decl.name] !== undefined,
|
|
44
44
|
});
|
|
45
45
|
}
|
|
@@ -68,7 +68,7 @@ function serializeEnvValue(value) {
|
|
|
68
68
|
/** Upserts `NAME=VALUE` into the project's `.env` file — preserving
|
|
69
69
|
* every other line (comments, blank lines, unrelated keys) — and
|
|
70
70
|
* applies it to *this* running process immediately via `process.env`,
|
|
71
|
-
* so a newly-installed
|
|
71
|
+
* so a newly-installed ability's tools work without a restart. Callers
|
|
72
72
|
* (the PUT route in adapters/http.ts) are responsible for refusing to
|
|
73
73
|
* call this at all when `LOOPENGINE_ADMIN_AUTH` isn't set — this
|
|
74
74
|
* function itself has no notion of HTTP auth, it just writes. */
|
|
@@ -40,9 +40,9 @@ export declare function toCamelCase(snakeCase: string): string;
|
|
|
40
40
|
export declare function addToolToIndex(toolsIndexPath: string, toolFileName: string, exportName: string): void;
|
|
41
41
|
/** addToolToIndex's own undo — removes the named tool's import line and
|
|
42
42
|
* array entry. Best-effort, not "refuse rather than guess" like
|
|
43
|
-
* addToolToIndex: this only ever runs against a tool
|
|
43
|
+
* addToolToIndex: this only ever runs against a tool ability-manager.ts
|
|
44
44
|
* itself installed (so the exact shape addToolToIndex wrote is known),
|
|
45
|
-
* and the caller (
|
|
45
|
+
* and the caller (removeAbility) has already deleted the tool's .ts
|
|
46
46
|
* file — leaving a stale entry behind, the previous behavior, means a
|
|
47
47
|
* dangling import that fails the next build, which is strictly worse
|
|
48
48
|
* than a no-op here if the file turns out not to match (hand-edited
|
|
@@ -191,8 +191,8 @@ export function addToolToIndex(toolsIndexPath, toolFileName, exportName) {
|
|
|
191
191
|
if (!arrayMatch) {
|
|
192
192
|
throw new HttpToolIndexShapeError(`${toolsIndexPath} doesn't match the expected "export const tools: ToolDefinition[] = [...]" shape — add this tool to it by hand instead.`);
|
|
193
193
|
}
|
|
194
|
-
// Idempotent — a caller that adds the same tool twice (e.g.
|
|
195
|
-
// manager.ts's
|
|
194
|
+
// Idempotent — a caller that adds the same tool twice (e.g. ability-
|
|
195
|
+
// manager.ts's removeAbility not having cleaned up this same file
|
|
196
196
|
// before a re-add, or simply calling this twice by mistake) gets a
|
|
197
197
|
// no-op the second time instead of a duplicate `import { X }`
|
|
198
198
|
// declaration, which is a TypeScript syntax error ("Duplicate
|
|
@@ -213,9 +213,9 @@ export function addToolToIndex(toolsIndexPath, toolFileName, exportName) {
|
|
|
213
213
|
}
|
|
214
214
|
/** addToolToIndex's own undo — removes the named tool's import line and
|
|
215
215
|
* array entry. Best-effort, not "refuse rather than guess" like
|
|
216
|
-
* addToolToIndex: this only ever runs against a tool
|
|
216
|
+
* addToolToIndex: this only ever runs against a tool ability-manager.ts
|
|
217
217
|
* itself installed (so the exact shape addToolToIndex wrote is known),
|
|
218
|
-
* and the caller (
|
|
218
|
+
* and the caller (removeAbility) has already deleted the tool's .ts
|
|
219
219
|
* file — leaving a stale entry behind, the previous behavior, means a
|
|
220
220
|
* dangling import that fails the next build, which is strictly worse
|
|
221
221
|
* than a no-op here if the file turns out not to match (hand-edited
|
package/dist/web/skills-admin.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// only adding/editing/removing one through this UI is out of scope.
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { dirname, join } from 'node:path';
|
|
14
|
-
import { parseSkillFile } from 'skillgarden';
|
|
14
|
+
import { parseSkillFile } from '../core/skillgarden/index.js';
|
|
15
15
|
import { agentDir } from '../core/gateway-tools.js';
|
|
16
16
|
export class SkillInvalidIdError extends Error {
|
|
17
17
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopengine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"dist/index.d.ts",
|
|
24
24
|
"dist/bin/cli.js",
|
|
25
25
|
"dist/bin/cli.d.ts",
|
|
26
|
-
"dist/bin/
|
|
27
|
-
"dist/bin/
|
|
26
|
+
"dist/bin/ability-manager.js",
|
|
27
|
+
"dist/bin/ability-manager.d.ts",
|
|
28
28
|
"dist/core/run-agent.js",
|
|
29
29
|
"dist/core/run-agent.d.ts",
|
|
30
30
|
"dist/core/loop-events.js",
|
|
@@ -55,6 +55,12 @@
|
|
|
55
55
|
"dist/core/agent-as-tool.d.ts",
|
|
56
56
|
"dist/core/gateway-tools.js",
|
|
57
57
|
"dist/core/gateway-tools.d.ts",
|
|
58
|
+
"dist/core/mcpplug.js",
|
|
59
|
+
"dist/core/mcpplug.d.ts",
|
|
60
|
+
"dist/core/toollane.js",
|
|
61
|
+
"dist/core/toollane.d.ts",
|
|
62
|
+
"dist/core/sessionknit.js",
|
|
63
|
+
"dist/core/sessionknit.d.ts",
|
|
58
64
|
"dist/core/budget.js",
|
|
59
65
|
"dist/core/budget.d.ts",
|
|
60
66
|
"dist/core/compaction.js",
|
|
@@ -68,6 +74,7 @@
|
|
|
68
74
|
"dist/core/http-notify-triggers",
|
|
69
75
|
"dist/core/system-tools",
|
|
70
76
|
"dist/core/system-skills",
|
|
77
|
+
"dist/core/skillgarden",
|
|
71
78
|
"dist/core/durable-approvals.js",
|
|
72
79
|
"dist/core/durable-approvals.d.ts",
|
|
73
80
|
"dist/web",
|
|
@@ -85,12 +92,8 @@
|
|
|
85
92
|
"@anthropic-ai/sdk": "^0.116.0",
|
|
86
93
|
"actauth": "^0.0.12",
|
|
87
94
|
"ioredis": "^6.0.0",
|
|
88
|
-
"mcpplug": "^0.0.1",
|
|
89
95
|
"openai": "^7.4.0",
|
|
90
96
|
"semver": "^7.8.5",
|
|
91
|
-
"sessionknit": "^0.0.2",
|
|
92
|
-
"skillgarden": "^0.0.9",
|
|
93
|
-
"toollane": "^0.0.1",
|
|
94
97
|
"typescript": "^5.7.0",
|
|
95
98
|
"yaml": "^2.6.0"
|
|
96
99
|
},
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export declare class SkillgardenUnavailableError extends Error {
|
|
2
|
-
}
|
|
3
|
-
export interface SkillgardenCatalogEntry {
|
|
4
|
-
category: string;
|
|
5
|
-
id: string;
|
|
6
|
-
description: string;
|
|
7
|
-
}
|
|
8
|
-
export interface SkillgardenCatalogDetail extends SkillgardenCatalogEntry {
|
|
9
|
-
body: string;
|
|
10
|
-
}
|
|
11
|
-
export declare function listSkillgardenCatalog(): SkillgardenCatalogEntry[];
|
|
12
|
-
export declare function readSkillgardenCatalogEntry(category: string, id: string): SkillgardenCatalogDetail;
|
|
13
|
-
/** Copies `registry/<category>/<id>/` into `agents/<agentName>/skills/<id>/`
|
|
14
|
-
* — delegates entirely to skillgarden's own `addSkill`, which already
|
|
15
|
-
* knows this exact `agents/<agent>/skills` destination convention (see
|
|
16
|
-
* its own doc comment) and already refuses (or, with `force`,
|
|
17
|
-
* overwrites) an existing destination. Returns the bare skill id actually
|
|
18
|
-
* installed — never category-qualified, matching `addSkill`'s own
|
|
19
|
-
* "category is a registry-organization concept, not forced onto what a
|
|
20
|
-
* caller now owns" convention. */
|
|
21
|
-
export declare function addSkillgardenSkillToAgent(agentName: string, category: string, id: string, force?: boolean): {
|
|
22
|
-
id: string;
|
|
23
|
-
path: string;
|
|
24
|
-
};
|