loopengine 0.3.7 → 0.3.8

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.
@@ -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
- const name = str(r.name, 80);
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);
@@ -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;
@@ -97,7 +97,8 @@ function workflowFrom(id, frontmatter, body) {
97
97
  return undefined;
98
98
  return {
99
99
  id,
100
- title: typeof frontmatter.title === 'string' && frontmatter.title.trim() ? frontmatter.title.trim() : titleFromId(id),
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);
@@ -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; }
@@ -837,6 +838,35 @@ ${consoleShellScript}
837
838
  var workflowsRefreshTimer = null;
838
839
  // Set by a workflow's "Add a schedule", for the Schedules tab's form.
839
840
  var pendingScheduleWorkflow = null;
841
+ // Set by a schedule's workflow link: the card to scroll to and mark.
842
+ var pendingWorkflowFocus = null;
843
+
844
+ function focusWorkflowCard(tries) {
845
+ if (!pendingWorkflowFocus) return;
846
+ var card = detail.querySelector('.workflow-card[data-workflow="' + CSS.escape(pendingWorkflowFocus) + '"]');
847
+ if (!card) {
848
+ if ((tries || 0) < 40) setTimeout(function () { focusWorkflowCard((tries || 0) + 1); }, 150);
849
+ return;
850
+ }
851
+ pendingWorkflowFocus = null;
852
+ card.scrollIntoView({ block: 'start' });
853
+ card.classList.add('workflow-focus');
854
+ setTimeout(function () { card.classList.remove('workflow-focus'); }, 2500);
855
+ }
856
+
857
+ // Set by a workflow's Edit on the Skills list: the workflow to open in
858
+ // its editor once the Workflows tab has drawn it.
859
+ var pendingWorkflowEdit = null;
860
+
861
+ function openPendingWorkflowEdit() {
862
+ if (!pendingWorkflowEdit) return;
863
+ var card = detail.querySelector('.workflow-card[data-workflow="' + CSS.escape(pendingWorkflowEdit) + '"]');
864
+ if (!card) return;
865
+ pendingWorkflowEdit = null;
866
+ openWorkflowEditor(currentName, card);
867
+ card.scrollIntoView({ block: 'start' });
868
+ }
869
+
840
870
  // Set by a schedule on a workflow card (or a #schedule-<id> link): the
841
871
  // Schedules tab's row to show.
842
872
  var pendingScheduleFocus = /^#schedule-/.test(location.hash) ? location.hash.slice('#schedule-'.length) : null;
@@ -874,6 +904,7 @@ ${consoleShellScript}
874
904
  el.innerHTML = renderWorkflowsHtml(name, workflows);
875
905
  var cards = el.querySelectorAll('.workflow-card');
876
906
  for (var i = 0; i < cards.length; i++) wireWorkflowCard(name, cards[i]);
907
+ openPendingWorkflowEdit();
877
908
  // Keep a running run's status current while the Overview is open.
878
909
  var running = workflows.some(function (w) { return (w.recentRuns || []).some(function (x) { return x.status === 'running'; }); });
879
910
  if (running && currentTab === 'workflows') workflowsRefreshTimer = setTimeout(function () { if (!workflowEditing) loadWorkflowsSection(name); }, 3000);
