loopengine 0.3.6 → 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,8 @@ 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; }
53
+ tr.schedule-focus td { background: var(--le-accent-soft, #e6f4ef); transition: background 0.3s; }
52
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); }
53
55
  .workflow-starts h4 { margin: 0; font-size: 13.5px; font-weight: 600; }
54
56
  .wf-start { padding: 10px 0; border-top: 1px solid var(--le-line, #e5e7eb); display: grid; gap: 6px; font-size: 13px; }
@@ -709,7 +711,7 @@ ${consoleShellScript}
709
711
  return text.charAt(0).toUpperCase() + text.slice(1);
710
712
  }
711
713
 
712
- // How it starts: four ways, each with a ? that says what it is. A
714
+ // Ways to run it: four, each with a ? that says what it is. A
713
715
  // schedule (pause, resume, remove, add), a webhook, Run now with the
714
716
  // values a run is given, and the API call an app makes.
715
717
  function wfStartSection(key, title, help, body) {
@@ -722,14 +724,15 @@ ${consoleShellScript}
722
724
  function workflowStartsHtml(w, agentName, paramsHtml) {
723
725
  var schedules = (w.schedules || []).map(function (x) {
724
726
  var state = x.waiting ? '<span class="le-pill ask">Waiting for a decision</span>'
725
- : x.status === 'paused' ? '<span class="le-pill">Paused</span>'
727
+ : x.status === 'paused' ? '<span class="le-pill warn">Paused</span>'
726
728
  : x.status === 'running' ? '<span class="le-pill accent">Running</span>'
727
729
  : (x.nextRunAt ? '<span class="hint">next ' + escapeHtml(new Date(x.nextRunAt).toLocaleString()) + '</span>' : '');
728
730
  var values = x.params ? Object.keys(x.params).map(function (k) { return escapeHtml(k) + ': ' + escapeHtml(x.params[k]); }).join(' ' + String.fromCharCode(183) + ' ') : '';
729
731
  return '<li style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">' +
730
- '<span>' + escapeHtml(x.describe.charAt(0).toUpperCase() + x.describe.slice(1)) + (x.name ? ' <span class="hint">(' + escapeHtml(x.name) + ')</span>' : '') + '</span>' + state +
732
+ '<span><a href="?agent=' + encodeURIComponent(agentName) + '&amp;tab=schedules#schedule-' + escapeHtml(x.taskId) + '" class="workflow-schedule-link" data-task="' + escapeHtml(x.taskId) + '" title="See it in the Schedules tab">' +
733
+ escapeHtml(x.describe.charAt(0).toUpperCase() + x.describe.slice(1)) + '</a></span>' + state +
731
734
  (values ? '<span class="hint">' + values + '</span>' : '') +
732
- (x.notify ? '<span class="hint">sends you each result</span>' : '') +
735
+ (x.notify ? '<span class="hint">results go to your messaging app</span>' : '') +
733
736
  '<span style="display:flex;gap:6px;margin-left:auto">' +
734
737
  (x.status === 'paused' ? '<button type="button" class="workflow-schedule-action" data-id="' + escapeHtml(x.taskId) + '" data-action="resume">Resume</button>'
735
738
  : x.status === 'scheduled' ? '<button type="button" class="workflow-schedule-action" data-id="' + escapeHtml(x.taskId) + '" data-action="pause">Pause</button>' : '') +
@@ -737,11 +740,11 @@ ${consoleShellScript}
737
740
  '</span></li>';
738
741
  }).join('');
739
742
  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
+ // A body that shows what a call can carry: its values, and waiting
744
+ // for the result.
745
+ var example = '{ ' + (paramNames.length ? '"params": { ' + paramNames.map(function (p) { return '"' + p + '": "' + String.fromCharCode(8230) + '"'; }).join(', ') + ' }, ' : '') + '"wait": true }';
743
746
  var apiUrl = location.origin + '/agents/' + encodeURIComponent(agentName) + '/workflows/' + encodeURIComponent(w.id) + '/run';
744
- return '<div class="workflow-starts"><h4>How it starts</h4>' +
747
+ return '<div class="workflow-starts"><h4>Ways to run it</h4>' +
745
748
  wfStartSection('schedule-' + w.id, 'Schedule',
746
749
  '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
750
  (schedules ? '<ul class="schedule-history">' + schedules + '</ul>' : '<span class="hint">None yet.</span>') +
@@ -756,7 +759,9 @@ ${consoleShellScript}
756
759
  '<div><button type="button" class="workflow-run primary">Run now</button></div>') +
757
760
  wfStartSection('api-' + w.id, 'API',
758
761
  '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>') +
762
+ '<pre>POST ' + escapeHtml(apiUrl) + String.fromCharCode(10) + escapeHtml(example) + '</pre>' +
763
+ '<p class="hint" style="margin:0">' + (paramNames.length ? '<code>params</code>: ' + paramNames.map(function (p) { return '<code>' + escapeHtml(p) + '</code>' + (w.parameters[p].required ? ' (required)' : '') + (w.parameters[p].description ? ', ' + escapeHtml(w.parameters[p].description) : ''); }).join('; ') + '. ' : 'It takes no params. ') +
764
+ 'Optional: <code>"wait": true</code> answers with the result; <code>"notify": true</code> sends it through the agent' + String.fromCharCode(8217) + 's messaging app; <code>"callback_url"</code> posts it to your app when the run ends. Leave them out to get the run' + String.fromCharCode(8217) + 's id back at once.</p>') +
760
765
  '</div>';
761
766
  }
762
767
 
@@ -771,7 +776,7 @@ ${consoleShellScript}
771
776
  : s.waiting
772
777
  ? '<span class="le-pill ask">Waiting for a decision</span>'
773
778
  : s.status === 'paused'
774
- ? '<span class="le-pill">Schedule paused</span>'
779
+ ? '<span class="le-pill warn">Schedule paused</span>'
775
780
  : s.status === 'running'
776
781
  ? '<span class="le-pill accent">Running</span>'
777
782
  : '<span class="le-pill good">On</span>' + (s.nextRunAt ? ' <span class="hint">next ' + escapeHtml(new Date(s.nextRunAt).toLocaleString()) + '</span>' : '');
@@ -833,6 +838,53 @@ ${consoleShellScript}
833
838
  var workflowsRefreshTimer = null;
834
839
  // Set by a workflow's "Add a schedule", for the Schedules tab's form.
835
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
+
870
+ // Set by a schedule on a workflow card (or a #schedule-<id> link): the
871
+ // Schedules tab's row to show.
872
+ var pendingScheduleFocus = /^#schedule-/.test(location.hash) ? location.hash.slice('#schedule-'.length) : null;
873
+
874
+ // Scrolls to that row and marks it for a moment, once the tab has drawn
875
+ // it (it may still be loading).
876
+ function focusScheduleRow(tries) {
877
+ if (!pendingScheduleFocus) return;
878
+ var row = detail.querySelector('#schedule-' + CSS.escape(pendingScheduleFocus));
879
+ if (!row) {
880
+ if ((tries || 0) < 40) setTimeout(function () { focusScheduleRow((tries || 0) + 1); }, 150);
881
+ return;
882
+ }
883
+ pendingScheduleFocus = null;
884
+ row.scrollIntoView({ block: 'center' });
885
+ row.classList.add('schedule-focus');
886
+ setTimeout(function () { row.classList.remove('schedule-focus'); }, 2500);
887
+ }
836
888
  var workflowsById = {};
837
889
  // Set while a workflow's editor is open, so the running-run refresh
838
890
  // doesn't redraw the card under someone typing.
@@ -852,6 +904,7 @@ ${consoleShellScript}
852
904
  el.innerHTML = renderWorkflowsHtml(name, workflows);
853
905
  var cards = el.querySelectorAll('.workflow-card');
854
906
  for (var i = 0; i < cards.length; i++) wireWorkflowCard(name, cards[i]);
907
+ openPendingWorkflowEdit();
855
908
  // Keep a running run's status current while the Overview is open.
856
909
  var running = workflows.some(function (w) { return (w.recentRuns || []).some(function (x) { return x.status === 'running'; }); });
857
910
  if (running && currentTab === 'workflows') workflowsRefreshTimer = setTimeout(function () { if (!workflowEditing) loadWorkflowsSection(name); }, 3000);
@@ -871,7 +924,10 @@ ${consoleShellScript}
871
924
  function workflowEditorHtml(w) {
872
925
  var rows = Object.keys(w.parameters || {}).map(function (p) { return workflowParamRowHtml(p, w.parameters[p]); }).join('');
873
926
  return '<form class="add-source workflow-editor-form" style="margin-top:10px">' +
874
- '<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>' +
875
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>' +
876
932
  '<textarea name="instructions" maxlength="12000" rows="12" required>' + escapeHtml(w.instructions) + '</textarea></label>' +
877
933
  '<div class="body-field">Parameters <span class="hint">(values a run is given; apps send them as params)</span>' +
@@ -938,7 +994,7 @@ ${consoleShellScript}
938
994
  result.textContent = 'Saving' + String.fromCharCode(8230);
939
995
  fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(w.id), {
940
996
  method: 'PUT', headers: { 'content-type': 'application/json' },
941
- 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 }),
942
998
  }).then(function (r) {
943
999
  if (!r.ok) { result.textContent = r.body.error || 'Could not save'; return; }
944
1000
  workflowEditing = false;
@@ -1134,6 +1190,17 @@ ${consoleShellScript}
1134
1190
  var result = card.querySelector('.workflow-result');
1135
1191
  var base = '/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(id);
1136
1192
  card.querySelector('.workflow-edit').addEventListener('click', function () { openWorkflowEditor(name, card); });
1193
+ // A schedule opens its own row in the Schedules tab.
1194
+ var scheduleLinks = card.querySelectorAll('.workflow-schedule-link');
1195
+ for (var sl = 0; sl < scheduleLinks.length; sl++) {
1196
+ scheduleLinks[sl].addEventListener('click', function (ev) {
1197
+ if (ev.metaKey || ev.ctrlKey || ev.shiftKey) return;
1198
+ ev.preventDefault();
1199
+ pendingScheduleFocus = ev.currentTarget.dataset.task;
1200
+ switchTab('schedules');
1201
+ focusScheduleRow();
1202
+ });
1203
+ }
1137
1204
  // The ? beside each way it starts shows or hides what it is.
1138
1205
  var helps = card.querySelectorAll('.help-q');
1139
1206
  for (var hq = 0; hq < helps.length; hq++) {
@@ -2345,10 +2412,11 @@ ${consoleShellScript}
2345
2412
  return '<li><span class="le-pill ' + (h.status === 'ok' ? 'good' : 'bad') + '">' + escapeHtml(h.status) + '</span> <span class="hint">' + escapeHtml(fmtScheduleTime(h.ran_at)) + '</span>' +
2346
2413
  '<div class="schedule-result">' + escapeHtml(h.status === 'ok' ? (h.result || '') : (h.error || '')) + '</div></li>';
2347
2414
  }).join('');
2348
- return '<tr>' +
2415
+ return '<tr id="schedule-' + escapeHtml(t.task_id) + '">' +
2349
2416
  '<td style="min-width:180px"><strong>' + escapeHtml(t.task_name || 'Untitled') + '</strong>' +
2350
2417
  (t.workflow
2351
- ? '<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) +
2352
2420
  (t.params ? '<br>' + Object.keys(t.params).map(function (k) { return escapeHtml(k) + ': ' + escapeHtml(t.params[k]); }).join(' ' + String.fromCharCode(183) + ' ') : '') + '</div>'
2353
2421
  : t.workflow_missing
2354
2422
  ? '<div><span class="le-pill bad">Workflow removed</span> <span class="hint">' + escapeHtml(t.workflow_id) + '</span></div>'
@@ -2381,7 +2449,7 @@ ${consoleShellScript}
2381
2449
  return '<option value="' + escapeHtml(w.id) + '">' + escapeHtml(w.title) + '</option>';
2382
2450
  }).join('') + '</select></div>' +
