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
|
-
|
|
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);
|
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);
|
|
@@ -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) + '"
|
|
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'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.
|
|
2418
|
+
? '<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>' +
|
|
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
|
-
|
|
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
|
-
|
|
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
|