loopengine 0.3.7 → 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 +6 -2
- package/dist/core/workflows.d.ts +3 -0
- package/dist/core/workflows.js +4 -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 +94 -29
- 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 +13 -16
- 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
|
});
|
|
@@ -131,7 +131,10 @@ export function validateWorkflowPlan(raw) {
|
|
|
131
131
|
if (!raw || typeof raw !== 'object')
|
|
132
132
|
return undefined;
|
|
133
133
|
const r = raw;
|
|
134
|
-
|
|
134
|
+
// A name for people: one written as an id ("daily-hn-digest") becomes
|
|
135
|
+
// words ("Daily hn digest").
|
|
136
|
+
const rawName = str(r.name, 80);
|
|
137
|
+
const name = rawName && /^[a-z0-9]+(-[a-z0-9]+)+$/.test(rawName) ? rawName.charAt(0).toUpperCase() + rawName.slice(1).replace(/-/g, ' ') : rawName;
|
|
135
138
|
const summary = str(r.summary, 600);
|
|
136
139
|
const skillRaw = (r.skill ?? {});
|
|
137
140
|
const skillBody = str(skillRaw.body, 12000);
|
|
@@ -408,7 +411,8 @@ export function workflowTemplate(plan) {
|
|
|
408
411
|
...(plan.apps?.length ? { apps: plan.apps } : {}),
|
|
409
412
|
keys: plan.keys,
|
|
410
413
|
skills: [{ id: plan.id, description: plan.skill.description, body: plan.skill.body }],
|
|
411
|
-
|
|
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 })),
|
|
412
416
|
...(plan.trigger.kind === 'schedule' && plan.trigger.every
|
|
413
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 } : {}) } }
|
|
414
418
|
: {}),
|
package/dist/core/workflows.d.ts
CHANGED
|
@@ -26,6 +26,9 @@ export interface Workflow {
|
|
|
26
26
|
* `startMessage` (from before) is kept for workflows saved that way. */
|
|
27
27
|
export interface WorkflowFields {
|
|
28
28
|
title?: string;
|
|
29
|
+
/** The skill's one-line description: what the job is, which chat
|
|
30
|
+
* matches requests against. Kept as it is when not given. */
|
|
31
|
+
description?: string;
|
|
29
32
|
instructions?: string;
|
|
30
33
|
startMessage?: string;
|
|
31
34
|
trigger?: string;
|
package/dist/core/workflows.js
CHANGED
|
@@ -97,7 +97,8 @@ function workflowFrom(id, frontmatter, body) {
|
|
|
97
97
|
return undefined;
|
|
98
98
|
return {
|
|
99
99
|
id,
|
|
100
|
-
|
|
100
|
+
// A title that's just the id (a plan once saved it that way) reads as words.
|
|
101
|
+
title: typeof frontmatter.title === 'string' && frontmatter.title.trim() && frontmatter.title.trim() !== id ? frontmatter.title.trim() : titleFromId(id),
|
|
101
102
|
description: typeof frontmatter.description === 'string' ? frontmatter.description : '',
|
|
102
103
|
startMessage,
|
|
103
104
|
trigger: typeof frontmatter.trigger === 'string' && frontmatter.trigger.trim() ? frontmatter.trigger.trim() : 'When asked',
|
|
@@ -141,6 +142,8 @@ export function setWorkflowFields(agentDir, id, fields) {
|
|
|
141
142
|
const frontmatter = { ...skill.frontmatter };
|
|
142
143
|
for (const key of WORKFLOW_KEYS)
|
|
143
144
|
delete frontmatter[key];
|
|
145
|
+
if (fields.description)
|
|
146
|
+
frontmatter.description = fields.description;
|
|
144
147
|
const parameters = fields.parameters && Object.keys(fields.parameters).length ? { parameters: fields.parameters } : {};
|
|
145
148
|
if (fields.instructions !== undefined) {
|
|
146
149
|
writeSkillFile(path, { ...frontmatter, workflow: true, ...(fields.title ? { title: fields.title } : {}), ...parameters }, fields.instructions);
|
|
@@ -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. */
|
|
@@ -49,6 +49,7 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
49
49
|
form.add-source .schedule-when input[type="number"] { width: 80px; }
|
|
50
50
|
form.add-source .schedule-when select, form.add-source .schedule-when input[type="datetime-local"] { width: auto; }
|
|
51
51
|
.workflow-card { margin-bottom: 10px; }
|
|
52
|
+
.workflow-card.workflow-focus { box-shadow: 0 0 0 2px var(--le-accent, #0f766e); transition: box-shadow 0.3s; }
|
|
52
53
|
tr.schedule-focus td { background: var(--le-accent-soft, #e6f4ef); transition: background 0.3s; }
|
|
53
54
|
.workflow-starts { margin: 10px 0 0; padding: 10px 12px; border: 1px solid var(--le-line, #e5e7eb); border-radius: 8px; background: var(--le-surface, #f6f7f9); }
|
|
54
55
|
.workflow-starts h4 { margin: 0; font-size: 13.5px; font-weight: 600; }
|
|
@@ -437,10 +438,6 @@ ${consoleShellScript}
|
|
|
437
438
|
return attempt().catch(function () { return attempt(); });
|
|
438
439
|
}
|
|
439
440
|
|
|
440
|
-
function badge(label) {
|
|
441
|
-
return '<span class="badge ' + (label === 'custom' ? 'badge-custom' : 'badge-default') + '">' + escapeHtml(label) + '</span>';
|
|
442
|
-
}
|
|
443
|
-
|
|
444
441
|
// A small, deliberately non-CommonMark markdown-to-HTML renderer for
|
|
445
442
|
// the skill Body field's Preview toggle (see wireSkillsHandlers) — this
|
|
446
443
|
// page stays a self-contained string, no build step, no CDN (same
|
|
@@ -837,6 +834,35 @@ ${consoleShellScript}
|
|
|
837
834
|
var workflowsRefreshTimer = null;
|
|
838
835
|
// Set by a workflow's "Add a schedule", for the Schedules tab's form.
|
|
839
836
|
var pendingScheduleWorkflow = null;
|
|
837
|
+
// Set by a schedule's workflow link: the card to scroll to and mark.
|
|
838
|
+
var pendingWorkflowFocus = null;
|
|
839
|
+
|
|
840
|
+
function focusWorkflowCard(tries) {
|
|
841
|
+
if (!pendingWorkflowFocus) return;
|
|
842
|
+
var card = detail.querySelector('.workflow-card[data-workflow="' + CSS.escape(pendingWorkflowFocus) + '"]');
|
|
843
|
+
if (!card) {
|
|
844
|
+
if ((tries || 0) < 40) setTimeout(function () { focusWorkflowCard((tries || 0) + 1); }, 150);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
pendingWorkflowFocus = null;
|
|
848
|
+
card.scrollIntoView({ block: 'start' });
|
|
849
|
+
card.classList.add('workflow-focus');
|
|
850
|
+
setTimeout(function () { card.classList.remove('workflow-focus'); }, 2500);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Set by a workflow's Edit on the Skills list: the workflow to open in
|
|
854
|
+
// its editor once the Workflows tab has drawn it.
|
|
855
|
+
var pendingWorkflowEdit = null;
|
|
856
|
+
|
|
857
|
+
function openPendingWorkflowEdit() {
|
|
858
|
+
if (!pendingWorkflowEdit) return;
|
|
859
|
+
var card = detail.querySelector('.workflow-card[data-workflow="' + CSS.escape(pendingWorkflowEdit) + '"]');
|
|
860
|
+
if (!card) return;
|
|
861
|
+
pendingWorkflowEdit = null;
|
|
862
|
+
openWorkflowEditor(currentName, card);
|
|
863
|
+
card.scrollIntoView({ block: 'start' });
|
|
864
|
+
}
|
|
865
|
+
|
|
840
866
|
// Set by a schedule on a workflow card (or a #schedule-<id> link): the
|
|
841
867
|
// Schedules tab's row to show.
|
|
842
868
|
var pendingScheduleFocus = /^#schedule-/.test(location.hash) ? location.hash.slice('#schedule-'.length) : null;
|
|
@@ -869,11 +895,13 @@ ${consoleShellScript}
|
|
|
869
895
|
if (!r.ok) { el.innerHTML = '<p class="error">Could not load workflows: ' + escapeHtml(r.body.error || 'unknown error') + '</p>'; return; }
|
|
870
896
|
var workflows = r.body.workflows || [];
|
|
871
897
|
fillWorkflowsCard(workflows);
|
|
898
|
+
leShell.setCount('workflows', workflows.length);
|
|
872
899
|
workflowsById = {};
|
|
873
900
|
workflows.forEach(function (w) { workflowsById[w.id] = w; });
|
|
874
901
|
el.innerHTML = renderWorkflowsHtml(name, workflows);
|
|
875
902
|
var cards = el.querySelectorAll('.workflow-card');
|
|
876
903
|
for (var i = 0; i < cards.length; i++) wireWorkflowCard(name, cards[i]);
|
|
904
|
+
openPendingWorkflowEdit();
|
|
877
905
|
// Keep a running run's status current while the Overview is open.
|
|
878
906
|
var running = workflows.some(function (w) { return (w.recentRuns || []).some(function (x) { return x.status === 'running'; }); });
|
|
879
907
|
if (running && currentTab === 'workflows') workflowsRefreshTimer = setTimeout(function () { if (!workflowEditing) loadWorkflowsSection(name); }, 3000);
|
|
@@ -893,7 +921,10 @@ ${consoleShellScript}
|
|
|
893
921
|
function workflowEditorHtml(w) {
|
|
894
922
|
var rows = Object.keys(w.parameters || {}).map(function (p) { return workflowParamRowHtml(p, w.parameters[p]); }).join('');
|
|
895
923
|
return '<form class="add-source workflow-editor-form" style="margin-top:10px">' +
|
|
896
|
-
'<label>Title<input name="title" maxlength="120" value="' + escapeHtml(w.title) + '"
|
|
924
|
+
'<label>Title<input name="title" maxlength="120" value="' + escapeHtml(w.title) + '">' +
|
|
925
|
+
'<span class="hint">ID: <code>' + escapeHtml(w.id) + '</code>, used by the API, schedules and webhooks; it doesn' + String.fromCharCode(8217) + 't change when you edit.</span></label>' +
|
|
926
|
+
'<label>Description <span class="hint">(one line on what this job is; the agent uses it to recognise the job when you ask in Chat)</span>' +
|
|
927
|
+
'<input name="description" maxlength="500" required value="' + escapeHtml(w.description) + '"></label>' +
|
|
897
928
|
'<label>Instructions <span class="hint">(what the agent does, step by step; $name puts in a parameter's value. Chat uses them too.)</span>' +
|
|
898
929
|
'<textarea name="instructions" maxlength="12000" rows="12" required>' + escapeHtml(w.instructions) + '</textarea></label>' +
|
|
899
930
|
'<div class="body-field">Parameters <span class="hint">(values a run is given; apps send them as params)</span>' +
|
|
@@ -960,7 +991,7 @@ ${consoleShellScript}
|
|
|
960
991
|
result.textContent = 'Saving' + String.fromCharCode(8230);
|
|
961
992
|
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(w.id), {
|
|
962
993
|
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
963
|
-
body: JSON.stringify({ title: form.title.value, instructions: form.instructions.value, parameters: parameters }),
|
|
994
|
+
body: JSON.stringify({ title: form.title.value, description: form.description.value, instructions: form.instructions.value, parameters: parameters }),
|
|
964
995
|
}).then(function (r) {
|
|
965
996
|
if (!r.ok) { result.textContent = r.body.error || 'Could not save'; return; }
|
|
966
997
|
workflowEditing = false;
|
|
@@ -1190,7 +1221,11 @@ ${consoleShellScript}
|
|
|
1190
1221
|
var action = btn.dataset.action;
|
|
1191
1222
|
if (action === 'cancel' && btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again to remove'; return; }
|
|
1192
1223
|
btn.disabled = true;
|
|
1193
|
-
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
|
+
});
|
|
1194
1229
|
});
|
|
1195
1230
|
}
|
|
1196
1231
|
card.querySelector('.workflow-run').addEventListener('click', function (ev) {
|
|
@@ -1243,6 +1278,7 @@ ${consoleShellScript}
|
|
|
1243
1278
|
var waiting = active.filter(function (t) { return t.status === 'awaiting_approval'; }).length;
|
|
1244
1279
|
var next = active.map(function (t) { return t.next_run_at; }).filter(Boolean).sort()[0];
|
|
1245
1280
|
set('ovSchedulesValue', String(active.length));
|
|
1281
|
+
leShell.setCount('schedules', active.length);
|
|
1246
1282
|
var schedulesDetail = [];
|
|
1247
1283
|
if (waiting) schedulesDetail.push(waiting + ' waiting for approval');
|
|
1248
1284
|
if (next) schedulesDetail.push('Next run ' + escapeHtml(new Date(next).toLocaleString()));
|
|
@@ -1276,7 +1312,10 @@ ${consoleShellScript}
|
|
|
1276
1312
|
'<dt>Provider</dt><dd>' + escapeHtml(cfg.model.provider) + '</dd>' +
|
|
1277
1313
|
'<dt>Model</dt><dd>' + escapeHtml(cfg.model.model || '(provider default)') + '</dd>' +
|
|
1278
1314
|
'<dt>Max tokens</dt><dd>' + escapeHtml(cfg.model.maxTokens != null ? cfg.model.maxTokens : '(default)') + '</dd>' +
|
|
1279
|
-
|
|
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>' +
|
|
1280
1319
|
'</dl>' +
|
|
1281
1320
|
'<form class="add-source" id="modelForm" style="display:none">' +
|
|
1282
1321
|
'<label>Provider' +
|
|
@@ -1292,7 +1331,7 @@ ${consoleShellScript}
|
|
|
1292
1331
|
'<label>Max tokens <span class="hint">(blank for provider default)</span>' +
|
|
1293
1332
|
'<input type="number" name="maxTokens" min="1" step="1" value="' + (cfg.model.maxTokens != null ? escapeHtml(cfg.model.maxTokens) : '') + '">' +
|
|
1294
1333
|
'</label>' +
|
|
1295
|
-
'<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>' +
|
|
1296
1335
|
'<select name="reasoningEffort">' +
|
|
1297
1336
|
['', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'].map(function (v) {
|
|
1298
1337
|
return '<option value="' + v + '"' + (v === (cfg.model.reasoningEffort || '') ? ' selected' : '') + '>' + (v || '(unset)') + '</option>';
|
|
@@ -1308,11 +1347,6 @@ ${consoleShellScript}
|
|
|
1308
1347
|
return renderOverviewCards(cfg) +
|
|
1309
1348
|
'<section>' + systemPromptSection + '</section>' +
|
|
1310
1349
|
'<section>' + modelSection + '</section>' +
|
|
1311
|
-
'<section><h3>Hooks</h3><dl class="kv">' +
|
|
1312
|
-
'<dt>sessionIdFor</dt><dd>' + badge(cfg.sessionIdFor.indexOf('custom') === 0 ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.sessionIdFor) + '</span></dd>' +
|
|
1313
|
-
'<dt>tenantFor</dt><dd>' + badge(cfg.tenantFor.indexOf('custom') === 0 ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.tenantFor) + '</span></dd>' +
|
|
1314
|
-
'<dt>isSafeTool</dt><dd>' + badge(cfg.isSafeTool === 'custom' ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.isSafeTool) + '</span></dd>' +
|
|
1315
|
-
'</dl></section>' +
|
|
1316
1350
|
'<section><h3>Limits & budgets <button type="button" class="edit-btn" id="editLimitsBtn">Edit</button></h3>' +
|
|
1317
1351
|
'<dl class="kv" id="limitsDisplay">' +
|
|
1318
1352
|
'<dt>maxTurns</dt><dd>' + escapeHtml(cfg.maxTurns) + '</dd>' +
|
|
@@ -1333,7 +1367,6 @@ ${consoleShellScript}
|
|
|
1333
1367
|
'<button type="button" id="cancelLimitsBtn">Cancel</button>' +
|
|
1334
1368
|
'<div class="error" id="limitsError"></div>' +
|
|
1335
1369
|
'</form>' +
|
|
1336
|
-
'<dl class="kv"><dt>skillsDirs</dt><dd>' + escapeHtml(cfg.skillsDirs.join(', ') || '(none)') + '</dd></dl>' +
|
|
1337
1370
|
'</section>';
|
|
1338
1371
|
}
|
|
1339
1372
|
|
|
@@ -1922,7 +1955,10 @@ ${consoleShellScript}
|
|
|
1922
1955
|
(saved ? '<button type="button" class="delete-btn notifier-remove"' + (locked ? ' disabled' : '') + '>Remove</button>' : '') + '</div>' +
|
|
1923
1956
|
(isWebhook ? '<p class="hint" style="margin:0">A webhook always sends once saved, alongside the primary messaging app.</p>' : '') +
|
|
1924
1957
|
'<div class="hint notifier-result"></div>' +
|
|
1925
|
-
'</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>';
|
|
1926
1962
|
}
|
|
1927
1963
|
|
|
1928
1964
|
function renderConnectionsHtml(n) {
|
|
@@ -1977,7 +2013,7 @@ ${consoleShellScript}
|
|
|
1977
2013
|
'<td><button type="button" class="delete-btn approver-remove" data-user="' + escapeHtml(a.user) + '"' + (n.canSave === false ? ' disabled' : '') + '>Remove</button></td></tr>';
|
|
1978
2014
|
}).join('');
|
|
1979
2015
|
var where = app === 'telegram' ? 'by replying to it' : 'by replying in its thread';
|
|
1980
|
-
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>' +
|
|
1981
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>' : '') +
|
|
1982
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>' +
|
|
1983
2019
|
(approvers.length
|
|
@@ -2067,8 +2103,7 @@ ${consoleShellScript}
|
|
|
2067
2103
|
'<p class="muted" style="margin:0 0 12px">' + renderNotifierStatus(n) + '</p>' +
|
|
2068
2104
|
(n.error ? '<p class="error">' + escapeHtml(n.error) + '</p>' : '') +
|
|
2069
2105
|
(n.source === 'code' ? '' : renderConnectionsHtml(n)) + '</section>' +
|
|
2070
|
-
|
|
2071
|
-
renderApproversHtml(n, 'telegram') +
|
|
2106
|
+
|
|
2072
2107
|
'<section><h3>Recent deliveries</h3>' + renderDeliveries(n.deliveries || []) + '</section>';
|
|
2073
2108
|
}
|
|
2074
2109
|
|
|
@@ -2217,8 +2252,16 @@ ${consoleShellScript}
|
|
|
2217
2252
|
fetchJsonRetryOnce(url).then(function (r) {
|
|
2218
2253
|
if (!r.ok) { out.textContent = r.body.error || 'Could not check'; return; }
|
|
2219
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
|
+
}
|
|
2220
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.') +
|
|
2221
|
-
(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.' : '') : '');
|
|
2222
2265
|
});
|
|
2223
2266
|
});
|
|
2224
2267
|
}
|
|
@@ -2381,7 +2424,8 @@ ${consoleShellScript}
|
|
|
2381
2424
|
return '<tr id="schedule-' + escapeHtml(t.task_id) + '">' +
|
|
2382
2425
|
'<td style="min-width:180px"><strong>' + escapeHtml(t.task_name || 'Untitled') + '</strong>' +
|
|
2383
2426
|
(t.workflow
|
|
2384
|
-
? '<div><span class="le-pill accent">Workflow</span> ' + escapeHtml(t.workflow.
|
|
2427
|
+
? '<div><span class="le-pill accent">Workflow</span> <a href="?agent=' + encodeURIComponent(currentName) + '&tab=workflows" class="schedule-workflow-link" data-workflow="' + escapeHtml(t.workflow.id) + '" title="See it on the Workflows tab">' + escapeHtml(t.workflow.title) + '</a></div>' +
|
|
2428
|
+
'<div class="schedule-result hint">' + escapeHtml(t.workflow.description || t.workflow.start_message) +
|
|
2385
2429
|
(t.params ? '<br>' + Object.keys(t.params).map(function (k) { return escapeHtml(k) + ': ' + escapeHtml(t.params[k]); }).join(' ' + String.fromCharCode(183) + ' ') : '') + '</div>'
|
|
2386
2430
|
: t.workflow_missing
|
|
2387
2431
|
? '<div><span class="le-pill bad">Workflow removed</span> <span class="hint">' + escapeHtml(t.workflow_id) + '</span></div>'
|
|
@@ -2455,6 +2499,17 @@ ${consoleShellScript}
|
|
|
2455
2499
|
function wireSchedulesHandlers(name) {
|
|
2456
2500
|
var content = schedulesContentEl();
|
|
2457
2501
|
if (!content) return;
|
|
2502
|
+
// A schedule's workflow opens its card on the Workflows tab.
|
|
2503
|
+
var workflowLinks = content.querySelectorAll('.schedule-workflow-link');
|
|
2504
|
+
for (var wl = 0; wl < workflowLinks.length; wl++) {
|
|
2505
|
+
workflowLinks[wl].addEventListener('click', function (ev) {
|
|
2506
|
+
if (ev.metaKey || ev.ctrlKey || ev.shiftKey) return;
|
|
2507
|
+
ev.preventDefault();
|
|
2508
|
+
pendingWorkflowFocus = ev.currentTarget.dataset.workflow;
|
|
2509
|
+
switchTab('workflows');
|
|
2510
|
+
focusWorkflowCard();
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2458
2513
|
var actionBtns = content.querySelectorAll('.schedule-action');
|
|
2459
2514
|
for (var i = 0; i < actionBtns.length; i++) {
|
|
2460
2515
|
actionBtns[i].addEventListener('click', function (ev) {
|
|
@@ -3544,12 +3599,17 @@ ${consoleShellScript}
|
|
|
3544
3599
|
// needs a further per-skill GET, to pull the body that index doesn't
|
|
3545
3600
|
// carry. ----
|
|
3546
3601
|
|
|
3602
|
+
// A skill that's a workflow is labelled, and its Edit opens the workflow
|
|
3603
|
+
// editor: its instructions are one text there, with its title,
|
|
3604
|
+
// description and parameters.
|
|
3547
3605
|
function renderSkillCard(s) {
|
|
3548
3606
|
return '<div class="source">' +
|
|
3549
3607
|
'<div class="source-head">' +
|
|
3550
|
-
'<h4>' + escapeHtml(s.name) + '</h4>' +
|
|
3608
|
+
'<h4>' + escapeHtml(s.name) + (s.workflow ? ' <span class="le-pill accent">Workflow</span>' : '') + '</h4>' +
|
|
3551
3609
|
'<span>' +
|
|
3552
|
-
|
|
3610
|
+
(s.workflow
|
|
3611
|
+
? '<button type="button" class="edit-workflow-skill-btn" data-id="' + escapeHtml(s.name) + '" title="Edit it as a workflow, on the Workflows tab">Edit</button> '
|
|
3612
|
+
: '<button type="button" class="edit-skill-btn" data-id="' + escapeHtml(s.name) + '">Edit</button> ') +
|
|
3553
3613
|
'<button type="button" class="delete-btn delete-skill-btn" data-id="' + escapeHtml(s.name) + '" title="Delete this skill" aria-label="Delete this skill">Delete</button>' +
|
|
3554
3614
|
'</span>' +
|
|
3555
3615
|
'</div>' +
|
|
@@ -3559,11 +3619,7 @@ ${consoleShellScript}
|
|
|
3559
3619
|
|
|
3560
3620
|
function renderSkillsTabHtml(cfg) {
|
|
3561
3621
|
var listHtml = cfg.skills.length ? cfg.skills.map(renderSkillCard).join('') : '<p class="hint">No skills yet.</p>';
|
|
3562
|
-
return '<section><h3>Skills
|
|
3563
|
-
'<dt>skillsDirs</dt><dd>' + escapeHtml(cfg.skillsDirs.join(', ') || '(none)') + '</dd>' +
|
|
3564
|
-
'<dt>skillIndexBudgetTokens</dt><dd>' + escapeHtml(cfg.skillIndexBudgetTokens) + '</dd>' +
|
|
3565
|
-
'</dl></section>' +
|
|
3566
|
-
'<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>' +
|
|
3567
3623
|
'<section><h3 id="skillFormHeading">Add a skill</h3>' +
|
|
3568
3624
|
'<form class="add-source" id="skillForm">' +
|
|
3569
3625
|
'<label>Id <span class="hint">(lowercase, hyphen-separated — becomes the SKILL.md folder name; nested skills aren\\'t editable here)</span>' +
|
|
@@ -3657,6 +3713,15 @@ ${consoleShellScript}
|
|
|
3657
3713
|
});
|
|
3658
3714
|
}
|
|
3659
3715
|
|
|
3716
|
+
var workflowEditButtons = panel.querySelectorAll('.edit-workflow-skill-btn');
|
|
3717
|
+
for (var we = 0; we < workflowEditButtons.length; we++) {
|
|
3718
|
+
workflowEditButtons[we].addEventListener('click', function (ev) {
|
|
3719
|
+
pendingWorkflowEdit = ev.currentTarget.getAttribute('data-id');
|
|
3720
|
+
switchTab('workflows');
|
|
3721
|
+
openPendingWorkflowEdit();
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3660
3725
|
var editButtons = panel.querySelectorAll('.edit-skill-btn');
|
|
3661
3726
|
for (var i = 0; i < editButtons.length; i++) {
|
|
3662
3727
|
editButtons[i].addEventListener('click', function (ev) {
|
|
@@ -4441,7 +4506,7 @@ ${consoleShellScript}
|
|
|
4441
4506
|
prompt: cfg.systemPrompt,
|
|
4442
4507
|
chips: [modelChip(cfg), { text: 'unmatched calls: ' + cfg.permissions.defaultDecision }],
|
|
4443
4508
|
tab: currentTab,
|
|
4444
|
-
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 },
|
|
4445
4510
|
}) +
|
|
4446
4511
|
'<div class="le-scroll"><div class="le-page" style="display:block">' +
|
|
4447
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 } : {}),
|
|
@@ -614,6 +607,9 @@ export function updateWorkflow(agentDir, id, input) {
|
|
|
614
607
|
return trimmed;
|
|
615
608
|
};
|
|
616
609
|
const title = text(input.title, 'The title', 120);
|
|
610
|
+
const description = input.description === undefined ? undefined : text(input.description, 'The description', 500);
|
|
611
|
+
if (description !== undefined && !description)
|
|
612
|
+
throw new WorkflowError('Write a one-line description: what this job is, e.g. "Daily digest of AI-agent stories on Hacker News". The agent uses it to recognise the job in Chat.');
|
|
617
613
|
const instructions = input.instructions === undefined ? undefined : text(input.instructions, 'The instructions', 12000);
|
|
618
614
|
if (instructions !== undefined && !instructions)
|
|
619
615
|
throw new WorkflowError('Write the instructions: what the agent does, e.g. "Check the refund request for order $order_number: look up the order, then…"');
|
|
@@ -631,7 +627,8 @@ export function updateWorkflow(agentDir, id, input) {
|
|
|
631
627
|
const s = spec && typeof spec === 'object' ? spec : {};
|
|
632
628
|
parameters[name] = { description: text(s.description, `The description of "${name}"`, 300), required: s.required === true };
|
|
633
629
|
}
|
|
634
|
-
|
|
630
|
+
const named = { ...(title ? { title } : {}), ...(description ? { description: description.replace(/\s+/g, ' ') } : {}) };
|
|
631
|
+
setWorkflowFields(agentDir, id, instructions !== undefined ? { ...named, instructions, parameters } : { ...named, startMessage, ...(trigger ? { trigger } : {}), parameters });
|
|
635
632
|
return readWorkflow(agentDir, id);
|
|
636
633
|
}
|
|
637
634
|
/** Removes a workflow: its skill stays, as a plain skill the agent can
|