2383
2451
  '<div class="workflow-params" id="scheduleParams" style="margin:0 0 4px 22px"></div>' +
2384
- '<p class="hint" style="margin:0 0 4px 22px">Each run starts with the workflow&#39;s start message as it is then, so changing the workflow changes what runs.</p>' +
2452
+ '<p class="hint" style="margin:0 0 4px 22px">Each run follows the workflow&#39;s instructions as they are then, so changing the workflow changes what runs.</p>' +
2385
2453
  '<div class="schedule-when"><input type="radio" name="runs" value="message" id="runsMessage"><label for="runsMessage">A message</label></div>' +
2386
2454
  '</div>'
2387
2455
  : '') +
@@ -2422,6 +2490,17 @@ ${consoleShellScript}
2422
2490
  function wireSchedulesHandlers(name) {
2423
2491
  var content = schedulesContentEl();
2424
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
+ }
2425
2504
  var actionBtns = content.querySelectorAll('.schedule-action');
2426
2505
  for (var i = 0; i < actionBtns.length; i++) {
2427
2506
  actionBtns[i].addEventListener('click', function (ev) {
@@ -2480,6 +2559,7 @@ ${consoleShellScript}
2480
2559
  form.scrollIntoView({ block: 'start' });
2481
2560
  }
2482
2561
  pendingScheduleWorkflow = null;
2562
+ focusScheduleRow();
2483
2563
  renderScheduleParams();
2484
2564
  if (workflowSelect) workflowSelect.addEventListener('change', renderScheduleParams);
2485
2565
  for (var k = 0; k < runsRadios.length; k++) {
@@ -3510,12 +3590,17 @@ ${consoleShellScript}
3510
3590
  // needs a further per-skill GET, to pull the body that index doesn't
3511
3591
  // carry. ----
3512
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.
3513
3596
  function renderSkillCard(s) {
3514
3597
  return '<div class="source">' +
3515
3598
  '<div class="source-head">' +
3516
- '<h4>' + escapeHtml(s.name) + '</h4>' +
3599
+ '<h4>' + escapeHtml(s.name) + (s.workflow ? ' <span class="le-pill accent">Workflow</span>' : '') + '</h4>' +
3517
3600
  '<span>' +
3518
- '<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> ') +
3519
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>' +
3520
3605
  '</span>' +
3521
3606
  '</div>' +
@@ -3623,6 +3708,15 @@ ${consoleShellScript}
3623
3708
  });
3624
3709
  }
