loopengine 0.3.10 → 0.3.11

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.
@@ -1,157 +1,15 @@
1
- // The console's Templates page (/templates): ready-made agents
2
- // (core/system-agents/templates.ts) a person can preview and create in
3
- // one click (web/templates-admin.ts behind /starter-templates/*). Same
4
- // shell and styles as the other console pages. Like them, its inline
5
- // script avoids backslashes and template placeholders, since it lives
6
- // inside a template literal.
7
- import { devUiCss } from './dev-ui-styles.js';
8
- import { consoleShellScript, consoleSidebarHtml } from './console-shell.js';
1
+ // Templates are now workflow examples, on the new-workflow page
2
+ // (web/workflow-page.ts). This page only sends a project server from an
3
+ // older template, which still serves it at /templates, there.
4
+ /** @deprecated The console redirects /templates to the workflow examples. */
9
5
  export const templatesPageHtml = `<!doctype html>
10
6
  <html lang="en">
11
7
  <head>
12
8
  <meta charset="utf-8">
13
- <meta name="viewport" content="width=device-width, initial-scale=1">
14
- <title>Templates</title>
15
- <style>${devUiCss}
16
- .tp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; max-width: 1000px; }
17
- .tp-card { border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px; display: grid; gap: 8px; align-content: start; }
18
- .tp-card h3 { margin: 0; font-size: 15px; }
19
- .tp-cat { justify-self: start; font-size: 11.5px; font-weight: 600; color: var(--ink-muted); background: var(--surface); padding: 2px 8px; border-radius: 999px; }
20
- .tp-card .meta { font-size: 12.5px; color: var(--ink-muted); display: grid; gap: 4px; }
21
- .tp-card .row { display: flex; gap: 8px; flex-wrap: wrap; }
22
- .tp-preview { border-top: 1px solid var(--line); padding-top: 10px; display: grid; gap: 8px; font-size: 13px; }
23
- .tp-preview pre { white-space: pre-wrap; max-height: 220px; overflow: auto; margin: 0; }
24
- .tp-btn { display: inline-block; padding: 6px 12px; border-radius: 6px; text-decoration: none; font-size: 13px; font-weight: 500; background: var(--accent); color: var(--on-accent); }
25
- .tp-btn.plain { background: none; color: var(--ink); border: 1px solid var(--line); }
26
- .hint { font-size: 12px; color: var(--ink-muted); }
27
- .error { font-size: 13px; color: var(--bad); }
28
- </style>
9
+ <meta http-equiv="refresh" content="0; url=/workflows/new#examples">
10
+ <title>Workflow examples</title>
29
11
  </head>
30
- <body data-le-page="templates">
31
- <div class="le-app">
32
- ${consoleSidebarHtml}
33
- <main class="le-main">
34
- <div class="le-head plain">
35
- <div class="le-head-row">
36
- <div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">&#9776;</button>
37
- <div><h1>Templates</h1><p>Ready-made agents to start from. Each one asks you before it sends, pays, changes or deletes anything.</p></div>
38
- </div>
39
- </div>
40
- </div>
41
- <div class="le-scroll"><div class="le-page" id="tpPage"><p class="hint">Loading&hellip;</p></div></div>
42
- </main>
43
- </div>
44
- ${consoleShellScript}
45
- <script>
46
- (function () {
47
- var page = document.getElementById('tpPage');
48
- var ELLIPSIS = String.fromCharCode(8230);
49
-
50
- function esc(s) {
51
- return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
52
- return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
53
- });
54
- }
55
- function api(method, path) {
56
- return fetch(path, { method: method, cache: 'no-store' })
57
- .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); });
58
- }
59
- function checklistHtml(items) {
60
- if (!items.length) return '<p>Nothing else to set up.</p>';
61
- return '<ul style="margin:0;padding-left:18px">' + items.map(function (it) {
62
- return '<li>' + (it.href ? '<a href="' + esc(it.href) + '">' + esc(it.label) + '</a>' : esc(it.label)) + '</li>';
63
- }).join('') + '</ul>';
64
- }
65
-
66
- var canInstall = true;
67
- var DECISIONS = { allow: 'runs on its own', ask: 'asks you first', deny: 'never' };
68
- var CHECK = String.fromCharCode(10003);
69
-
70
- function cardHtml(t) {
71
- return '<div class="tp-card" data-id="' + esc(t.id) + '">' +
72
- '<span class="tp-cat">' + esc(t.category) + '</span>' +
73
- '<h3>' + esc(t.title) + '</h3>' +
74
- '<div>' + esc(t.summary) + '</div>' +
75
- '<div class="meta">' +
76
- '<div><strong>Runs:</strong> ' + esc(t.trigger) + '</div>' +
77
- (t.asksFirst.length ? '<div><strong>Asks you first:</strong> ' + esc(t.asksFirst.join(', ')) + '</div>' : '') +
78
- (t.abilities.length || (t.apps || []).length ? '<div><strong>Uses:</strong> ' + t.abilities.map(function (a) { return '<code>' + esc(a) + '</code>'; }).concat((t.apps || []).map(function (a) { return esc(a); })).join(' ') + '</div>' : '') +
79
- '</div>' +
80
- '<div class="row"><button type="button" class="primary tp-use">Use this template</button></div>' +
81
- '<div class="tp-slot"></div></div>';
82
- }
83
-
84
- function listHtml(items) {
85
- return '<ul style="margin:4px 0 0;padding-left:18px">' + items.join('') + '</ul>';
86
- }
87
-
88
- function show() {
89
- api('GET', '/starter-templates').then(function (r) {
90
- if (!r.ok) { page.innerHTML = '<p class="error">' + esc(r.body.error || 'Could not load the templates') + '</p>'; return; }
91
- canInstall = r.body.canInstallAbilities !== false;
92
- page.innerHTML = '<div class="tp-grid">' + r.body.templates.map(cardHtml).join('') + '</div>' +
93
- '<p class="hint" style="margin-top:16px">Not what you need? <a href="/interview">Find your first agent</a> suggests one from your own work, or <a href="/agents?new=1">start from a blank agent</a>.</p>';
94
- Array.prototype.forEach.call(page.querySelectorAll('.tp-use'), function (btn) {
95
- btn.addEventListener('click', function () { preview(btn.closest('.tp-card')); });
96
- });
97
- });
98
- }
99
-
100
- function preview(card) {
101
- var id = card.getAttribute('data-id');
102
- var slot = card.querySelector('.tp-slot');
103
- slot.innerHTML = '<p class="hint">Loading' + ELLIPSIS + '</p>';
104
- api('GET', '/starter-templates/' + encodeURIComponent(id) + '/preview').then(function (r) {
105
- if (!r.ok) { slot.innerHTML = '<p class="error">' + esc(r.body.error || 'Could not prepare it') + '</p>'; return; }
106
- var p = r.body;
107
- var settings = Object.keys(p.settings || {});
108
- slot.innerHTML = '<div class="tp-preview">' +
109
- '<div><strong>Agent:</strong> <code>' + esc(p.name) + '</code></div>' +
110
- '<div><strong>Instructions</strong> <span class="hint">(edit them later in its Overview tab)</span><pre>' + esc(p.systemPrompt) + '</pre></div>' +
111
- (p.abilities.length ? '<div><strong>Installs</strong> <span class="hint">(abilities: their tools, skills and permission rules)</span>' +
112
- listHtml(p.abilities.map(function (a) { return '<li><code>' + esc(a) + '</code></li>'; })) +
113
- (canInstall ? '' : '<p class="error" style="margin:4px 0 0">Installing abilities needs <code>LOOPENGINE_ADMIN_AUTH</code> set on the server. Without it, the agent is created and its abilities head the checklist.</p>') + '</div>' : '') +
114
- (p.apps && p.apps.length ? '<div><strong>Apps it uses</strong> <span class="hint">(connected on the <a href="/config?section=apps">Apps</a> page; one that isn' + String.fromCharCode(8217) + 't connected yet heads the checklist)</span>' + listHtml(p.apps.map(function (a) { return '<li>' + esc(a) + '</li>'; })) + '</div>' : '') +
115
- '<div><strong>Permissions</strong> <span class="hint">(anything not listed asks you first)</span>' +
116
- listHtml(p.rules.map(function (rule) { return '<li><code>' + esc(rule.tool) + '</code>: ' + esc(DECISIONS[rule.decision] || rule.decision) + '</li>'; })) + '</div>' +
117
- (p.skills && p.skills.length ? '<div><strong>Skills it starts with</strong> <span class="hint">(edit them later in Advanced ' + String.fromCharCode(8594) + ' Skills)</span>' + listHtml(p.skills.map(function (sk) { return '<li><code>' + esc(sk) + '</code></li>'; })) + '</div>' : '') +
118
- (p.keys.length ? '<div><strong>Keys it needs</strong>' +
119
- listHtml(p.keys.map(function (k) {
120
- var names = k.names.length > 1 ? 'one of ' + k.names.join(', ') : k.names[0];
121
- return '<li>' + esc(names) + (k.set ? ' <span class="hint">' + CHECK + ' already set</span>' : '') + '</li>';
122
- })) + '</div>' : '') +
123
- (settings.length ? '<div><strong>Settings filled in:</strong> ' + settings.map(function (s) { return '<code>' + esc(s) + '=' + esc(p.settings[s]) + '</code>'; }).join(', ') + '</div>' : '') +
124
- (p.schedule ? '<div><strong>Schedule:</strong> ' + esc(p.schedule) + '</div>' : '') +
125
- (p.needsApprovals ? '<div><strong>Approvals:</strong> requests reach you wherever its Messaging tab sends them (Slack, email, Lark or a webhook).</div>' : '') +
126
- (p.notes.length ? '<div><strong>You also</strong>' + listHtml(p.notes.map(function (n) { return '<li>' + esc(n) + '</li>'; })) + '</div>' : '') +
127
- '<div class="row"><button type="button" class="primary tp-create">Create it</button><button type="button" class="tp-cancel">Cancel</button></div>' +
128
- '<div class="hint tp-status"></div></div>';
129
- slot.querySelector('.tp-cancel').addEventListener('click', function () { slot.innerHTML = ''; });
130
- slot.querySelector('.tp-create').addEventListener('click', function (ev) {
131
- var btn = ev.currentTarget;
132
- btn.disabled = true;
133
- slot.querySelector('.tp-status').textContent = 'Creating it and installing its abilities' + ELLIPSIS + ' (this can take a minute)';
134
- api('POST', '/starter-templates/' + encodeURIComponent(id) + '/create').then(function (r2) {
135
- if (!r2.ok) { btn.disabled = false; slot.querySelector('.tp-status').textContent = r2.body.error || 'Could not create it'; return; }
136
- var c = r2.body;
137
- var installed = c.abilities.installed.map(function (a) { return '<code>' + esc(a) + '</code>'; }).join(' ');
138
- slot.innerHTML = '<div class="tp-preview">' +
139
- '<div><strong>Created <code>' + esc(c.name) + '</code>.</strong> Anything its permissions don' + String.fromCharCode(8217) + 't cover asks you first.</div>' +
140
- (installed ? '<div>' + CHECK + ' Installed ' + installed + '</div>' : '') +
141
- (c.schedule ? '<div>' + CHECK + ' Schedule created, paused: ' + esc(c.schedule.description) + (c.schedule.next_run_at ? ' (first run ' + esc(new Date(c.schedule.next_run_at).toLocaleString(undefined, c.schedule.timeZone ? { timeZone: c.schedule.timeZone, timeZoneName: 'short' } : undefined)) + ' once turned on)' : '') + '</div>' : '') +
142
- (c.loadError ? '<div class="error">It was written but couldn' + String.fromCharCode(8217) + 't be loaded: ' + esc(c.loadError) + '</div>' : '') +
143
- (c.scheduleError ? '<div class="error">Its schedule wasn' + String.fromCharCode(8217) + 't created: ' + esc(c.scheduleError) + '. Add one in its Schedules tab.</div>' : '') +
144
- '<div><strong>Finish setup:</strong>' + checklistHtml(c.checklist) + '</div>' +
145
- (c.exampleMessage ? '<div><strong>Try it</strong> in Chat with: <em>' + esc(c.exampleMessage) + '</em></div>' : '') +
146
- '<div class="row"><a class="tp-btn" href="/agents/config?agent=' + encodeURIComponent(c.name) + '">Open ' + esc(c.name) + '</a>' +
147
- '<a class="tp-btn plain" href="' + esc(leShell.tabHref(c.name, 'chat')) + '">Try it in Chat</a></div></div>';
148
- }).catch(function () { btn.disabled = false; slot.querySelector('.tp-status').textContent = 'Could not reach the server; try again.'; });
149
- });
150
- });
151
- }
152
-
153
- show();
154
- })();
155
- </script>
12
+ <body>
13
+ <p>Templates are now workflow examples: <a href="/workflows/new#examples">open them</a>.</p>
156
14
  </body>
157
15
  </html>`;
