loopengine 0.3.8 → 0.3.9
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/core/messaging/connectors/telegram/index.d.ts +4 -0
- package/dist/core/messaging/connectors/telegram/index.js +10 -1
- package/dist/core/model-calls/openai-model-call.d.ts +4 -0
- package/dist/core/model-calls/openai-model-call.js +12 -2
- package/dist/core/system-agents/workflow-planner.js +2 -1
- package/dist/web/actauth-admin.d.ts +33 -1
- package/dist/web/actauth-admin.js +39 -2
- package/dist/web/agents-config-page.js +29 -24
- package/dist/web/console-shell.js +2 -2
- package/dist/web/notifier-admin.d.ts +2 -0
- package/dist/web/notifier-admin.js +3 -1
- package/dist/web/playground.js +1 -1
- package/dist/web/templates-admin.js +5 -12
- package/dist/web/workflow-admin.js +8 -15
- package/package.json +1 -1
|
@@ -31,9 +31,13 @@ export declare function setTelegramWebhook(botToken: string, url: string, secret
|
|
|
31
31
|
botUsername?: string;
|
|
32
32
|
}>;
|
|
33
33
|
/** Where the bot's updates go now, and the last delivery error Telegram saw. */
|
|
34
|
+
/** Where the bot's updates go now; the last delivery error Telegram saw
|
|
35
|
+
* and when (it keeps it until a newer one); and how many are waiting. */
|
|
34
36
|
export declare function telegramWebhookInfo(botToken: string): Promise<{
|
|
35
37
|
ok: boolean;
|
|
36
38
|
url?: string;
|
|
37
39
|
lastError?: string;
|
|
40
|
+
lastErrorAt?: string;
|
|
41
|
+
pending?: number;
|
|
38
42
|
error?: string;
|
|
39
43
|
}>;
|
|
@@ -121,11 +121,20 @@ export async function setTelegramWebhook(botToken, url, secret) {
|
|
|
121
121
|
return { ok: true, botUsername: await telegramBotUsername(botToken) };
|
|
122
122
|
}
|
|
123
123
|
/** Where the bot's updates go now, and the last delivery error Telegram saw. */
|
|
124
|
+
/** Where the bot's updates go now; the last delivery error Telegram saw
|
|
125
|
+
* and when (it keeps it until a newer one); and how many are waiting. */
|
|
124
126
|
export async function telegramWebhookInfo(botToken) {
|
|
125
127
|
const info = await callTelegram(botToken, 'getWebhookInfo', {});
|
|
126
128
|
if (!info.ok)
|
|
127
129
|
return { ok: false, error: telegramSetupError(info.description) };
|
|
128
|
-
|
|
130
|
+
const r = info.result;
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
url: r?.url || undefined,
|
|
134
|
+
...(r?.last_error_message ? { lastError: r.last_error_message } : {}),
|
|
135
|
+
...(r?.last_error_date ? { lastErrorAt: new Date(r.last_error_date * 1000).toISOString() } : {}),
|
|
136
|
+
...(r?.pending_update_count ? { pending: r.pending_update_count } : {}),
|
|
137
|
+
};
|
|
129
138
|
}
|
|
130
139
|
function telegramSetupError(description) {
|
|
131
140
|
if (/unauthorized|not found/i.test(description ?? ''))
|
|
@@ -34,5 +34,9 @@ export declare function toResponsesInput(message: Message): ResponseInputItem[];
|
|
|
34
34
|
* blocks, message text, and function calls as tool_use blocks (their
|
|
35
35
|
* call_id is the id a tool result answers). */
|
|
36
36
|
export declare function fromResponse(response: OpenAI.Responses.Response): ModelResponse;
|
|
37
|
+
/** OpenAI's reasoning models (GPT-5 on, the o-series), which reason by
|
|
38
|
+
* default and take a reasoning effort; older ones (gpt-4o, gpt-4.1)
|
|
39
|
+
* reject one. */
|
|
40
|
+
export declare function isOpenAIReasoningModel(model: string): boolean;
|
|
37
41
|
export declare function createOpenAIModelCall(options: OpenAIModelCallOptions): ModelCall;
|
|
38
42
|
export {};
|
|
@@ -232,10 +232,20 @@ export function fromResponse(response) {
|
|
|
232
232
|
const usage = response.usage ? { input_tokens: response.usage.input_tokens, output_tokens: response.usage.output_tokens } : undefined;
|
|
233
233
|
return { stop_reason, content, ...(usage ? { usage } : {}) };
|
|
234
234
|
}
|
|
235
|
+
/** OpenAI's reasoning models (GPT-5 on, the o-series), which reason by
|
|
236
|
+
* default and take a reasoning effort; older ones (gpt-4o, gpt-4.1)
|
|
237
|
+
* reject one. */
|
|
238
|
+
export function isOpenAIReasoningModel(model) {
|
|
239
|
+
return /^(gpt-(5|6|7|8|9)|o\d)/i.test(model);
|
|
240
|
+
}
|
|
235
241
|
export function createOpenAIModelCall(options) {
|
|
236
242
|
const client = options.client ?? new OpenAI({ apiKey: options.apiKey });
|
|
243
|
+
// A reasoning model with no effort set reasons at OpenAI's default
|
|
244
|
+
// anyway (medium), which used to spend the 4096-token cap before it
|
|
245
|
+
// got to a tool call. Unset is low for those: quick, and room to act.
|
|
246
|
+
const reasoningEffort = options.reasoningEffort ?? (isOpenAIReasoningModel(options.model) ? 'low' : undefined);
|
|
237
247
|
// Reasoning counts toward max_output_tokens.
|
|
238
|
-
const maxTokens = options.maxTokens ?? (
|
|
248
|
+
const maxTokens = options.maxTokens ?? (reasoningEffort && reasoningEffort !== 'none' ? 16000 : 4096);
|
|
239
249
|
return async (messages, system, tools) => {
|
|
240
250
|
const response = await client.responses.create({
|
|
241
251
|
model: options.model,
|
|
@@ -245,7 +255,7 @@ export function createOpenAIModelCall(options) {
|
|
|
245
255
|
tools: tools.length
|
|
246
256
|
? tools.map((t) => ({ type: 'function', name: t.name, description: t.description, parameters: t.input_schema, strict: false }))
|
|
247
257
|
: undefined,
|
|
248
|
-
...(
|
|
258
|
+
...(reasoningEffort ? { reasoning: { effort: reasoningEffort } } : {}),
|
|
249
259
|
store: false,
|
|
250
260
|
include: ['reasoning.encrypted_content'],
|
|
251
261
|
});
|
|
@@ -411,7 +411,8 @@ export function workflowTemplate(plan) {
|
|
|
411
411
|
...(plan.apps?.length ? { apps: plan.apps } : {}),
|
|
412
412
|
keys: plan.keys,
|
|
413
413
|
skills: [{ id: plan.id, description: plan.skill.description, body: plan.skill.body }],
|
|
414
|
-
|
|
414
|
+
// Named after the tool: readable, and made unique if taken.
|
|
415
|
+
rules: plan.rules.map((r) => ({ name: r.tool, tool: r.tool, decision: r.decision })),
|
|
415
416
|
...(plan.trigger.kind === 'schedule' && plan.trigger.every
|
|
416
417
|
? { schedule: { every: plan.trigger.every, ...(plan.trigger.at ? { at: plan.trigger.at } : {}), ...(plan.trigger.weekday !== undefined ? { weekday: plan.trigger.weekday } : {}), name: plan.name, message: plan.startMessage, workflowId: plan.id, ...(plan.notify?.on ? { notify: true } : {}) } }
|
|
417
418
|
: {}),
|
|
@@ -23,7 +23,38 @@ export declare function readActauthConfig(agentName: string): ActauthConfigView;
|
|
|
23
23
|
/** Appends a new rule. `rule.name` must be unique among this agent's
|
|
24
24
|
* existing rules — it's the only handle update/removeActauthRule below
|
|
25
25
|
* have to find it again, so a duplicate would make those ambiguous. */
|
|
26
|
-
|
|
26
|
+
/** Adds a rule at the end, or, with `before`, just above that named rule
|
|
27
|
+
* (the first rule that matches a call decides it, so above is what makes
|
|
28
|
+
* it count). */
|
|
29
|
+
export declare function addActauthRule(agentName: string, rule: ActauthRuleInput, options?: {
|
|
30
|
+
before?: string;
|
|
31
|
+
}): void;
|
|
32
|
+
type RuleDecision = 'allow' | 'ask' | 'deny';
|
|
33
|
+
export interface PlannedRule {
|
|
34
|
+
tool: string;
|
|
35
|
+
decision: RuleDecision;
|
|
36
|
+
/** Its name in actauth.yml; the tool's name (made unique) by default. */
|
|
37
|
+
name?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface PlannedRulesResult {
|
|
40
|
+
added: string[];
|
|
41
|
+
/** Tools that already had a rule with the same decision. */
|
|
42
|
+
unchanged: string[];
|
|
43
|
+
/** Tools whose first rule is stricter or looser than planned and was
|
|
44
|
+
* kept as it is: the plan never loosens a decision the agent has. */
|
|
45
|
+
kept: {
|
|
46
|
+
tool: string;
|
|
47
|
+
has: RuleDecision;
|
|
48
|
+
wants: RuleDecision;
|
|
49
|
+
}[];
|
|
50
|
+
}
|
|
51
|
+
/** Gives the planned tools their decisions, once the agent's abilities and
|
|
52
|
+
* apps are installed (their own rules in place), so nothing is added twice:
|
|
53
|
+
* a tool with no rule gets one; one whose first matching rule already says
|
|
54
|
+
* the same is left alone; one a plan wants stricter (ask where an ability
|
|
55
|
+
* allows) gets its rule just above, so it counts; one a plan wants looser
|
|
56
|
+
* keeps what it has, and is reported. */
|
|
57
|
+
export declare function applyPlannedRules(agentName: string, planned: PlannedRule[]): PlannedRulesResult;
|
|
27
58
|
/** Mutates an existing rule's scope/tool/decision in place (via the
|
|
28
59
|
* node's own `.set()`, not remove-then-re-add), so any comment attached
|
|
29
60
|
* to that specific rule in the source YAML survives the edit. */
|
|
@@ -34,3 +65,4 @@ export declare function updateActauthRule(agentName: string, ruleName: string, u
|
|
|
34
65
|
}): void;
|
|
35
66
|
export declare function removeActauthRule(agentName: string, ruleName: string): void;
|
|
36
67
|
export declare function setDefaultDecision(agentName: string, decision: Decision): void;
|
|
68
|
+
export {};
|
|
@@ -62,16 +62,53 @@ function findRuleIndex(rules, ruleName) {
|
|
|
62
62
|
/** Appends a new rule. `rule.name` must be unique among this agent's
|
|
63
63
|
* existing rules — it's the only handle update/removeActauthRule below
|
|
64
64
|
* have to find it again, so a duplicate would make those ambiguous. */
|
|
65
|
-
|
|
65
|
+
/** Adds a rule at the end, or, with `before`, just above that named rule
|
|
66
|
+
* (the first rule that matches a call decides it, so above is what makes
|
|
67
|
+
* it count). */
|
|
68
|
+
export function addActauthRule(agentName, rule, options = {}) {
|
|
66
69
|
const dir = agentDir(agentName);
|
|
67
70
|
const { path, doc } = loadDoc(dir);
|
|
68
71
|
const rules = doc.get('rules');
|
|
69
72
|
if (findRuleIndex(rules, rule.name) !== -1) {
|
|
70
73
|
throw new ActauthRuleExistsError(`A rule named '${rule.name}' already exists for '${agentName}' — pick a different name or remove it first.`);
|
|
71
74
|
}
|
|
72
|
-
|
|
75
|
+
const node = doc.createNode({ name: rule.name, scope: rule.scope, tool: rule.tool, decision: rule.decision });
|
|
76
|
+
const at = options.before ? findRuleIndex(rules, options.before) : -1;
|
|
77
|
+
if (at === -1)
|
|
78
|
+
rules.add(node);
|
|
79
|
+
else
|
|
80
|
+
rules.items.splice(at, 0, node);
|
|
73
81
|
saveDoc(dir, path, doc);
|
|
74
82
|
}
|
|
83
|
+
const STRICTNESS = { allow: 0, ask: 1, deny: 2 };
|
|
84
|
+
/** Gives the planned tools their decisions, once the agent's abilities and
|
|
85
|
+
* apps are installed (their own rules in place), so nothing is added twice:
|
|
86
|
+
* a tool with no rule gets one; one whose first matching rule already says
|
|
87
|
+
* the same is left alone; one a plan wants stricter (ask where an ability
|
|
88
|
+
* allows) gets its rule just above, so it counts; one a plan wants looser
|
|
89
|
+
* keeps what it has, and is reported. */
|
|
90
|
+
export function applyPlannedRules(agentName, planned) {
|
|
91
|
+
const result = { added: [], unchanged: [], kept: [] };
|
|
92
|
+
for (const rule of planned) {
|
|
93
|
+
const current = readActauthConfig(agentName).rules;
|
|
94
|
+
const first = current.find((r) => r.tool === rule.tool);
|
|
95
|
+
if (first && first.decision === rule.decision) {
|
|
96
|
+
result.unchanged.push(rule.tool);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (first && STRICTNESS[rule.decision] < STRICTNESS[first.decision]) {
|
|
100
|
+
result.kept.push({ tool: rule.tool, has: first.decision, wants: rule.decision });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const base = rule.name ?? rule.tool;
|
|
104
|
+
let name = base;
|
|
105
|
+
for (let i = 2; current.some((r) => r.name === name); i++)
|
|
106
|
+
name = `${base}-${i}`;
|
|
107
|
+
addActauthRule(agentName, { name, scope: '*/*', tool: rule.tool, decision: rule.decision }, first ? { before: first.name } : {});
|
|
108
|
+
result.added.push(name);
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
75
112
|
/** Mutates an existing rule's scope/tool/decision in place (via the
|
|
76
113
|
* node's own `.set()`, not remove-then-re-add), so any comment attached
|
|
77
114
|
* to that specific rule in the source YAML survives the edit. */
|
|
@@ -438,10 +438,6 @@ ${consoleShellScript}
|
|
|
438
438
|
return attempt().catch(function () { return attempt(); });
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
function badge(label) {
|
|
442
|
-
return '<span class="badge ' + (label === 'custom' ? 'badge-custom' : 'badge-default') + '">' + escapeHtml(label) + '</span>';
|
|
443
|
-
}
|
|
444
|
-
|
|
445
441
|
// A small, deliberately non-CommonMark markdown-to-HTML renderer for
|
|
446
442
|
// the skill Body field's Preview toggle (see wireSkillsHandlers) — this
|
|
447
443
|
// page stays a self-contained string, no build step, no CDN (same
|
|
@@ -899,6 +895,7 @@ ${consoleShellScript}
|
|
|
899
895
|
if (!r.ok) { el.innerHTML = '<p class="error">Could not load workflows: ' + escapeHtml(r.body.error || 'unknown error') + '</p>'; return; }
|
|
900
896
|
var workflows = r.body.workflows || [];
|
|
901
897
|
fillWorkflowsCard(workflows);
|
|
898
|
+
leShell.setCount('workflows', workflows.length);
|
|
902
899
|
workflowsById = {};
|
|
903
900
|
workflows.forEach(function (w) { workflowsById[w.id] = w; });
|
|
904
901
|
el.innerHTML = renderWorkflowsHtml(name, workflows);
|
|
@@ -1224,7 +1221,11 @@ ${consoleShellScript}
|
|
|
1224
1221
|
var action = btn.dataset.action;
|
|
1225
1222
|
if (action === 'cancel' && btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again to remove'; return; }
|
|
1226
1223
|
btn.disabled = true;
|
|
1227
|
-
fetchJsonRetryOnce('/schedules/' + encodeURIComponent(btn.dataset.id) + '/' + action, { method: 'POST' }).then(function () {
|
|
1224
|
+
fetchJsonRetryOnce('/schedules/' + encodeURIComponent(btn.dataset.id) + '/' + action, { method: 'POST' }).then(function () {
|
|
1225
|
+
loadWorkflowsSection(name);
|
|
1226
|
+
// The Schedules tab's count, and the Overview's tile.
|
|
1227
|
+
fillOverviewCards(name);
|
|
1228
|
+
});
|
|
1228
1229
|
});
|
|
1229
1230
|
}
|
|
1230
1231
|
card.querySelector('.workflow-run').addEventListener('click', function (ev) {
|
|
@@ -1277,6 +1278,7 @@ ${consoleShellScript}
|
|
|
1277
1278
|
var waiting = active.filter(function (t) { return t.status === 'awaiting_approval'; }).length;
|
|
1278
1279
|
var next = active.map(function (t) { return t.next_run_at; }).filter(Boolean).sort()[0];
|
|
1279
1280
|
set('ovSchedulesValue', String(active.length));
|
|
1281
|
+
leShell.setCount('schedules', active.length);
|
|
1280
1282
|
var schedulesDetail = [];
|
|
1281
1283
|
if (waiting) schedulesDetail.push(waiting + ' waiting for approval');
|
|
1282
1284
|
if (next) schedulesDetail.push('Next run ' + escapeHtml(new Date(next).toLocaleString()));
|
|
@@ -1310,7 +1312,10 @@ ${consoleShellScript}
|
|
|
1310
1312
|
'<dt>Provider</dt><dd>' + escapeHtml(cfg.model.provider) + '</dd>' +
|
|
1311
1313
|
'<dt>Model</dt><dd>' + escapeHtml(cfg.model.model || '(provider default)') + '</dd>' +
|
|
1312
1314
|
'<dt>Max tokens</dt><dd>' + escapeHtml(cfg.model.maxTokens != null ? cfg.model.maxTokens : '(default)') + '</dd>' +
|
|
1313
|
-
|
|
1315
|
+
// Unset is low for OpenAI's reasoning models (core/model-calls/openai-model-call.ts).
|
|
1316
|
+
'<dt>Reasoning effort</dt><dd>' + escapeHtml(cfg.model.reasoningEffort ||
|
|
1317
|
+
(cfg.model.provider === 'openai' && /^(gpt-[5-9]|o[0-9])/i.test(cfg.model.model || '') ? 'low (default)'
|
|
1318
|
+
: REASONING_PROVIDERS.indexOf(cfg.model.provider) !== -1 ? '(model default)' : 'n/a')) + '</dd>' +
|
|
1314
1319
|
'</dl>' +
|
|
1315
1320
|
'<form class="add-source" id="modelForm" style="display:none">' +
|
|
1316
1321
|
'<label>Provider' +
|
|
@@ -1326,7 +1331,7 @@ ${consoleShellScript}
|
|
|
1326
1331
|
'<label>Max tokens <span class="hint">(blank for provider default)</span>' +
|
|
1327
1332
|
'<input type="number" name="maxTokens" min="1" step="1" value="' + (cfg.model.maxTokens != null ? escapeHtml(cfg.model.maxTokens) : '') + '">' +
|
|
1328
1333
|
'</label>' +
|
|
1329
|
-
'<label id="reasoningEffortField">Reasoning effort <span class="hint">(how much the model thinks before answering: none turns it off, unset uses the model\\\'s default)</span>' +
|
|
1334
|
+
'<label id="reasoningEffortField">Reasoning effort <span class="hint">(how much the model thinks before answering: none turns it off, unset uses the model\\\'s default, and low for OpenAI\\\'s reasoning models)</span>' +
|
|
1330
1335
|
'<select name="reasoningEffort">' +
|
|
1331
1336
|
['', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'].map(function (v) {
|
|
1332
1337
|
return '<option value="' + v + '"' + (v === (cfg.model.reasoningEffort || '') ? ' selected' : '') + '>' + (v || '(unset)') + '</option>';
|
|
@@ -1342,11 +1347,6 @@ ${consoleShellScript}
|
|
|
1342
1347
|
return renderOverviewCards(cfg) +
|
|
1343
1348
|
'<section>' + systemPromptSection + '</section>' +
|
|
1344
1349
|
'<section>' + modelSection + '</section>' +
|
|
1345
|
-
'<section><h3>Hooks</h3><dl class="kv">' +
|
|
1346
|
-
'<dt>sessionIdFor</dt><dd>' + badge(cfg.sessionIdFor.indexOf('custom') === 0 ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.sessionIdFor) + '</span></dd>' +
|
|
1347
|
-
'<dt>tenantFor</dt><dd>' + badge(cfg.tenantFor.indexOf('custom') === 0 ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.tenantFor) + '</span></dd>' +
|
|
1348
|
-
'<dt>isSafeTool</dt><dd>' + badge(cfg.isSafeTool === 'custom' ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.isSafeTool) + '</span></dd>' +
|
|
1349
|
-
'</dl></section>' +
|
|
1350
1350
|
'<section><h3>Limits & budgets <button type="button" class="edit-btn" id="editLimitsBtn">Edit</button></h3>' +
|
|
1351
1351
|
'<dl class="kv" id="limitsDisplay">' +
|
|
1352
1352
|
'<dt>maxTurns</dt><dd>' + escapeHtml(cfg.maxTurns) + '</dd>' +
|
|
@@ -1367,7 +1367,6 @@ ${consoleShellScript}
|
|
|
1367
1367
|
'<button type="button" id="cancelLimitsBtn">Cancel</button>' +
|
|
1368
1368
|
'<div class="error" id="limitsError"></div>' +
|
|
1369
1369
|
'</form>' +
|
|
1370
|
-
'<dl class="kv"><dt>skillsDirs</dt><dd>' + escapeHtml(cfg.skillsDirs.join(', ') || '(none)') + '</dd></dl>' +
|
|
1371
1370
|
'</section>';
|
|
1372
1371
|
}
|
|
1373
1372
|
|
|
@@ -1956,7 +1955,10 @@ ${consoleShellScript}
|
|
|
1956
1955
|
(saved ? '<button type="button" class="delete-btn notifier-remove"' + (locked ? ' disabled' : '') + '>Remove</button>' : '') + '</div>' +
|
|
1957
1956
|
(isWebhook ? '<p class="hint" style="margin:0">A webhook always sends once saved, alongside the primary messaging app.</p>' : '') +
|
|
1958
1957
|
'<div class="hint notifier-result"></div>' +
|
|
1959
|
-
'</form
|
|
1958
|
+
'</form>' +
|
|
1959
|
+
// Who can decide and give jobs here: part of this app's settings.
|
|
1960
|
+
(channel === 'slack' || channel === 'telegram' ? renderApproversHtml(n, channel) : '') +
|
|
1961
|
+
'</details>';
|
|
1960
1962
|
}
|
|
1961
1963
|
|
|
1962
1964
|
function renderConnectionsHtml(n) {
|
|
@@ -2011,7 +2013,7 @@ ${consoleShellScript}
|
|
|
2011
2013
|
'<td><button type="button" class="delete-btn approver-remove" data-user="' + escapeHtml(a.user) + '"' + (n.canSave === false ? ' disabled' : '') + '>Remove</button></td></tr>';
|
|
2012
2014
|
}).join('');
|
|
2013
2015
|
var where = app === 'telegram' ? 'by replying to it' : 'by replying in its thread';
|
|
2014
|
-
return '<section class="approvers-section" data-app="' + app + '"
|
|
2016
|
+
return '<section class="approvers-section" data-app="' + app + '" style="max-width:560px;margin-top:18px;padding-top:14px;border-top:1px solid var(--le-line, #e5e7eb)"><h4 style="margin:0 0 6px">Approvers</h4>' +
|
|
2015
2017
|
(n.primary !== app ? '<p class="hint" style="margin:0 0 8px">' + title + ' isn't primary, so requests aren't sent there until it is. Jobs started there still use this list.</p>' : '') +
|
|
2016
2018
|
'<p class="muted" style="margin:0 0 10px">People who can approve, deny, change or answer a request ' + where + ', like “yes, go ahead” or “make it $40”. A plain “yes” runs it; a reply that's less clear (“sure, send it”, “yes but check the amount”) shows exactly what will run with a Confirm button first, and a change is shown to approve first. Once anyone is listed, only approvers' button clicks count, and only approvers can give the agent jobs. Anyone can still ask about a request.</p>' +
|
|
2017
2019
|
(approvers.length
|
|
@@ -2101,8 +2103,7 @@ ${consoleShellScript}
|
|
|
2101
2103
|
'<p class="muted" style="margin:0 0 12px">' + renderNotifierStatus(n) + '</p>' +
|
|
2102
2104
|
(n.error ? '<p class="error">' + escapeHtml(n.error) + '</p>' : '') +
|
|
2103
2105
|
(n.source === 'code' ? '' : renderConnectionsHtml(n)) + '</section>' +
|
|
2104
|
-
|
|
2105
|
-
renderApproversHtml(n, 'telegram') +
|
|
2106
|
+
|
|
2106
2107
|
'<section><h3>Recent deliveries</h3>' + renderDeliveries(n.deliveries || []) + '</section>';
|
|
2107
2108
|
}
|
|
2108
2109
|
|
|
@@ -2251,8 +2252,16 @@ ${consoleShellScript}
|
|
|
2251
2252
|
fetchJsonRetryOnce(url).then(function (r) {
|
|
2252
2253
|
if (!r.ok) { out.textContent = r.body.error || 'Could not check'; return; }
|
|
2253
2254
|
var b = r.body;
|
|
2255
|
+
// Telegram keeps its last error until a newer one, so it says when.
|
|
2256
|
+
var when = '';
|
|
2257
|
+
if (b.lastErrorAt) {
|
|
2258
|
+
var mins = Math.max(0, Math.round((Date.now() - Date.parse(b.lastErrorAt)) / 60000));
|
|
2259
|
+
when = mins < 2 ? 'just now' : mins < 120 ? mins + ' minutes ago' : mins < 2880 ? Math.round(mins / 60) + ' hours ago' : Math.round(mins / 1440) + ' days ago';
|
|
2260
|
+
when = ' (' + when + ', ' + new Date(b.lastErrorAt).toLocaleString() + ')';
|
|
2261
|
+
}
|
|
2254
2262
|
out.textContent = (b.pointsHere ? 'Connected' + (b.botUsername ? ' (@' + b.botUsername + ')' : '') + '.' : b.url ? 'The bot sends its updates somewhere else: ' + b.url + '. Connect again to point it here.' : 'The bot isn' + String.fromCharCode(8217) + 't sending its updates anywhere. Connect again.') +
|
|
2255
|
-
(b.
|
|
2263
|
+
(b.pending ? ' ' + b.pending + ' update' + (b.pending === 1 ? '' : 's') + ' waiting to be delivered.' : '') +
|
|
2264
|
+
(b.lastError ? ' Telegram' + String.fromCharCode(8217) + 's last delivery error' + when + ': ' + b.lastError + (b.pointsHere && !b.pending ? ' Nothing is waiting, so it' + String.fromCharCode(8217) + 's likely from before and since fixed.' : '') : '');
|
|
2256
2265
|
});
|
|
2257
2266
|
});
|
|
2258
2267
|
}
|
|
@@ -3610,11 +3619,7 @@ ${consoleShellScript}
|
|
|
3610
3619
|
|
|
3611
3620
|
function renderSkillsTabHtml(cfg) {
|
|
3612
3621
|
var listHtml = cfg.skills.length ? cfg.skills.map(renderSkillCard).join('') : '<p class="hint">No skills yet.</p>';
|
|
3613
|
-
return '<section><h3>Skills
|
|
3614
|
-
'<dt>skillsDirs</dt><dd>' + escapeHtml(cfg.skillsDirs.join(', ') || '(none)') + '</dd>' +
|
|
3615
|
-
'<dt>skillIndexBudgetTokens</dt><dd>' + escapeHtml(cfg.skillIndexBudgetTokens) + '</dd>' +
|
|
3616
|
-
'</dl></section>' +
|
|
3617
|
-
'<section><h3>Skills (' + cfg.skills.length + ')</h3><div id="skillList">' + listHtml + '</div></section>' +
|
|
3622
|
+
return '<section><h3>Skills (' + cfg.skills.length + ')</h3><div id="skillList">' + listHtml + '</div></section>' +
|
|
3618
3623
|
'<section><h3 id="skillFormHeading">Add a skill</h3>' +
|
|
3619
3624
|
'<form class="add-source" id="skillForm">' +
|
|
3620
3625
|
'<label>Id <span class="hint">(lowercase, hyphen-separated — becomes the SKILL.md folder name; nested skills aren\\'t editable here)</span>' +
|
|
@@ -4501,7 +4506,7 @@ ${consoleShellScript}
|
|
|
4501
4506
|
prompt: cfg.systemPrompt,
|
|
4502
4507
|
chips: [modelChip(cfg), { text: 'unmatched calls: ' + cfg.permissions.defaultDecision }],
|
|
4503
4508
|
tab: currentTab,
|
|
4504
|
-
counts: { tools: cfg.tools.length, skills: cfg.skills.length, abilities: cfg.abilityCount,
|
|
4509
|
+
counts: { tools: cfg.tools.length, skills: cfg.skills.length, abilities: cfg.abilityCount, workflows: cfg.skills.filter(function (s) { return s.workflow; }).length },
|
|
4505
4510
|
}) +
|
|
4506
4511
|
'<div class="le-scroll"><div class="le-page" style="display:block">' +
|
|
4507
4512
|
'<div class="tab-panel" data-tab-panel="overview">' + renderOverviewHtml(cfg) + '</div>' +
|
|
@@ -56,8 +56,8 @@ export const consoleShellScript = `<script>
|
|
|
56
56
|
// ability on the Apps tab (id 'abilities', so older links still work).
|
|
57
57
|
// ?tab=tools and ?tab=skills still open Advanced.
|
|
58
58
|
var TABS = [
|
|
59
|
-
['overview', 'Overview'], ['workflows', 'Workflows'], ['abilities', 'Apps'], ['
|
|
60
|
-
['messaging', 'Messaging'], ['schedules', 'Schedules'], ['
|
|
59
|
+
['overview', 'Overview'], ['workflows', 'Workflows'], ['abilities', 'Apps'], ['chat', 'Chat'],
|
|
60
|
+
['messaging', 'Messaging'], ['schedules', 'Schedules'], ['actauth', 'Permissions'], ['env', 'Environment'], ['advanced', 'Advanced']
|
|
61
61
|
];
|
|
62
62
|
|
|
63
63
|
function esc(s) {
|
|
@@ -127,6 +127,8 @@ export declare function setupAgentTelegramWebhook(config: AgentConfig, baseUrl:
|
|
|
127
127
|
export declare function agentTelegramWebhookInfo(config: AgentConfig): Promise<{
|
|
128
128
|
url?: string;
|
|
129
129
|
lastError?: string;
|
|
130
|
+
lastErrorAt?: string;
|
|
131
|
+
pending?: number;
|
|
130
132
|
pointsHere: boolean;
|
|
131
133
|
botUsername?: string;
|
|
132
134
|
}>;
|
|
@@ -161,7 +161,7 @@ export function jobAllowed(config, app, message) {
|
|
|
161
161
|
const home = app === 'slack' ? file?.channelId : file?.chatId;
|
|
162
162
|
if (!message.direct && home && home === message.conversation)
|
|
163
163
|
return { allowed: true };
|
|
164
|
-
return { allowed: false, message: `Jobs by direct message need approvers:
|
|
164
|
+
return { allowed: false, message: `Jobs by direct message need approvers: in LoopEngine, open this agent's Messaging tab, then ${APP_NAMES[app]} → Approvers, and add yourself.` };
|
|
165
165
|
}
|
|
166
166
|
/** Replaces the approver list on the agent's saved connection for `app`,
|
|
167
167
|
* primary or not. Throws NotifierFileError when there's no connection
|
|
@@ -224,6 +224,8 @@ export async function agentTelegramWebhookInfo(config) {
|
|
|
224
224
|
return {
|
|
225
225
|
...(info.url ? { url: info.url } : {}),
|
|
226
226
|
...(info.lastError ? { lastError: info.lastError } : {}),
|
|
227
|
+
...(info.lastErrorAt ? { lastErrorAt: info.lastErrorAt } : {}),
|
|
228
|
+
...(info.pending ? { pending: info.pending } : {}),
|
|
227
229
|
pointsHere: Boolean(info.url?.endsWith(`/hooks/telegram/${encodeURIComponent(config.name)}`)),
|
|
228
230
|
...(botUsername ? { botUsername } : {}),
|
|
229
231
|
};
|
package/dist/web/playground.js
CHANGED
|
@@ -1433,7 +1433,7 @@ ${consoleShellScript}
|
|
|
1433
1433
|
var counts = {};
|
|
1434
1434
|
if (cfg) {
|
|
1435
1435
|
chips.push({ text: typeof cfg.model === 'string' ? cfg.model : cfg.model.provider + ' \u00b7 ' + (cfg.model.model || 'default model'), mono: true });
|
|
1436
|
-
counts = { tools: cfg.tools.length, skills: cfg.skills.length, abilities: cfg.abilityCount
|
|
1436
|
+
counts = { tools: cfg.tools.length, skills: cfg.skills.length, abilities: cfg.abilityCount };
|
|
1437
1437
|
}
|
|
1438
1438
|
head.innerHTML = leShell.renderHead({ name: name, prompt: agent.systemPrompt, chips: chips, tab: 'chat', counts: counts });
|
|
1439
1439
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { createScheduledTask, pauseScheduledTask } from '../core/scheduler.js';
|
|
12
12
|
import { workspaceTimeZone } from '../core/time-zone.js';
|
|
13
13
|
import { STARTER_TEMPLATES, starterTemplate, templateScheduleStart } from '../core/system-agents/templates.js';
|
|
14
|
-
import { ActauthRuleExistsError, addActauthRule, setDefaultDecision } from './actauth-admin.js';
|
|
14
|
+
import { ActauthRuleExistsError, addActauthRule, applyPlannedRules, setDefaultDecision } from './actauth-admin.js';
|
|
15
15
|
import { setAgentEnvVar } from './env-admin.js';
|
|
16
16
|
import { freeAgentName } from './interview-admin.js';
|
|
17
17
|
import { writeSkill } from './skills-admin.js';
|
|
@@ -126,19 +126,9 @@ export async function createFromTemplate(id, deps) {
|
|
|
126
126
|
export async function createFromTemplateObject(template, deps) {
|
|
127
127
|
const { candidate, setup } = template;
|
|
128
128
|
const name = freeAgentName(candidate.agent.name, deps.agentExists);
|
|
129
|
-
// 1. The agent
|
|
130
|
-
// otherwise, and the template's rules come first.
|
|
129
|
+
// 1. The agent: everything asks unless a rule says otherwise.
|
|
131
130
|
await deps.scaffoldAgent(name, candidate.agent.systemPrompt);
|
|
132
131
|
setDefaultDecision(name, 'ask');
|
|
133
|
-
for (const rule of setup.rules) {
|
|
134
|
-
try {
|
|
135
|
-
addActauthRule(name, { name: rule.name, scope: '*/*', tool: rule.tool, decision: rule.decision });
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
if (!(err instanceof ActauthRuleExistsError))
|
|
139
|
-
throw err;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
132
|
// 2. Abilities: their tools, skills and rules. One failing doesn't stop
|
|
143
133
|
// the rest; it heads the checklist.
|
|
144
134
|
const installed = [];
|
|
@@ -185,6 +175,9 @@ export async function createFromTemplateObject(template, deps) {
|
|
|
185
175
|
failed.push({ name: app.title, error: installFailureReason(err) });
|
|
186
176
|
}
|
|
187
177
|
}
|
|
178
|
+
// The template's rules, now that abilities and apps have added theirs:
|
|
179
|
+
// none twice, a stricter one placed so it counts, none loosened.
|
|
180
|
+
applyPlannedRules(name, setup.rules.map((r) => ({ name: r.name, tool: r.tool, decision: r.decision })));
|
|
188
181
|
// 3. Its own skills and non-secret settings.
|
|
189
182
|
for (const skill of setup.skills ?? [])
|
|
190
183
|
writeSkill(name, skill.id, { description: skill.description, body: skill.body });
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
// one without changing anything that agent already does.
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { addGatewayTool, readGatewayToolsFromDir } from '../core/gateway-tools.js';
|
|
9
|
-
import {
|
|
9
|
+
import { applyPlannedRules } from './actauth-admin.js';
|
|
10
10
|
import { writeSkill } from './skills-admin.js';
|
|
11
11
|
import { createFromTemplateObject, installFailureReason, templateChecklist } from './templates-admin.js';
|
|
12
12
|
import { templateScheduleStart } from '../core/system-agents/templates.js';
|
|
13
|
-
import { planParameters, planWebhook, planWorkflowWithModel,
|
|
13
|
+
import { planParameters, planWebhook, planWorkflowWithModel, workflowTemplate } from '../core/system-agents/workflow-planner.js';
|
|
14
14
|
import { createTrigger, getTriggerStore, listTriggers, publicTrigger } from '../core/workflow-triggers.js';
|
|
15
15
|
import { mcpAbilityNames } from '../core/mcp-ability-generator.js';
|
|
16
16
|
import { clearWorkflowFields, migrateWorkflowsFile, readWorkflow, readWorkflows, setWorkflowFields, validateParams, workflowRunMessage } from '../core/workflows.js';
|
|
@@ -375,18 +375,7 @@ export async function createWorkflow(input, deps) {
|
|
|
375
375
|
if (!agent)
|
|
376
376
|
throw new WorkflowError(`There's no agent named ${target.agent ?? '(none)'}.`);
|
|
377
377
|
const check = checkWorkflowForAgent(plan, agent);
|
|
378
|
-
// 1.
|
|
379
|
-
const { rules: current } = readActauthConfig(agent.name);
|
|
380
|
-
for (const rule of plan.rules) {
|
|
381
|
-
if (current.some((r) => r.tool === rule.tool))
|
|
382
|
-
continue;
|
|
383
|
-
let name = slugify(`${plan.id}-${rule.tool}`, `${plan.id}-rule`);
|
|
384
|
-
for (let i = 2; current.some((r) => r.name === name); i++)
|
|
385
|
-
name = `${slugify(`${plan.id}-${rule.tool}`)}-${i}`;
|
|
386
|
-
addActauthRule(agent.name, { name, scope: '*/*', tool: rule.tool, decision: rule.decision });
|
|
387
|
-
current.push({ name, scope: '*/*', tool: rule.tool, decision: rule.decision });
|
|
388
|
-
}
|
|
389
|
-
// 2. Abilities it doesn't have yet.
|
|
378
|
+
// 1. Abilities it doesn't have yet (their rules come with them).
|
|
390
379
|
const installed = [];
|
|
391
380
|
const failed = [...mcp.failed];
|
|
392
381
|
for (const spec of check.toInstall) {
|
|
@@ -441,6 +430,10 @@ export async function createWorkflow(input, deps) {
|
|
|
441
430
|
const connected = copyConnectedTools(plan, agent.name, agents, deps.agentDir);
|
|
442
431
|
installed.push(...connected.copied);
|
|
443
432
|
failed.push(...connected.failed);
|
|
433
|
+
// 2. Rules, now that what was installed has added its own: none twice,
|
|
434
|
+
// a stricter one (ask where an ability allows) placed so it counts,
|
|
435
|
+
// and a decision the agent already has never loosened.
|
|
436
|
+
const ruled = applyPlannedRules(agent.name, plan.rules);
|
|
444
437
|
// 3. Its steps, as a skill; then load the agent again with all of it.
|
|
445
438
|
writeSkill(agent.name, plan.id, { description: plan.skill.description, body: plan.skill.body });
|
|
446
439
|
let loadError;
|
|
@@ -477,7 +470,7 @@ export async function createWorkflow(input, deps) {
|
|
|
477
470
|
newAgent: false,
|
|
478
471
|
workflow: plan.name,
|
|
479
472
|
abilities: { installed, failed },
|
|
480
|
-
keptDecisions:
|
|
473
|
+
keptDecisions: ruled.kept,
|
|
481
474
|
checklist: [...withMissingSteps(plan, agent.name, templateChecklist(template, agent.name, deps, failed, appsToConnect)), ...hook.checklist],
|
|
482
475
|
...(hook.webhook ? { webhook: hook.webhook } : {}),
|
|
483
476
|
...(hook.webhookError ? { webhookError: hook.webhookError } : {}),
|