3625
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
+
3626
3720
  var editButtons = panel.querySelectorAll('.edit-skill-btn');
3627
3721
  for (var i = 0; i < editButtons.length; i++) {
3628
3722
  editButtons[i].addEventListener('click', function (ev) {
@@ -4413,7 +4507,7 @@ ${consoleShellScript}
4413
4507
  '<div class="tab-panel" data-tab-panel="overview">' + renderOverviewHtml(cfg) + '</div>' +
4414
4508
  // Workflows: the jobs this agent does, each with what starts it.
4415
4509
  '<div class="tab-panel" data-tab-panel="workflows">' +
4416
- '<p class="muted" style="margin:0 0 20px">The jobs this agent does on its own: what it does, what starts it (schedules, webhooks, Run now or an app&#39;s API call) and how its runs went. In Chat it can do them too, when you ask.</p>' +
4510
+ '<p class="muted" style="margin:0 0 20px">The jobs this agent does on its own: what it does, the ways to run it (a schedule, a webhook, Run now or an app&#39;s API call) and how its runs went. In Chat it can do them too, when you ask.</p>' +
4417
4511
  '<section id="workflowsSection"><div id="workflowsContent"><p class="hint">Loading&hellip;</p></div></section>' +
4418
4512
  '</div>' +
4419
4513
  // Advanced: the agent's own tools (and Composio, subagents), then its
@@ -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.6",
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",