@@ -4,10 +4,12 @@
4
4
  // agent it would go into, and creates it: as a new agent through the same
5
5
  // path as a template (web/templates-admin.ts), or added to an existing
6
6
  // one without changing anything that agent already does.
7
+ import { existsSync } from 'node:fs';
7
8
  import { join } from 'node:path';
8
9
  import { addGatewayTool, readGatewayToolsFromDir } from '../core/gateway-tools.js';
9
10
  import { applyPlannedRules } from './actauth-admin.js';
10
11
  import { writeSkill } from './skills-admin.js';
12
+ import { setAgentEnvVar } from './env-admin.js';
11
13
  import { createFromTemplateObject, installFailureReason, templateChecklist } from './templates-admin.js';
12
14
  import { templateScheduleStart } from '../core/system-agents/templates.js';
13
15
  import { planParameters, planWebhook, planWorkflowWithModel, workflowTemplate } from '../core/system-agents/workflow-planner.js';
@@ -436,6 +438,17 @@ export async function createWorkflow(input, deps) {
436
438
  const ruled = applyPlannedRules(agent.name, plan.rules);
437
439
  // 3. Its steps, as a skill; then load the agent again with all of it.
438
440
  writeSkill(agent.name, plan.id, { description: plan.skill.description, body: plan.skill.body });
441
+ // The skills and settings it comes with, where the agent has none:
442
+ // its own policies and values stay as they are.
443
+ for (const skill of plan.skills ?? []) {
444
+ if (skill.id === plan.id || existsSync(join(deps.agentDir(agent.name), 'skills', skill.id, 'SKILL.md')))
445
+ continue;
446
+ writeSkill(agent.name, skill.id, { description: skill.description, body: skill.body });
447
+ }
448
+ for (const [variable, value] of Object.entries(plan.env ?? {})) {
449
+ if (!deps.envValue(agent.name, variable))
450
+ await setAgentEnvVar(agent.name, variable, value);
451
+ }
439
452
  let loadError;
440
453
  try {
441
454
  await deps.reloadAgent(agent.name);
@@ -1,6 +1,7 @@
1
- // The console's "Describe a workflow" page (/workflows/new): a person
2
- // describes a workflow they already have in mind, sees the plan the
3
- // planner makes of it (web/workflow-admin.ts behind /workflows/*): each
1
+ // The console's "New workflow" page (/workflows/new): a person starts
2
+ // from a ready-made example (core/system-agents/workflow-examples.ts,
3
+ // which replace the old templates) or describes a workflow they already
4
+ // have in mind, and sees the plan (web/workflow-admin.ts behind /workflows/*): each
4
5
  // step matched to a tool, the checkpoints where it asks them, the
5
6
  // permissions, the abilities it needs and the agent it goes into, and
6
7
  // creates it with one click. Same shell and styles as the other console
@@ -13,12 +14,18 @@ export const workflowPageHtml = `<!doctype html>
13
14
  <head>
14
15
  <meta charset="utf-8">
15
16
  <meta name="viewport" content="width=device-width, initial-scale=1">
16
- <title>Describe a workflow</title>
17
+ <title>New workflow</title>
17
18
  <style>${devUiCss}
18
19
  .wf { max-width: 860px; display: grid; gap: 18px; }
19
20
  .wf textarea { width: 100%; min-height: 130px; box-sizing: border-box; font: inherit; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); color: var(--ink); }
20
- .wf-examples { display: flex; flex-wrap: wrap; gap: 6px; }
21
- .wf-examples button { font-size: 12.5px; }
21
+ .wf-ex-group { display: grid; gap: 8px; }
22
+ .wf-ex-group h3 { margin: 4px 0 0; font-size: 12px; font-weight: 600; color: var(--ink-muted); text-transform: uppercase; letter-spacing: 0.05em; }
23
+ .wf-ex-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
24
+ .wf-ex { border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; display: grid; gap: 6px; align-content: start; background: var(--bg); }
25
+ .wf-ex strong { font-size: 14px; }
26
+ .wf-ex p { margin: 0; font-size: 13px; color: var(--ink-muted); }
27
+ .wf-ex .wf-btn { justify-self: start; }
28
+ .wf-instructions textarea { min-height: 180px; font-family: var(--font-mono); font-size: 12px; }
22
29
  .wf-card { border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px; display: grid; gap: 10px; background: var(--bg); }
23
30
  .wf-card h2 { margin: 0; font-size: 18px; }
24
31
  .wf-card h3 { margin: 6px 0 0; font-size: 14px; }
@@ -61,7 +68,7 @@ ${consoleSidebarHtml}
61
68
  <div class="le-head plain">
62
69
  <div class="le-head-row">
63
70
  <div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">&#9776;</button>
64
- <div><h1>Describe a workflow</h1><p>Say what you want done, step by step, and where you want to decide. You get a plan to check before anything is created.</p></div>
71
+ <div><h1>New workflow</h1><p>Describe what you want done, or start from an example. You get a plan to check before anything is created.</p></div>
65
72
  </div>
66
73
  </div>
67
74
  </div>
@@ -75,14 +82,8 @@ ${consoleShellScript}
75
82
  var ELLIPSIS = String.fromCharCode(8230);
76
83
  var DASH = String.fromCharCode(8212);
77
84
  var LABELS = { allow: 'Allow', ask: 'Ask me', deny: 'Never' };
78
- var EXAMPLES = [
79
- ['Blog post', 'Every Monday morning, browse the web for timely blog post ideas about our product area. Ask me on Slack to pick one of the ideas. Then draft the post and let me review it before it is created.'],
80
- ['Weekly digest', 'Every Friday afternoon, search the web for news about our competitors and summarise the important changes. Send me the summary for review before it goes to the team channel.'],
81
- ['Refund requests', 'When asked, look up the customer' + String.fromCharCode(8217) + 's order and check our refund policy. If a refund fits the policy, prepare it and ask me to approve it before it is issued.'],
82
- ['Stripe disputes', 'When a Stripe dispute comes in, look up the order and its delivery, write the evidence for Stripe, and ask me before submitting it.']
83
- ];
84
85
  var SENDERS = { stripe: 'Stripe', github: 'GitHub', shopify: 'Shopify', standard: 'Standard Webhooks', custom: 'your app' };
85
- var state = { description: '', planned: null, agents: [], ready: false, canInstall: false };
86
+ var state = { description: '', planned: null, agents: [], examples: [], ready: false, canInstall: false };
86
87
 
87
88
  function esc(s) {
88
89
  return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
@@ -103,16 +104,20 @@ ${consoleShellScript}
103
104
  (state.ready ? '' : '<p class="error">Planning needs a model: set one on an agent (Overview ' + String.fromCharCode(8594) + ' Model) and add its key under <a href="/config?section=models">Models</a>.</p>') +
104
105
  '<div class="wf-card"><label for="wfText"><strong>Your workflow</strong> <span class="hint">what starts it, the steps in order, and where you want to choose or approve</span></label>' +
105
106
  '<textarea id="wfText" placeholder="Every Monday, find blog ideas on the web, ask me on Slack to pick one, draft it, and let me review it before it goes up.">' + esc(state.description) + '</textarea>' +
106
- '<div class="wf-examples"><span class="hint">Examples:</span>' + EXAMPLES.map(function (e, i) { return '<button type="button" class="wf-btn plain" data-example="' + i + '">' + esc(e[0]) + '</button>'; }).join('') + '</div>' +
107
107
  '<div class="wf-actions"><button type="button" class="wf-btn" id="wfPlan"' + (state.ready ? '' : ' disabled') + '>Plan it</button>' +
108
108
  '<span class="hint" id="wfStatus">It looks at your agents and abilities first; this can take a few minutes.</span></div>' +
109
109
  (error ? '<p class="error">' + esc(error) + '</p>' : '') +
110
110
  '</div>' +
111
- '<p class="hint">Rather start from something ready-made? <a href="/templates">Start from a template</a>, or <a href="/interview">find your first agent</a>.</p>';
111
+ '<p class="hint">Rather start from something ready-made? <a href="#examples">Choose a workflow example</a>, or <a href="/interview">find your first agent</a>.</p>' +
112
+ examplesHtml();
112
113
  var text = document.getElementById('wfText');
113
114
  var examples = page.querySelectorAll('[data-example]');
114
115
  for (var i = 0; i < examples.length; i++) {
115
- examples[i].addEventListener('click', function (ev) { text.value = EXAMPLES[Number(ev.currentTarget.dataset.example)][1]; text.focus(); });
116
+ examples[i].addEventListener('click', function (ev) { openExample(ev.currentTarget.dataset.example); });
117
+ }
118
+ if (location.hash === '#examples') {
119
+ var box = document.getElementById('examples');
120
+ if (box) box.scrollIntoView();
116
121
  }
117
122
  document.getElementById('wfPlan').addEventListener('click', function () {
118
123
  state.description = text.value.trim();
@@ -127,6 +132,37 @@ ${consoleShellScript}
127
132
  });
128
133
  }
129
134
 
135
+ // Ready-made plans, by category: opening one goes straight to the
136
+ // review, with nothing planned by a model.
137
+ function examplesHtml() {
138
+ if (!state.examples.length) return '';
139
+ var groups = [];
140
+ state.examples.forEach(function (e) {
141
+ var g = groups.filter(function (x) { return x.name === e.category; })[0];
142
+ if (!g) { g = { name: e.category, items: [] }; groups.push(g); }
143
+ g.items.push(e);
144
+ });
145
+ return '<div class="wf-card" id="examples"><h2>Start from an example</h2>' +
146
+ '<p class="hint" style="margin:0">Ready-made and tested. You see the whole plan, and can change it, before anything is created.</p>' +
147
+ groups.map(function (g) {
148
+ return '<div class="wf-ex-group"><h3>' + esc(g.name) + '</h3><div class="wf-ex-grid">' + g.items.map(function (e) {
149
+ return '<div class="wf-ex"><strong>' + esc(e.plan.name) + '</strong><p>' + esc(e.plan.summary) + '</p>' +
150
+ '<p><strong>Starts:</strong> ' + esc(e.plan.trigger.description) + '</p>' +
151
+ '<button type="button" class="wf-btn plain" data-example="' + esc(e.id) + '">Use this</button></div>';
152
+ }).join('') + '</div></div>';
153
+ }).join('') +
154
+ '</div>';
155
+ }
156
+
157
+ function openExample(id) {
158
+ var e = state.examples.filter(function (x) { return x.id === id; })[0];
159
+ if (!e) return;
160
+ // A copy: the review screen edits the plan it shows.
161
+ state.planned = { plan: JSON.parse(JSON.stringify(e.plan)), example: e.id };
162
+ renderPlan();
163
+ window.scrollTo(0, 0);
164
+ }
165
+
130
166
  // Planning runs on the server as a job; check on it every few seconds.
131
167
  function waitForPlan(id, started) {
132
168
  setTimeout(function () {
@@ -272,10 +308,13 @@ ${consoleShellScript}
272
308
  (p.rules.length ? '<h3>Permissions it sets</h3><ul class="wf-list">' + p.rules.map(function (r) { return '<li><code>' + esc(r.tool) + '</code> ' + pill(r.decision) + '</li>'; }).join('') + '</ul>' : '') +
273
309
  (p.abilities.length ? '<h3>Abilities it uses</h3><ul class="wf-list">' + p.abilities.map(function (a) { return '<li><code>' + esc(a) + '</code></li>'; }).join('') + '</ul>' + (state.canInstall ? '' : '<p class="hint">Installing abilities needs LOOPENGINE_ADMIN_AUTH set; otherwise they head the checklist.</p>') : '') +
274
310
  (p.mcpServers && p.mcpServers.length ? '<h3>Apps it connects to</h3><div class="wf-mcp">' + p.mcpServers.map(mcpRow).join('') + '</div>' : '') +
311
+ (p.apps && p.apps.length ? '<h3>Apps it uses</h3><ul class="wf-list">' + p.apps.map(function (a) { return '<li>' + esc(a.title) + ' <span class="hint">' + a.tools.length + ' tool' + (a.tools.length === 1 ? '' : 's') + '</span></li>'; }).join('') + '</ul><p class="hint" style="margin:0">Connected on the <a href="/config?section=apps">Apps</a> page; one that isn' + String.fromCharCode(8217) + 't connected yet heads the checklist.</p>' : '') +
312
+ (p.skills && p.skills.length ? '<h3>Skills it comes with</h3><ul class="wf-list">' + p.skills.map(function (sk) { return '<li><code>' + esc(sk.id) + '</code> <span class="hint">' + esc(sk.description) + '</span></li>'; }).join('') + '</ul><p class="hint" style="margin:0">An agent that already has one keeps its own.</p>' : '') +
275
313
  (p.keys.length ? '<h3>Keys it needs</h3><p class="hint" style="margin:0">' + p.keys.map(esc).join(', ') + ' ' + DASH + ' you add them after, from the checklist.</p>' : '') +
276
314
  (p.needsApprovals ? '<p class="hint" style="margin:0">It asks you through the agent' + String.fromCharCode(8217) + 's primary messaging app (Messaging tab) when nobody is in Chat.</p>' : '') +
277
315
  '</div>' +
278
316
  (p.gaps.length ? '<div class="wf-gaps"><strong>Worth knowing</strong><ul>' + p.gaps.map(function (g) { return '<li>' + esc(g) + '</li>'; }).join('') + '</ul></div>' : '') +
317
+ (p.notes && p.notes.length ? '<div class="wf-card"><strong>After it' + String.fromCharCode(8217) + 's created, you also</strong><ul class="wf-list">' + p.notes.map(function (n) { return '<li>' + esc(n) + '</li>'; }).join('') + '</ul></div>' : '') +
279
318
  '<div class="wf-card"><h3 style="margin:0">Where it goes</h3>' +
280
319
  '<div class="wf-target">' +
281
320
  '<label><input type="radio" name="wfTarget" value="new"' + (suggested ? '' : ' checked') + '> A new agent, <code>' + esc(p.newAgent.name) + '</code></label>' +
@@ -283,14 +322,29 @@ ${consoleShellScript}
283
322
  (others.length ? '<label><input type="radio" name="wfTarget" value="other"> Add to <select id="wfOther">' + others.map(function (a) { return '<option>' + esc(a) + '</option>'; }).join('') + '</select></label>' : '') +
284
323
  '</div>' +
285
324
  (p.target.reason ? '<p class="hint" style="margin:0">' + esc(p.target.reason) + '</p>' : '') +
325
+ '<div class="wf-instructions" id="wfNewAgentBox"' + (suggested ? ' hidden' : '') + '><label for="wfInstructions"><strong>Its instructions</strong> <span class="hint">read on every run of every workflow it has: who it is, rules it always follows, and anything marked (edit this). The steps are the workflow' + String.fromCharCode(8217) + 's.</span></label>' +
326
+ '<textarea id="wfInstructions">' + esc(p.newAgent.systemPrompt) + '</textarea></div>' +
327
+ '<p class="hint" style="margin:0" id="wfExistingNote"' + (suggested ? '' : ' hidden') + '>An agent you have keeps its own instructions; only the workflow, its abilities and permissions are added.</p>' +
286
328
  (state.planned.note ? '<p class="hint" style="margin:0"><strong>Note:</strong> ' + esc(state.planned.note) + '</p>' : '') +
287
329
  '<p class="hint" style="margin:0">Added to an existing agent, it keeps every decision that agent already has; any it would change are listed afterwards.</p>' +
288
330
  '</div>' +
289
331
  '<details class="wf-card"><summary><strong>The steps the agent follows</strong> <span class="hint">(saved as its skill <code>' + esc(p.id) + '</code>)</span></summary><div class="wf-skill">' + esc(p.skill.body) + '</div></details>' +
290
- '<div class="wf-actions"><button type="button" class="wf-btn" id="wfCreate">Create it</button><button type="button" class="wf-btn plain" id="wfBack">Change the description</button>' +
332
+ '<div class="wf-actions"><button type="button" class="wf-btn" id="wfCreate">Create it</button><button type="button" class="wf-btn plain" id="wfBack">' + (state.planned.example ? 'Back to the examples' : 'Change the description') + '</button>' +
291
333
  '<span class="hint" id="wfStatus"></span></div>' +
292
334
  (error ? '<p class="error">' + esc(error) + '</p>' : '');
293
- document.getElementById('wfBack').addEventListener('click', function () { renderDescribe(); });
335
+ document.getElementById('wfBack').addEventListener('click', function () {
336
+ var fromExample = state.planned.example;
337
+ renderDescribe();
338
+ if (fromExample) document.getElementById('examples').scrollIntoView();
339
+ });
340
+ var targets = page.querySelectorAll('input[name="wfTarget"]');
341
+ for (var t = 0; t < targets.length; t++) {
342
+ targets[t].addEventListener('change', function () {
343
+ var isNew = page.querySelector('input[name="wfTarget"]:checked').value === 'new';
344
+ document.getElementById('wfNewAgentBox').hidden = !isNew;
345
+ document.getElementById('wfExistingNote').hidden = isNew;
346
+ });
347
+ }
294
348
  var paramsBox = document.getElementById('wfParams');
295
349
  paramsBox.addEventListener('click', function (ev) {
296
350
  if (ev.target.classList.contains('wf-param-remove')) ev.target.closest('.wf-param').remove();
@@ -310,6 +364,7 @@ ${consoleShellScript}
310
364
  delete p.webhook;
311
365
  if (p.trigger.kind === 'webhook') p.trigger.kind = 'on_request';
312
366
  }
367
+ p.newAgent.systemPrompt = document.getElementById('wfInstructions').value.trim() || p.newAgent.systemPrompt;
313
368
  var chosen = page.querySelector('input[name="wfTarget"]:checked');
314
369
  var target = chosen.value === 'new' ? { kind: 'new' } : { kind: 'existing', agent: chosen.value === 'other' ? document.getElementById('wfOther').value : chosen.dataset.agent };
315
370
  var btn = document.getElementById('wfCreate');
@@ -340,7 +395,7 @@ ${consoleShellScript}
340
395
  (c.keptDecisions.length ? '<p class="hint" style="margin:0"><strong>Kept as they were:</strong> ' + c.keptDecisions.map(function (k) { return '<code>' + esc(k.tool) + '</code> stays ' + esc(LABELS[k.has]) + ' (the workflow wanted ' + esc(LABELS[k.wants]) + ')'; }).join('; ') + '. Change them in Permissions if you want.</p>' : '') +
341
396
  (items.length ? '<h3>Left to do</h3><ul class="wf-list">' + items.map(function (it) { return '<li>' + (it.href ? '<a href="' + esc(it.href) + '">' + esc(it.label) + '</a>' : esc(it.label)) + '</li>'; }).join('') + '</ul>' : '<p class="hint" style="margin:0">Nothing left to set up.</p>') +
342
397
  '<div class="wf-actions"><a class="wf-btn" href="' + agentHref + '">Open ' + esc(c.agent) + '</a><a class="wf-btn plain" href="/playground?agent=' + encodeURIComponent(c.agent) + '">Try it in Chat</a>' +
343
- '<button type="button" class="wf-btn plain" id="wfAnother">Describe another</button></div>' +
398
+ '<button type="button" class="wf-btn plain" id="wfAnother">New workflow</button></div>' +
344
399
  '</div>';
345
400
  document.getElementById('wfAnother').addEventListener('click', function () { state.description = ''; state.planned = null; renderDescribe(); });
346
401
  wireWebhookDone(c);
@@ -410,6 +465,7 @@ ${consoleShellScript}
410
465
  state.ready = Boolean(d.ready);
411
466
  state.canInstall = Boolean(d.canInstallAbilities);
412
467
  state.agents = d.agents || [];
468
+ state.examples = d.examples || [];
413
469
  renderDescribe();
414
470
  })
415
471
  .catch(function (err) { page.innerHTML = '<p class="error">Could not load: ' + esc(err.message) + '</p>'; });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",