@@ -893,7 +924,10 @@ ${consoleShellScript}
893
924
  function workflowEditorHtml(w) {
894
925
  var rows = Object.keys(w.parameters || {}).map(function (p) { return workflowParamRowHtml(p, w.parameters[p]); }).join('');
895
926
  return '<form class="add-source workflow-editor-form" style="margin-top:10px">' +
896
- '<label>Title<input name="title" maxlength="120" value="' + escapeHtml(w.title) + '"></label>' +
927
+ '<label>Title<input name="title" maxlength="120" value="' + escapeHtml(w.title) + '">' +
928
+ '<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>' +
929
+ '<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>' +
930
+ '<input name="description" maxlength="500" required value="' + escapeHtml(w.description) + '"></label>' +
897
931
  '<label>Instructions <span class="hint">(what the agent does, step by step; $name puts in a parameter&#39;s value. Chat uses them too.)</span>' +
898
932
  '<textarea name="instructions" maxlength="12000" rows="12" required>' + escapeHtml(w.instructions) + '</textarea></label>' +
899
933
  '<div class="body-field">Parameters <span class="hint">(values a run is given; apps send them as params)</span>' +
@@ -960,7 +994,7 @@ ${consoleShellScript}
960
994
  result.textContent = 'Saving' + String.fromCharCode(8230);
961
995
  fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(w.id), {
962
996
  method: 'PUT', headers: { 'content-type': 'application/json' },
963
- body: JSON.stringify({ title: form.title.value, instructions: form.instructions.value, parameters: parameters }),
997
+ body: JSON.stringify({ title: form.title.value, description: form.description.value, instructions: form.instructions.value, parameters: parameters }),
964
998
  }).then(function (r) {
965
999
  if (!r.ok) { result.textContent = r.body.error || 'Could not save'; return; }
966
1000
  workflowEditing = false;
@@ -2381,7 +2415,8 @@ ${consoleShellScript}
2381
2415
  return '<tr id="schedule-' + escapeHtml(t.task_id) + '">' +
2382
2416
  '<td style="min-width:180px"><strong>' + escapeHtml(t.task_name || 'Untitled') + '</strong>' +
2383
2417
  (t.workflow
2384
- ? '<div><span class="le-pill accent">Workflow</span> ' + escapeHtml(t.workflow.title) + '</div><div class="schedule-result hint">' + escapeHtml(t.workflow.start_message) +
2418
+ ? '<div><span class="le-pill accent">Workflow</span> <a href="?agent=' + encodeURIComponent(currentName) + '&amp;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>' +
2419
+ '<div class="schedule-result hint">' + escapeHtml(t.workflow.description || t.workflow.start_message) +
2385
2420
  (t.params ? '<br>' + Object.keys(t.params).map(function (k) { return escapeHtml(k) + ': ' + escapeHtml(t.params[k]); }).join(' ' + String.fromCharCode(183) + ' ') : '') + '</div>'
2386
2421
  : t.workflow_missing
2387
2422
  ? '<div><span class="le-pill bad">Workflow removed</span> <span class="hint">' + escapeHtml(t.workflow_id) + '</span></div>'
@@ -2455,6 +2490,17 @@ ${consoleShellScript}
2455
2490
  function wireSchedulesHandlers(name) {
2456
2491
  var content = schedulesContentEl();
2457
2492
  if (!content) return;
2493
+ // A schedule's workflow opens its card on the Workflows tab.
2494
+ var workflowLinks = content.querySelectorAll('.schedule-workflow-link');
2495
+ for (var wl = 0; wl < workflowLinks.length; wl++) {
2496
+ workflowLinks[wl].addEventListener('click', function (ev) {
2497
+ if (ev.metaKey || ev.ctrlKey || ev.shiftKey) return;
2498
+ ev.preventDefault();
2499
+ pendingWorkflowFocus = ev.currentTarget.dataset.workflow;
2500
+ switchTab('workflows');
2501
+ focusWorkflowCard();
2502
+ });
2503
+ }
2458
2504
  var actionBtns = content.querySelectorAll('.schedule-action');
2459
2505
  for (var i = 0; i < actionBtns.length; i++) {
2460
2506
  actionBtns[i].addEventListener('click', function (ev) {
@@ -3544,12 +3590,17 @@ ${consoleShellScript}
3544
3590
  // needs a further per-skill GET, to pull the body that index doesn't
3545
3591
  // carry. ----
3546
3592
 
3593
+ // A skill that's a workflow is labelled, and its Edit opens the workflow
3594
+ // editor: its instructions are one text there, with its title,
3595
+ // description and parameters.
3547
3596
  function renderSkillCard(s) {
3548
3597
  return '<div class="source">' +
3549
3598
  '<div class="source-head">' +
3550
- '<h4>' + escapeHtml(s.name) + '</h4>' +
3599
+ '<h4>' + escapeHtml(s.name) + (s.workflow ? ' <span class="le-pill accent">Workflow</span>' : '') + '</h4>' +
3551
3600
  '<span>' +
3552
- '<button type="button" class="edit-skill-btn" data-id="' + escapeHtml(s.name) + '">Edit</button> ' +
3601
+ (s.workflow
3602
+ ? '<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> '
3603
+ : '<button type="button" class="edit-skill-btn" data-id="' + escapeHtml(s.name) + '">Edit</button> ') +
3553
3604
  '<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
3605
  '</span>' +
3555
3606
  '</div>' +
@@ -3657,6 +3708,15 @@ ${consoleShellScript}
3657
3708
  });
3658
3709
  }
3659
3710
 
3711
+ var workflowEditButtons = panel.querySelectorAll('.edit-workflow-skill-btn');
3712
+ for (var we = 0; we < workflowEditButtons.length; we++) {
3713
+ workflowEditButtons[we].addEventListener('click', function (ev) {
3714
+ pendingWorkflowEdit = ev.currentTarget.getAttribute('data-id');
3715
+ switchTab('workflows');
3716
+ openPendingWorkflowEdit();
3717
+ });
3718
+ }
3719
+
3660
3720
  var editButtons = panel.querySelectorAll('.edit-skill-btn');
3661
3721
  for (var i = 0; i < editButtons.length; i++) {
3662
3722
  editButtons[i].addEventListener('click', function (ev) {
@@ -614,6 +614,9 @@ export function updateWorkflow(agentDir, id, input) {
614
614
  return trimmed;
615
615
  };
616
616
  const title = text(input.title, 'The title', 120);
617
+ const description = input.description === undefined ? undefined : text(input.description, 'The description', 500);
618
+ if (description !== undefined && !description)
619
+ 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
620
  const instructions = input.instructions === undefined ? undefined : text(input.instructions, 'The instructions', 12000);
618
621
  if (instructions !== undefined && !instructions)
619
622
  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 +634,8 @@ export function updateWorkflow(agentDir, id, input) {
631
634
  const s = spec && typeof spec === 'object' ? spec : {};
632
635
  parameters[name] = { description: text(s.description, `The description of "${name}"`, 300), required: s.required === true };
633
636
  }
634
- setWorkflowFields(agentDir, id, instructions !== undefined ? { ...(title ? { title } : {}), instructions, parameters } : { ...(title ? { title } : {}), startMessage, ...(trigger ? { trigger } : {}), parameters });
637
+ const named = { ...(title ? { title } : {}), ...(description ? { description: description.replace(/\s+/g, ' ') } : {}) };
638
+ setWorkflowFields(agentDir, id, instructions !== undefined ? { ...named, instructions, parameters } : { ...named, startMessage, ...(trigger ? { trigger } : {}), parameters });
635
639
  return readWorkflow(agentDir, id);
636
640
  }
637
641
  /** Removes a workflow: its skill stays, as a plain skill the agent can
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",