loopengine 0.3.5 → 0.3.6

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.
@@ -49,6 +49,18 @@ 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-starts { margin: 10px 0 0; padding: 10px 12px; border: 1px solid var(--le-line, #e5e7eb); border-radius: 8px; background: var(--le-surface, #f6f7f9); }
53
+ .workflow-starts h4 { margin: 0; font-size: 13.5px; font-weight: 600; }
54
+ .wf-start { padding: 10px 0; border-top: 1px solid var(--le-line, #e5e7eb); display: grid; gap: 6px; font-size: 13px; }
55
+ .wf-start:first-of-type { border-top: 0; padding-top: 6px; }
56
+ .wf-start-head { display: flex; align-items: center; gap: 6px; }
57
+ .wf-start-head strong { font-size: 13px; }
58
+ .help-q { width: 18px; height: 18px; padding: 0; border-radius: 50%; font-size: 11px; line-height: 16px; font-weight: 600; text-align: center; cursor: pointer; }
59
+ .help-text { margin: 0; }
60
+ .help-text[hidden] { display: none; }
61
+ .wf-start .schedule-history { margin: 0; gap: 6px; }
62
+ .wf-start .workflow-params { margin: 0; }
63
+ .wf-start pre { margin: 0; padding: 8px 10px; border-radius: 6px; background: var(--le-bg, #fff); border: 1px solid var(--le-line, #e5e7eb); font-size: 12px; white-space: pre-wrap; overflow-wrap: anywhere; }
52
64
  .workflow-steps-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; margin: 6px 0 0; padding: 8px 10px; border-radius: 6px; background: var(--le-surface, #f6f7f9); }
53
65
  .workflow-params { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
54
66
  .workflow-params[hidden] { display: none; }
@@ -692,14 +704,22 @@ ${consoleShellScript}
692
704
  function workflowStartsSummary(w) {
693
705
  var parts = (w.schedules || []).filter(function (x) { return x.repeating && x.status !== 'paused'; }).map(function (x) { return x.describe; });
694
706
  if (w.webhooks) parts.push(w.webhooks === 1 ? 'when its webhook is called' : 'when one of its ' + w.webhooks + ' webhooks is called');
695
- if (!parts.length) return 'When asked';
707
+ if (!parts.length) return 'No schedule or webhook yet';
696
708
  var text = parts.join(', and ');
697
709
  return text.charAt(0).toUpperCase() + text.slice(1);
698
710
  }
699
711
 
700
- // How it starts: each schedule (pause, resume, remove), its webhooks,
701
- // and Run now or the API, which always work.
702
- function workflowStartsHtml(w) {
712
+ // How it starts: four ways, each with a ? that says what it is. A
713
+ // schedule (pause, resume, remove, add), a webhook, Run now with the
714
+ // values a run is given, and the API call an app makes.
715
+ function wfStartSection(key, title, help, body) {
716
+ return '<div class="wf-start">' +
717
+ '<div class="wf-start-head"><strong>' + title + '</strong>' +
718
+ '<button type="button" class="help-q" aria-expanded="false" aria-controls="help-' + key + '" aria-label="What is ' + title.toLowerCase() + '?">?</button></div>' +
719
+ '<p class="hint help-text" hidden>' + help + '</p>' + body + '</div>';
720
+ }
721
+
722
+ function workflowStartsHtml(w, agentName, paramsHtml) {
703
723
  var schedules = (w.schedules || []).map(function (x) {
704
724
  var state = x.waiting ? '<span class="le-pill ask">Waiting for a decision</span>'
705
725
  : x.status === 'paused' ? '<span class="le-pill">Paused</span>'
@@ -716,15 +736,28 @@ ${consoleShellScript}
716
736
  '<button type="button" class="delete-btn workflow-schedule-action" data-id="' + escapeHtml(x.taskId) + '" data-action="cancel">Remove</button>' +
717
737
  '</span></li>';
718
738
  }).join('');
719
- return '<div class="body-field workflow-starts" style="margin:6px 0 0">How it starts' +
720
- '<ul class="schedule-history" style="margin:4px 0 0">' + schedules +
721
- (w.webhooks ? '<li>' + (w.webhooks === 1 ? 'A webhook' : w.webhooks + ' webhooks') + ' <span class="hint">another app calls it when something happens</span></li>' : '') +
722
- '<li class="hint">Run now, below, or an app' + String.fromCharCode(8217) + 's API call, any time</li>' +
723
- '</ul>' +
724
- '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:6px">' +
725
- '<button type="button" class="workflow-add-schedule">Add a schedule</button>' +
726
- '<button type="button" class="workflow-hooks-btn">' + (w.webhooks ? 'Webhooks' : 'Add a webhook') + '</button>' +
727
- '</div></div>';
739
+ var paramNames = Object.keys(w.parameters || {});
740
+ var example = paramNames.length
741
+ ? '{ "params": { ' + paramNames.map(function (p) { return '"' + p + '": "' + String.fromCharCode(8230) + '"'; }).join(', ') + ' } }'
742
+ : '{}';
743
+ var apiUrl = location.origin + '/agents/' + encodeURIComponent(agentName) + '/workflows/' + encodeURIComponent(w.id) + '/run';
744
+ return '<div class="workflow-starts"><h4>How it starts</h4>' +
745
+ wfStartSection('schedule-' + w.id, 'Schedule',
746
+ 'Runs it on a timer, like every day at 9:00, with the values you give the schedule. Nobody needs to be there: approvals and questions go to the agent' + String.fromCharCode(8217) + 's messaging app.',
747
+ (schedules ? '<ul class="schedule-history">' + schedules + '</ul>' : '<span class="hint">None yet.</span>') +
748
+ '<div><button type="button" class="workflow-add-schedule">Add a schedule</button></div>') +
749
+ wfStartSection('webhook-' + w.id, 'Webhook',
750
+ 'A private web address another app (Stripe, GitHub, Shopify or your own) calls when something happens. Each call starts a run, with values taken from what the app sends.',
751
+ '<span' + (w.webhooks ? '' : ' class="hint"') + '>' + (w.webhooks ? (w.webhooks === 1 ? '1 webhook is on.' : w.webhooks + ' webhooks are on.') : 'None yet.') + '</span>' +
752
+ '<div><button type="button" class="workflow-hooks-btn">' + (w.webhooks ? 'Webhooks' : 'Add a webhook') + '</button></div>') +
753
+ wfStartSection('run-' + w.id, 'Run now',
754
+ 'Starts a run right away, from here' + (paramNames.length ? ', with the values you fill in' : '') + '. It shows under Recent runs as it goes.',
755
+ (paramsHtml ? '<div class="workflow-params">' + paramsHtml + '</div>' : '') +
756
+ '<div><button type="button" class="workflow-run primary">Run now</button></div>') +
757
+ wfStartSection('api-' + w.id, 'API',
758
+ 'Your own app starts a run by calling this address with an API key. Add "wait": true to get the result in the answer, or "callback_url" to have it posted to you when the run ends.',
759
+ '<pre>POST ' + escapeHtml(apiUrl) + String.fromCharCode(10) + escapeHtml(example) + '</pre>') +
760
+ '</div>';
728
761
  }
729
762
 
730
763
  function renderWorkflowsHtml(name, workflows) {
@@ -763,15 +796,15 @@ ${consoleShellScript}
763
796
  '<div style="display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;align-items:baseline">' +
764
797
  '<strong>' + escapeHtml(w.title) + '</strong><span>' + state + '</span></div>' +
765
798
  '<p class="hint" style="margin:4px 0 6px">' + escapeHtml(w.description) + (s ? ' ' + String.fromCharCode(183) + ' ' + escapeHtml(workflowStartsSummary(w)) : '') + '</p>' +
766
- '<details><summary class="hint">Instructions <span>(the ' + escapeHtml(w.id) + ' skill)</span></summary><div class="workflow-steps-text">' + escapeHtml(w.instructions) + '</div></details>' +
767
- workflowStartsHtml(w) +
768
- (params ? '<div class="workflow-params">' + params + '</div>' : '') +
769
- (runs ? '<details class="workflow-runs"' + ((w.recentRuns || []).some(function (r) { return r.status === 'running' || r.status === 'awaiting_approval'; }) ? ' open' : '') + '><summary class="hint">Recent runs (' + w.recentRuns.length + ')</summary><ul class="schedule-history">' + runs + '</ul></details>' : '') +
770
- '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">' +
771
- '<button type="button" class="workflow-run">Run now</button>' +
799
+ '<details open><summary class="hint">Instructions <span>(the ' + escapeHtml(w.id) + ' skill)</span></summary><div class="workflow-steps-text">' + escapeHtml(w.instructions) + '</div></details>' +
800
+ workflowStartsHtml(w, name, params) +
801
+ '<div class="hint workflow-result" style="margin-top:6px"></div><div class="workflow-hooks" hidden></div>' +
802
+ (runs ? '<details class="workflow-runs" style="margin-top:10px"' + ((w.recentRuns || []).some(function (r) { return r.status === 'running' || r.status === 'awaiting_approval'; }) ? ' open' : '') + '><summary class="hint">Recent runs (' + w.recentRuns.length + ')</summary><ul class="schedule-history">' + runs + '</ul></details>'
803
+ : '<p class="hint" style="margin:10px 0 0">No runs yet.</p>') +
804
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:10px">' +
772
805
  '<button type="button" class="workflow-edit">Edit</button>' +
773
- '<button type="button" class="delete-btn workflow-forget">Forget</button>' +
774
- '</div><div class="hint workflow-result"></div><div class="workflow-editor" hidden></div><div class="workflow-hooks" hidden></div></div>';
806
+ '<button type="button" class="delete-btn workflow-forget">Remove workflow</button>' +
807
+ '</div><div class="workflow-editor" hidden></div></div>';
775
808
  }).join('') + '<p class="hint" style="margin:6px 0 0"><a href="/workflows/new">Describe another workflow</a></p>';
776
809
  }
777
810
 
@@ -1101,6 +1134,16 @@ ${consoleShellScript}
1101
1134
  var result = card.querySelector('.workflow-result');
1102
1135
  var base = '/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(id);
1103
1136
  card.querySelector('.workflow-edit').addEventListener('click', function () { openWorkflowEditor(name, card); });
1137
+ // The ? beside each way it starts shows or hides what it is.
1138
+ var helps = card.querySelectorAll('.help-q');
1139
+ for (var hq = 0; hq < helps.length; hq++) {
1140
+ helps[hq].addEventListener('click', function (ev) {
1141
+ var btn = ev.currentTarget;
1142
+ var text = btn.closest('.wf-start').querySelector('.help-text');
1143
+ text.hidden = !text.hidden;
1144
+ btn.setAttribute('aria-expanded', String(!text.hidden));
1145
+ });
1146
+ }
1104
1147
  card.querySelector('.workflow-hooks-btn').addEventListener('click', function () { openWorkflowHooks(name, card); });
1105
1148
  // Add a schedule: the Schedules tab's form, with this workflow chosen.
1106
1149
  card.querySelector('.workflow-add-schedule').addEventListener('click', function () {
@@ -1137,7 +1180,7 @@ ${consoleShellScript}
1137
1180
  });
1138
1181
  card.querySelector('.workflow-forget').addEventListener('click', function (ev) {
1139
1182
  var btn = ev.currentTarget;
1140
- if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again (its schedules are cancelled; the skill stays)'; return; }
1183
+ if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again: its schedules and webhooks go; its instructions stay as a skill for Chat'; return; }
1141
1184
  fetchJsonRetryOnce(base, { method: 'DELETE' }).then(function () { loadWorkflowsSection(name); });
1142
1185
  });
1143
1186
  }
@@ -2327,7 +2370,7 @@ ${consoleShellScript}
2327
2370
  var list = tasks.length
2328
2371
  ? '<div class="le-table-wrap"><table><thead><tr><th>Task</th><th>When</th><th>Status</th><th>Last run</th><th></th></tr></thead><tbody>' + tasks.map(renderScheduleRow).join('') + '</tbody></table></div>'
2329
2372
  : '<p class="hint">Nothing scheduled yet.</p>';
2330
- return '<p class="muted" style="margin:0 0 20px">Workflows and messages this agent runs on a timer, like a cron job. A workflow schedule starts with the workflow&#39;s start message as it is at each run. Each run works like an app calling it over HTTP: an approval goes wherever the Messaging tab sends it, and with nowhere set, a call that needs approval is denied. The agent can also schedule its own tasks when asked in Chat (&ldquo;remind me tomorrow at 9&rdquo;); they appear here too.</p>' +
2373
+ return '<p class="muted" style="margin:0 0 20px">Workflows and messages this agent runs on a timer, like a cron job. A workflow schedule runs the workflow&#39;s instructions as they are at each run. Each run works like an app calling it over HTTP: an approval goes wherever the Messaging tab sends it, and with nowhere set, a call that needs approval is denied. The agent can also schedule its own tasks when asked in Chat (&ldquo;remind me tomorrow at 9&rdquo;); they appear here too.</p>' +
2331
2374
  '<section><h3>Scheduled (' + active.length + ' active)</h3>' + list + '</section>' +
2332
2375
  '<section><h3>New schedule</h3>' +
2333
2376
  '<form class="add-source" id="scheduleForm" style="max-width:560px">' +
@@ -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'], ['chat', 'Chat'], ['abilities', 'Apps'], ['actauth', 'Permissions'],
60
- ['messaging', 'Messaging'], ['schedules', 'Schedules'], ['env', 'Environment'], ['advanced', 'Advanced']
59
+ ['overview', 'Overview'], ['workflows', 'Workflows'], ['abilities', 'Apps'], ['actauth', 'Permissions'],
60
+ ['messaging', 'Messaging'], ['schedules', 'Schedules'], ['env', 'Environment'], ['advanced', 'Advanced'], ['chat', 'Chat']
61
61
  ];
62
62
 
63
63
  function esc(s) {
@@ -72,11 +72,13 @@ export const consoleShellScript = `<script>
72
72
  return '/agents/config?agent=' + enc(name) + (tab && tab !== 'overview' ? '&tab=' + enc(tab) : '');
73
73
  }
74
74
 
75
- // Switching agents keeps you where you are: on a config tab you land on
76
- // the same tab for the other agent, anywhere else you land in its Chat.
75
+ // An agent in the list opens its Overview. Switching agents keeps you
76
+ // where you are: on a config tab you land on the same tab for the other
77
+ // agent, and in Chat on its Chat.
77
78
  function agentHref(name) {
78
79
  if (page === 'agent-config') return tabHref(name, params.get('tab') || 'overview');
79
- return tabHref(name, 'chat');
80
+ if (page === 'chat') return tabHref(name, 'chat');
81
+ return tabHref(name, 'overview');
80
82
  }
81
83
 
82
84
  function renderList() {
@@ -207,10 +207,13 @@ export declare function prepareWorkflowRun(agentName: string, agentDir: string,
207
207
  * matter is kept. A body with `start_message` (and no `instructions`) is
208
208
  * still taken, as before. */
209
209
  export declare function updateWorkflow(agentDir: string, id: string, input: Record<string, unknown>): Workflow;
210
- /** Forgets a workflow: its skill stays, as a plain skill, and the
211
- * schedules that ran it are cancelled. Returns how many were. */
210
+ /** Removes a workflow: its skill stays, as a plain skill the agent can
211
+ * still use in chat, and what started it goes: its schedules are
212
+ * cancelled and its webhooks deleted (their URLs stop working). Returns
213
+ * how many of each. */
212
214
  export declare function forgetWorkflow(agentName: string, agentDir: string, id: string): Promise<{
213
215
  cancelled: number;
216
+ webhooks: number;
214
217
  }>;
215
218
  export declare function agentDirIn(projectDir: string, name: string): string;
216
219
  export {};
@@ -11,7 +11,7 @@ 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
13
  import { planParameters, planWebhook, planWorkflowWithModel, slugify, workflowTemplate } from '../core/system-agents/workflow-planner.js';
14
- import { createTrigger, listTriggers, publicTrigger } from '../core/workflow-triggers.js';
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';
17
17
  import { isUnderWay, listRuns } from '../core/workflow-runs.js';
@@ -634,8 +634,10 @@ export function updateWorkflow(agentDir, id, input) {
634
634
  setWorkflowFields(agentDir, id, instructions !== undefined ? { ...(title ? { title } : {}), instructions, parameters } : { ...(title ? { title } : {}), startMessage, ...(trigger ? { trigger } : {}), parameters });
635
635
  return readWorkflow(agentDir, id);
636
636
  }
637
- /** Forgets a workflow: its skill stays, as a plain skill, and the
638
- * schedules that ran it are cancelled. Returns how many were. */
637
+ /** Removes a workflow: its skill stays, as a plain skill the agent can
638
+ * still use in chat, and what started it goes: its schedules are
639
+ * cancelled and its webhooks deleted (their URLs stop working). Returns
640
+ * how many of each. */
639
641
  export async function forgetWorkflow(agentName, agentDir, id) {
640
642
  clearWorkflowFields(agentDir, id);
641
643
  let cancelled = 0;
@@ -643,7 +645,12 @@ export async function forgetWorkflow(agentName, agentDir, id) {
643
645
  await cancelScheduledTask(task.task_id);
644
646
  cancelled++;
645
647
  }
646
- return { cancelled };
648
+ let webhooks = 0;
649
+ for (const trigger of await listTriggers(agentName, id)) {
650
+ await getTriggerStore().remove(trigger.trigger_id);
651
+ webhooks++;
652
+ }
653
+ return { cancelled, webhooks };
647
654
  }
648
655
  export function agentDirIn(projectDir, name) {
649
656
  return join(projectDir, 'agents', name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",