loopengine 0.1.91 → 0.1.93
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.
- package/README.md +16 -4
- package/dist/bin/cli.js +3 -2
- package/dist/core/agent-config.d.ts +19 -7
- package/dist/core/email-senders.d.ts +26 -0
- package/dist/core/email-senders.js +108 -0
- package/dist/core/http-notifier.js +6 -6
- package/dist/core/http-notify-triggers/email.d.ts +14 -8
- package/dist/core/http-notify-triggers/email.js +70 -28
- package/dist/core/notifier-file.d.ts +11 -3
- package/dist/core/notifier-file.js +37 -5
- package/dist/core/scheduler.d.ts +6 -0
- package/dist/core/scheduler.js +24 -6
- package/dist/core/system-agents/templates.d.ts +53 -0
- package/dist/core/system-agents/templates.js +396 -0
- package/dist/core/system-tools/schedule.js +2 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/web/agents-config-page.js +25 -7
- package/dist/web/agents-list-page.js +2 -2
- package/dist/web/console-shell.js +2 -1
- package/dist/web/dev-ui-styles.js +1 -0
- package/dist/web/interview-admin.d.ts +12 -0
- package/dist/web/interview-admin.js +46 -22
- package/dist/web/notifier-admin.js +11 -2
- package/dist/web/templates-admin.d.ts +93 -0
- package/dist/web/templates-admin.js +227 -0
- package/dist/web/templates-page.d.ts +1 -0
- package/dist/web/templates-page.js +155 -0
- package/package.json +3 -1
|
@@ -641,7 +641,7 @@ ${consoleShellScript}
|
|
|
641
641
|
var messagingDetail = [];
|
|
642
642
|
if (pending.length) messagingDetail.push(pending.length + ' waiting for a decision');
|
|
643
643
|
if (n && connected && n.channel === 'slack' && n.plan !== 'free' && (!n.file || n.file.conversations !== false)) messagingDetail.push('answers replies in threads');
|
|
644
|
-
if (n && !connected) messagingDetail.push(n.missingSecret ? 'Saved, but its secret isn' + String.fromCharCode(8217) + 't set' : 'Send approvals and questions to Slack, Lark or a webhook');
|
|
644
|
+
if (n && !connected) messagingDetail.push(n.missingSecret ? 'Saved, but its secret isn' + String.fromCharCode(8217) + 't set' : 'Send approvals and questions to Slack, Lark, email or a webhook');
|
|
645
645
|
if (!messagingDetail.length && n) messagingDetail.push('Sends ' + escapeHtml((n.events || []).join(', ')));
|
|
646
646
|
set('ovMessagingDetail', messagingDetail.join(' \u00b7 '));
|
|
647
647
|
showPendingCount(pending.length);
|
|
@@ -1076,7 +1076,7 @@ ${consoleShellScript}
|
|
|
1076
1076
|
if (n.source === 'file') return 'Sending to <strong>' + escapeHtml(n.channel) + '</strong> for: ' + escapeHtml((n.events || []).join(', ')) + '.';
|
|
1077
1077
|
if (n.missingSecret) return 'Saved, but not sending yet: set the secret below (<code>' + escapeHtml(n.missingSecret) + '</code>).';
|
|
1078
1078
|
if (n.source === 'default') return 'Using the project-wide default webhook (<code>LOOPENGINE_DEFAULT_WEBHOOK_URL</code>, ' + escapeHtml(n.defaultWebhookUrl) + '). Set one below to use a different target for this agent.';
|
|
1079
|
-
return '<strong>Nowhere yet</strong>, so nobody is notified. Choose a
|
|
1079
|
+
return '<strong>Nowhere yet</strong>, so nobody is notified. Choose a Slack channel, Lark chat, email address or webhook below.';
|
|
1080
1080
|
}
|
|
1081
1081
|
|
|
1082
1082
|
// Slack only: whether replies in a request's thread are answered. On
|
|
@@ -1114,7 +1114,7 @@ ${consoleShellScript}
|
|
|
1114
1114
|
function secretPlaceholder(isSet, missing) {
|
|
1115
1115
|
return isSet ? 'Saved ' + String.fromCharCode(8212) + ' type a new one only to replace it' : missing;
|
|
1116
1116
|
}
|
|
1117
|
-
var SECRET_HINTS = { slack: 'xoxb-' + String.fromCharCode(8230) + ' from OAuth & Permissions', webhook: 'A long random string, or Generate', lark: 'App Secret from the Lark developer console' };
|
|
1117
|
+
var SECRET_HINTS = { slack: 'xoxb-' + String.fromCharCode(8230) + ' from OAuth & Permissions', webhook: 'A long random string, or Generate', lark: 'App Secret from the Lark developer console', email: 'An API key from your email provider' };
|
|
1118
1118
|
function secretField(channel, label) {
|
|
1119
1119
|
var isSet = n.secretsSet && n.secretsSet[channel];
|
|
1120
1120
|
return '<label>' + secretHead(label, isSet) + '<span class="hint">(stored in this agent's env as ' + escapeHtml(n.secretVars[channel]) + ')</span>' +
|
|
@@ -1123,7 +1123,7 @@ ${consoleShellScript}
|
|
|
1123
1123
|
}
|
|
1124
1124
|
return '<form class="add-source" id="notifierForm" style="max-width:560px">' +
|
|
1125
1125
|
'<label>Send to<select name="channel" id="notifierChannel">' +
|
|
1126
|
-
['webhook', 'slack', 'lark'].map(function (c) { return '<option value="' + c + '"' + (f.channel === c ? ' selected' : '') + '>' + { webhook: 'Webhook (your app)', slack: 'Slack channel', lark: 'Lark / Feishu chat' }[c] + '</option>'; }).join('') +
|
|
1126
|
+
['webhook', 'slack', 'lark', 'email'].map(function (c) { return '<option value="' + c + '"' + (f.channel === c ? ' selected' : '') + '>' + { webhook: 'Webhook (your app)', slack: 'Slack channel', lark: 'Lark / Feishu chat', email: 'Email' }[c] + '</option>'; }).join('') +
|
|
1127
1127
|
'</select></label>' +
|
|
1128
1128
|
field('webhook', '<label>Webhook URL<input type="url" name="url" placeholder="https://your-app.example.com/hooks/loopengine" value="' + escapeHtml(f.url || '') + '"></label>' + secretField('webhook', 'Signing secret') +
|
|
1129
1129
|
'<p class="hint" style="margin:0">Your app checks the <code>X-Actauth-Signature</code> header (HMAC-SHA256 of the body with the signing secret). Each payload carries a <code>decideUrl</code> it can simply forward to a person, or it can decide itself by sending <code>{"decision":"approve"}</code> or <code>{"decision":"deny"}</code> to <code>resolveUrl</code>.</p>') +
|
|
@@ -1149,6 +1149,14 @@ ${consoleShellScript}
|
|
|
1149
1149
|
renderConversationsField(n, f) +
|
|
1150
1150
|
'<p class="hint" style="margin:0" id="slackEventsNote"></p>') +
|
|
1151
1151
|
field('lark', '<label>App ID<input name="appId" value="' + escapeHtml(f.appId || '') + '"></label><label>Chat ID<input name="chatId" value="' + escapeHtml(f.chatId || '') + '"></label>' + secretField('lark', 'App secret')) +
|
|
1152
|
+
field('email',
|
|
1153
|
+
'<label>Email addresses <span class="hint">(who gets the requests; separate several with commas)</span><input name="emailTo" placeholder="ops@example.com, lead@example.com" value="' + escapeHtml(f.to || '') + '"></label>' +
|
|
1154
|
+
'<label>Sent by <span class="hint">(your email provider must allow sending from this address)</span><input name="emailFrom" placeholder="LoopEngine <approvals@example.com>" value="' + escapeHtml(f.from || '') + '"></label>' +
|
|
1155
|
+
'<label>Email provider<select name="emailProvider">' +
|
|
1156
|
+
[['resend', 'Resend'], ['sendgrid', 'SendGrid'], ['postmark', 'Postmark']].map(function (p) { return '<option value="' + p[0] + '"' + (f.provider === p[0] ? ' selected' : '') + '>' + p[1] + '</option>'; }).join('') +
|
|
1157
|
+
'</select></label>' +
|
|
1158
|
+
secretField('email', 'API key') +
|
|
1159
|
+
'<p class="hint" style="margin:0">Each email links to a page where the person approves, denies, edits or answers, with no login. The link address below must be one they can open.</p>') +
|
|
1152
1160
|
'<label>Link address <span class="hint">(messages link here so people can approve without logging in; use an address they can reach)</span>' +
|
|
1153
1161
|
'<input type="url" name="publicUrl" id="notifierPublicUrl" value="' + escapeHtml(n.publicUrl || f.publicUrl || location.origin) + '"' + (n.publicUrl ? ' disabled title="Set by LOOPENGINE_PUBLIC_URL"' : '') + '></label>' +
|
|
1154
1162
|
'<div class="body-field">Send' +
|
|
@@ -1392,6 +1400,7 @@ ${consoleShellScript}
|
|
|
1392
1400
|
var data = new FormData(form);
|
|
1393
1401
|
var channel = data.get('channel');
|
|
1394
1402
|
var body = { channel: channel, events: data.getAll('events'), url: data.get('url'), channelId: data.get('channelId'), appId: data.get('appId'), chatId: data.get('chatId'), publicUrl: data.get('publicUrl') };
|
|
1403
|
+
if (channel === 'email') { body.to = data.get('emailTo'); body.from = data.get('emailFrom'); body.provider = data.get('emailProvider'); }
|
|
1395
1404
|
var secret = data.get('secret_' + channel);
|
|
1396
1405
|
if (secret) body.secret = secret;
|
|
1397
1406
|
if (channel === 'slack' && data.get('signingSecret')) body.signingSecret = data.get('signingSecret');
|
|
@@ -1476,7 +1485,9 @@ ${consoleShellScript}
|
|
|
1476
1485
|
};
|
|
1477
1486
|
|
|
1478
1487
|
function scheduleText(s) {
|
|
1479
|
-
|
|
1488
|
+
// A repeating task's anchor is its first run, so it says when the
|
|
1489
|
+
// later ones fall (e.g. every 1d from 9:00).
|
|
1490
|
+
return s.run_at ? 'Once, ' + new Date(s.run_at).toLocaleString() : 'Every ' + escapeHtml(s.every) + (s.anchor ? '<div class="hint">from ' + escapeHtml(new Date(s.anchor).toLocaleString()) + '</div>' : '');
|
|
1480
1491
|
}
|
|
1481
1492
|
|
|
1482
1493
|
function renderScheduleRow(t) {
|
|
@@ -1520,7 +1531,9 @@ ${consoleShellScript}
|
|
|
1520
1531
|
'<div class="body-field">When' +
|
|
1521
1532
|
'<div class="schedule-when"><input type="radio" name="when" value="every" id="whenEvery" checked><label for="whenEvery">Every</label>' +
|
|
1522
1533
|
'<input type="number" name="everyN" value="1" min="1" aria-label="How many">' +
|
|
1523
|
-
'<select name="everyUnit" aria-label="Unit"><option value="m">minutes</option><option value="h" selected>hours</option><option value="d">days</option></select
|
|
1534
|
+
'<select name="everyUnit" aria-label="Unit"><option value="m">minutes</option><option value="h" selected>hours</option><option value="d">days</option><option value="w">weeks</option></select>' +
|
|
1535
|
+
'<label for="everyStart">starting</label><input type="datetime-local" name="everyStart" id="everyStart" aria-label="First run"></div>' +
|
|
1536
|
+
'<p class="hint" style="margin:0 0 4px 22px">Optional. The first run, which sets the time of day (and the weekday, for weeks) every later run keeps, e.g. every 1 day starting tomorrow 09:00. Without it, the first run is one interval from now.</p>' +
|
|
1524
1537
|
'<div class="schedule-when"><input type="radio" name="when" value="once" id="whenOnce"><label for="whenOnce">Once at</label>' +
|
|
1525
1538
|
'<input type="datetime-local" name="runAt" aria-label="Date and time"></div>' +
|
|
1526
1539
|
'</div>' +
|
|
@@ -1565,7 +1578,12 @@ ${consoleShellScript}
|
|
|
1565
1578
|
if (!data.get('runAt')) { result.textContent = 'Pick a date and time.'; return; }
|
|
1566
1579
|
schedule = { run_at: new Date(data.get('runAt')).toISOString() };
|
|
1567
1580
|
} else {
|
|
1568
|
-
|
|
1581
|
+
// Weeks go to the scheduler as days; the start comes from the
|
|
1582
|
+
// browser's own time zone.
|
|
1583
|
+
var unit = data.get('everyUnit');
|
|
1584
|
+
var n = Number(data.get('everyN')) || 1;
|
|
1585
|
+
schedule = { every: unit === 'w' ? String(n * 7) + 'd' : String(n) + unit };
|
|
1586
|
+
if (data.get('everyStart')) schedule.start_at = new Date(data.get('everyStart')).toISOString();
|
|
1569
1587
|
}
|
|
1570
1588
|
result.textContent = 'Scheduling' + String.fromCharCode(8230);
|
|
1571
1589
|
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/schedules', {
|
|
@@ -50,13 +50,13 @@ ${consoleSidebarHtml}
|
|
|
50
50
|
<div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">☰</button>
|
|
51
51
|
<div><h1>Agents</h1><p>Every agent under agents/ in this project. Open one to chat with it or change its tools, permissions and settings.</p></div>
|
|
52
52
|
</div>
|
|
53
|
-
<div style="display:flex;gap:8px;flex-wrap:wrap"><a href="/interview" style="padding:6px 12px;border:1px solid var(--line);border-radius:6px;text-decoration:none;color:var(--ink);font-size:13px">Find your first agent</a>
|
|
53
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap"><a href="/templates" style="padding:6px 12px;border:1px solid var(--line);border-radius:6px;text-decoration:none;color:var(--ink);font-size:13px">Templates</a><a href="/interview" style="padding:6px 12px;border:1px solid var(--line);border-radius:6px;text-decoration:none;color:var(--ink);font-size:13px">Find your first agent</a>
|
|
54
54
|
<button type="button" id="newAgentBtn" class="primary">New agent</button></div>
|
|
55
55
|
</div>
|
|
56
56
|
</div>
|
|
57
57
|
<div class="le-scroll"><div class="le-page">
|
|
58
58
|
<form id="newAgentForm" style="display:none">
|
|
59
|
-
<p class="hint" style="margin:0">Not sure what to build? <a href="/interview">
|
|
59
|
+
<p class="hint" style="margin:0">Not sure what to build? <a href="/templates">Start from a template</a>, or <a href="/interview">find your first agent</a>: a short interview that suggests one and creates it.</p>
|
|
60
60
|
<h2>New agent</h2>
|
|
61
61
|
<label>Name <span class="hint">(lowercase, hyphens allowed: becomes agents/<name>/)</span>
|
|
62
62
|
<input type="text" name="name" placeholder="weather-agent" pattern="[a-z0-9]+(-[a-z0-9]+)*" required>
|
|
@@ -22,6 +22,7 @@ export const consoleSidebarHtml = `<aside class="le-sidebar" aria-label="Console
|
|
|
22
22
|
<a class="le-brand" href="/agents"><span class="le-brand-mark">${BRAND_MARK}</span><span class="le-brand-name">LoopEngine</span></a>
|
|
23
23
|
<div class="le-nav-label"><span>Agents</span><a href="/agents?new=1">+ New</a></div>
|
|
24
24
|
<ul class="le-agent-list" id="leAgentList"></ul>
|
|
25
|
+
<a class="le-find" href="/templates" data-le-nav="templates">Start from a template</a>
|
|
25
26
|
<a class="le-find" href="/interview" data-le-nav="interview">Find your first agent</a>
|
|
26
27
|
<nav class="le-foot" aria-label="Settings">
|
|
27
28
|
<a href="/config?section=models" data-le-nav="models">Models</a>
|
|
@@ -160,7 +161,7 @@ export const consoleShellScript = `<script>
|
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
var section = params.get('section');
|
|
163
|
-
var foot = page === 'config' ? (section === 'gateways' ? 'integrations' : section === 'settings' ? 'settings' : 'models') : page === 'interview'
|
|
164
|
+
var foot = page === 'config' ? (section === 'gateways' ? 'integrations' : section === 'settings' ? 'settings' : 'models') : page === 'interview' || page === 'templates' ? page : '';
|
|
164
165
|
var footLinks = document.querySelectorAll('[data-le-nav]');
|
|
165
166
|
for (var i = 0; i < footLinks.length; i++) {
|
|
166
167
|
if (footLinks[i].getAttribute('data-le-nav') === foot) footLinks[i].setAttribute('aria-current', 'page');
|
|
@@ -121,6 +121,7 @@ export const devUiCss = `
|
|
|
121
121
|
.le-agent-list { list-style: none; margin: 0; padding: 0 8px; overflow-y: auto; flex: 1; min-height: 0; }
|
|
122
122
|
.le-find { margin: 6px 16px 10px; font-size: 12.5px; font-weight: 500; color: var(--accent-ink); text-decoration: none; }
|
|
123
123
|
.le-find[aria-current="page"] { font-weight: 600; }
|
|
124
|
+
.le-find + .le-find { margin-top: -4px; }
|
|
124
125
|
.le-agent-item { display: grid; grid-template-columns: 8px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 7px 8px; border-radius: 6px; color: var(--ink); text-decoration: none; }
|
|
125
126
|
.le-agent-item:hover { background: var(--surface-2); }
|
|
126
127
|
.le-agent-item[aria-current="page"] { background: var(--accent-soft); }
|
|
@@ -52,6 +52,9 @@ export interface CandidatePreview {
|
|
|
52
52
|
/** What's still needed once the agent exists, each linked to where it's done. */
|
|
53
53
|
export declare function setupChecklist(candidate: AgentCandidate, agentName: string): ChecklistItem[];
|
|
54
54
|
export declare function previewCandidate(id: string, index: number, deps: Pick<CreateDeps, 'agentExists'>): CandidatePreview;
|
|
55
|
+
/** What creating `candidate` would make, under the first free name.
|
|
56
|
+
* Shared with templates (web/templates-admin.ts). */
|
|
57
|
+
export declare function describeCandidate(candidate: AgentCandidate, deps: Pick<CreateDeps, 'agentExists'>): CandidatePreview;
|
|
55
58
|
export interface CreatedAgent {
|
|
56
59
|
name: string;
|
|
57
60
|
checklist: ChecklistItem[];
|
|
@@ -62,6 +65,11 @@ export interface CreatedAgent {
|
|
|
62
65
|
/** Creates the agent: unmatched tools ask (a new agent's default is to
|
|
63
66
|
* deny), and a schedule, if any, is created paused until setup is done. */
|
|
64
67
|
export declare function createFromCandidate(id: string, index: number, deps: CreateDeps): Promise<CreatedAgent>;
|
|
68
|
+
/** Creates `candidate` as an agent under the first free name: the agent
|
|
69
|
+
* itself, its permission default switched to "ask", and a paused
|
|
70
|
+
* schedule when it runs on a timer. Shared with templates
|
|
71
|
+
* (web/templates-admin.ts). */
|
|
72
|
+
export declare function createAgentFromCandidate(candidate: AgentCandidate, deps: CreateDeps): Promise<CreatedAgent>;
|
|
65
73
|
/** A registered agent, as the adapter's registry has it. */
|
|
66
74
|
export interface InterviewAgentSource {
|
|
67
75
|
name: string;
|
|
@@ -80,5 +88,9 @@ export interface InterviewModel {
|
|
|
80
88
|
* agent that has one set, preferring one whose provider key is set.
|
|
81
89
|
* undefined when no agent names a model. */
|
|
82
90
|
export declare function pickInterviewModel(agents: InterviewAgentSource[]): InterviewModel | undefined;
|
|
91
|
+
/** The model a new agent should start on: the same one the interviewer
|
|
92
|
+
* would pick, without building a model call (which throws when its key
|
|
93
|
+
* isn't set). Templates use it. */
|
|
94
|
+
export declare function pickAgentModel(agents: InterviewAgentSource[]): InterviewModel['model'] | undefined;
|
|
83
95
|
/** The interviewer's read-only tools over this workspace. */
|
|
84
96
|
export declare function interviewTools(agents: InterviewAgentSource[]): InterviewTools;
|
|
@@ -140,7 +140,11 @@ export function setupChecklist(candidate, agentName) {
|
|
|
140
140
|
return items;
|
|
141
141
|
}
|
|
142
142
|
export function previewCandidate(id, index, deps) {
|
|
143
|
-
|
|
143
|
+
return describeCandidate(candidateAt(getInterview(id), index), deps);
|
|
144
|
+
}
|
|
145
|
+
/** What creating `candidate` would make, under the first free name.
|
|
146
|
+
* Shared with templates (web/templates-admin.ts). */
|
|
147
|
+
export function describeCandidate(candidate, deps) {
|
|
144
148
|
const name = freeAgentName(candidate.agent.name, deps.agentExists);
|
|
145
149
|
return {
|
|
146
150
|
name,
|
|
@@ -159,33 +163,53 @@ export async function createFromCandidate(id, index, deps) {
|
|
|
159
163
|
if (interview.created[String(index)] && deps.agentExists(interview.created[String(index)])) {
|
|
160
164
|
throw new InterviewError(`Already created as ${interview.created[String(index)]}`);
|
|
161
165
|
}
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
setDefaultDecision(name, 'ask');
|
|
165
|
-
let scheduleError;
|
|
166
|
-
if (candidate.trigger.kind === 'schedule') {
|
|
167
|
-
try {
|
|
168
|
-
const task = await createScheduledTask({
|
|
169
|
-
agent: name,
|
|
170
|
-
message: candidate.agent.scheduleMessage ?? candidate.summary,
|
|
171
|
-
schedule: { every: candidate.trigger.every ?? '1d' },
|
|
172
|
-
task_name: candidate.title,
|
|
173
|
-
});
|
|
174
|
-
await pauseScheduledTask(task.task_id);
|
|
175
|
-
}
|
|
176
|
-
catch (err) {
|
|
177
|
-
scheduleError = err instanceof Error ? err.message : String(err);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
interview.created[String(index)] = name;
|
|
166
|
+
const created = await createAgentFromCandidate(candidate, deps);
|
|
167
|
+
interview.created[String(index)] = created.name;
|
|
181
168
|
save(interview);
|
|
182
|
-
return
|
|
169
|
+
return created;
|
|
183
170
|
});
|
|
184
171
|
}
|
|
172
|
+
/** Creates `candidate` as an agent under the first free name: the agent
|
|
173
|
+
* itself, its permission default switched to "ask", and a paused
|
|
174
|
+
* schedule when it runs on a timer. Shared with templates
|
|
175
|
+
* (web/templates-admin.ts). */
|
|
176
|
+
export async function createAgentFromCandidate(candidate, deps) {
|
|
177
|
+
const name = freeAgentName(candidate.agent.name, deps.agentExists);
|
|
178
|
+
await deps.createAgent(name, candidate.agent.systemPrompt);
|
|
179
|
+
setDefaultDecision(name, 'ask');
|
|
180
|
+
let scheduleError;
|
|
181
|
+
if (candidate.trigger.kind === 'schedule') {
|
|
182
|
+
try {
|
|
183
|
+
const task = await createScheduledTask({
|
|
184
|
+
agent: name,
|
|
185
|
+
message: candidate.agent.scheduleMessage ?? candidate.summary,
|
|
186
|
+
schedule: { every: candidate.trigger.every ?? '1d' },
|
|
187
|
+
task_name: candidate.title,
|
|
188
|
+
});
|
|
189
|
+
await pauseScheduledTask(task.task_id);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
scheduleError = err instanceof Error ? err.message : String(err);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { name, checklist: setupChecklist(candidate, name), ...(candidate.agent.exampleMessage ? { exampleMessage: candidate.agent.exampleMessage } : {}), ...(scheduleError ? { scheduleError } : {}) };
|
|
196
|
+
}
|
|
185
197
|
/** The interviewer runs on the workspace's own model: borrowed from an
|
|
186
198
|
* agent that has one set, preferring one whose provider key is set.
|
|
187
199
|
* undefined when no agent names a model. */
|
|
188
200
|
export function pickInterviewModel(agents) {
|
|
201
|
+
const chosen = pickModelAgent(agents);
|
|
202
|
+
if (!chosen)
|
|
203
|
+
return undefined;
|
|
204
|
+
return { modelCall: chosen.agent.createModelCall(), model: chosen.model };
|
|
205
|
+
}
|
|
206
|
+
/** The model a new agent should start on: the same one the interviewer
|
|
207
|
+
* would pick, without building a model call (which throws when its key
|
|
208
|
+
* isn't set). Templates use it. */
|
|
209
|
+
export function pickAgentModel(agents) {
|
|
210
|
+
return pickModelAgent(agents)?.model;
|
|
211
|
+
}
|
|
212
|
+
function pickModelAgent(agents) {
|
|
189
213
|
const withModel = agents.filter((a) => a.config.model && typeof a.config.model === 'object' && 'provider' in a.config.model);
|
|
190
214
|
const keyed = withModel.find((a) => {
|
|
191
215
|
const envVar = modelKeyEnvVar(a.config.model.provider);
|
|
@@ -195,7 +219,7 @@ export function pickInterviewModel(agents) {
|
|
|
195
219
|
if (!chosen)
|
|
196
220
|
return undefined;
|
|
197
221
|
const m = chosen.config.model;
|
|
198
|
-
return {
|
|
222
|
+
return { agent: chosen, model: { provider: m.provider, ...(m.model ? { model: m.model } : {}) } };
|
|
199
223
|
}
|
|
200
224
|
/** The interviewer's read-only tools over this workspace. */
|
|
201
225
|
export function interviewTools(agents) {
|
|
@@ -143,7 +143,7 @@ function activeNotifierTarget(config, defaultWebhook) {
|
|
|
143
143
|
const dir = agentDirFor(config);
|
|
144
144
|
const file = readNotifierFile(dir);
|
|
145
145
|
if (file) {
|
|
146
|
-
const { config: fromFile } = notifierConfigFromFile(file, createAgentEnv(config.name, dir));
|
|
146
|
+
const { config: fromFile } = notifierConfigFromFile(file, createAgentEnv(config.name, dir), config.name);
|
|
147
147
|
if (fromFile)
|
|
148
148
|
return fromFile;
|
|
149
149
|
}
|
|
@@ -185,7 +185,7 @@ export async function testAgentNotifier(config, defaultWebhook) {
|
|
|
185
185
|
const dir = agentDirFor(config);
|
|
186
186
|
const file = readNotifierFile(dir);
|
|
187
187
|
if (file) {
|
|
188
|
-
const { config: fromFile, missingSecret } = notifierConfigFromFile(file, createAgentEnv(config.name, dir));
|
|
188
|
+
const { config: fromFile, missingSecret } = notifierConfigFromFile(file, createAgentEnv(config.name, dir), config.name);
|
|
189
189
|
if (!fromFile)
|
|
190
190
|
return { ok: false, channel: file.channel, error: `${missingSecret} is not set for this agent` };
|
|
191
191
|
target = fromFile;
|
|
@@ -202,6 +202,15 @@ export async function testAgentNotifier(config, defaultWebhook) {
|
|
|
202
202
|
if (target.channel === 'slack') {
|
|
203
203
|
return { channel: 'slack', ...(await sendTestSlackMessage(target.config.botToken, target.config.channelId, `Test from LoopEngine: approvals for ${config.name} will be posted here.`)) };
|
|
204
204
|
}
|
|
205
|
+
if (target.channel === 'email') {
|
|
206
|
+
try {
|
|
207
|
+
await target.config.sendEmail(target.config.to, `Test from LoopEngine: ${config.name}`, `<p>This is a test. Approval requests and questions from <b>${config.name}</b> will be emailed here, each with a link to approve, deny or answer.</p>`, { kind: 'test' });
|
|
208
|
+
return { channel: 'email', ok: true };
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
return { channel: 'email', ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
205
214
|
return { ok: false, channel: target.channel, error: `Send test isn't supported for ${target.channel} yet` };
|
|
206
215
|
}
|
|
207
216
|
/** Every decision still waiting for this agent, oldest first, each with
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type StarterTemplate, type TemplateSchedule } from '../core/system-agents/templates.js';
|
|
2
|
+
export declare class TemplateError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export interface TemplateCreateDeps {
|
|
5
|
+
agentExists(name: string): boolean;
|
|
6
|
+
/** Writes agents/<name>/index.ts, as Agents → + New does, without loading it. */
|
|
7
|
+
scaffoldAgent(name: string, systemPrompt: string): Promise<void>;
|
|
8
|
+
/** Loads the finished agent into the running server. */
|
|
9
|
+
registerAgent(name: string): Promise<void>;
|
|
10
|
+
/** Installs an ability (an npm spec) into the agent. Absent where
|
|
11
|
+
* installing isn't allowed (no admin login): the abilities then head the
|
|
12
|
+
* checklist instead. */
|
|
13
|
+
installAbility?: (name: string, spec: string) => Promise<void>;
|
|
14
|
+
/** A setting as the agent sees it (its own, else the shared one). */
|
|
15
|
+
envValue(name: string, variable: string): string | undefined;
|
|
16
|
+
/** Whether approval requests from the agent reach anyone. */
|
|
17
|
+
messagingConfigured(name: string): boolean;
|
|
18
|
+
}
|
|
19
|
+
/** What a project's adapters/http.ts from before full template setups
|
|
20
|
+
* (create-loopengine 0.1.74) passes: one call that writes and loads the
|
|
21
|
+
* agent. Still accepted, so upgrading loopengine alone doesn't break the
|
|
22
|
+
* Templates page; it just can't install abilities before the agent loads. */
|
|
23
|
+
export interface LegacyTemplateCreateDeps {
|
|
24
|
+
agentExists(name: string): boolean;
|
|
25
|
+
createAgent(name: string, systemPrompt: string): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export interface TemplateChecklistItem {
|
|
28
|
+
label: string;
|
|
29
|
+
href?: string;
|
|
30
|
+
}
|
|
31
|
+
/** A template as the page's cards show it. */
|
|
32
|
+
export interface TemplateSummary {
|
|
33
|
+
id: string;
|
|
34
|
+
category: string;
|
|
35
|
+
title: string;
|
|
36
|
+
summary: string;
|
|
37
|
+
trigger: string;
|
|
38
|
+
asksFirst: string[];
|
|
39
|
+
abilities: string[];
|
|
40
|
+
schedule: string | null;
|
|
41
|
+
}
|
|
42
|
+
export interface TemplatePreview {
|
|
43
|
+
name: string;
|
|
44
|
+
systemPrompt: string;
|
|
45
|
+
abilities: string[];
|
|
46
|
+
skills: string[];
|
|
47
|
+
rules: {
|
|
48
|
+
tool: string;
|
|
49
|
+
decision: string;
|
|
50
|
+
}[];
|
|
51
|
+
keys: {
|
|
52
|
+
names: string[];
|
|
53
|
+
set: boolean;
|
|
54
|
+
}[];
|
|
55
|
+
settings: Record<string, string>;
|
|
56
|
+
schedule: string | null;
|
|
57
|
+
needsApprovals: boolean;
|
|
58
|
+
notes: string[];
|
|
59
|
+
}
|
|
60
|
+
export interface CreatedTemplateAgent {
|
|
61
|
+
name: string;
|
|
62
|
+
abilities: {
|
|
63
|
+
installed: string[];
|
|
64
|
+
failed: {
|
|
65
|
+
name: string;
|
|
66
|
+
error: string;
|
|
67
|
+
}[];
|
|
68
|
+
};
|
|
69
|
+
checklist: TemplateChecklistItem[];
|
|
70
|
+
exampleMessage?: string;
|
|
71
|
+
schedule?: {
|
|
72
|
+
description: string;
|
|
73
|
+
next_run_at?: string;
|
|
74
|
+
};
|
|
75
|
+
/** Set when the schedule couldn't be created; the agent still was. */
|
|
76
|
+
scheduleError?: string;
|
|
77
|
+
/** Set when the agent was written but couldn't be loaded. */
|
|
78
|
+
loadError?: string;
|
|
79
|
+
}
|
|
80
|
+
/** A failed install, in a line the checklist can show: npm's own output
|
|
81
|
+
* runs to many lines of command and log paths. */
|
|
82
|
+
export declare function installFailureReason(err: unknown): string;
|
|
83
|
+
export declare function describeTemplateSchedule(schedule: TemplateSchedule): string;
|
|
84
|
+
export declare function listTemplates(): TemplateSummary[];
|
|
85
|
+
export declare function previewTemplate(id: string, deps: Pick<TemplateCreateDeps, 'agentExists'> & Partial<Pick<TemplateCreateDeps, 'envValue'>>): TemplatePreview;
|
|
86
|
+
/** What's left once the agent exists, each linked to where it's done. */
|
|
87
|
+
export declare function templateChecklist(template: StarterTemplate, name: string, deps: Pick<TemplateCreateDeps, 'envValue' | 'messagingConfigured'>, failed?: {
|
|
88
|
+
name: string;
|
|
89
|
+
error: string;
|
|
90
|
+
}[]): TemplateChecklistItem[];
|
|
91
|
+
/** Creates a new agent from the template, with its whole setup, under the
|
|
92
|
+
* first free name, so the same template can be used more than once. */
|
|
93
|
+
export declare function createFromTemplate(id: string, deps: TemplateCreateDeps | LegacyTemplateCreateDeps): Promise<CreatedTemplateAgent>;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Backs the console's Templates page (/templates): the ready-made agents
|
|
2
|
+
// in core/system-agents/templates.ts, previewed and created with their
|
|
3
|
+
// whole setup in one go — instructions, permission rules, abilities,
|
|
4
|
+
// skills, settings and a paused schedule — before the agent first loads,
|
|
5
|
+
// so nothing needs a restart. What only the person can do (their keys,
|
|
6
|
+
// where approvals go, turning the schedule on) becomes the checklist.
|
|
7
|
+
//
|
|
8
|
+
// Every agent made from a template asks before anything not covered by a
|
|
9
|
+
// rule. Its template's rules are written before its abilities' own, so
|
|
10
|
+
// they take precedence where both name the same tool.
|
|
11
|
+
import { createScheduledTask, pauseScheduledTask } from '../core/scheduler.js';
|
|
12
|
+
import { STARTER_TEMPLATES, starterTemplate, templateScheduleStart } from '../core/system-agents/templates.js';
|
|
13
|
+
import { ActauthRuleExistsError, addActauthRule, setDefaultDecision } from './actauth-admin.js';
|
|
14
|
+
import { setAgentEnvVar } from './env-admin.js';
|
|
15
|
+
import { freeAgentName } from './interview-admin.js';
|
|
16
|
+
import { writeSkill } from './skills-admin.js';
|
|
17
|
+
export class TemplateError extends Error {
|
|
18
|
+
}
|
|
19
|
+
/** A failed install, in a line the checklist can show: npm's own output
|
|
20
|
+
* runs to many lines of command and log paths. */
|
|
21
|
+
export function installFailureReason(err) {
|
|
22
|
+
const text = err instanceof Error ? err.message : String(err);
|
|
23
|
+
if (/E404|404 Not Found|is not in this registry/i.test(text))
|
|
24
|
+
return "it isn't published on npm yet";
|
|
25
|
+
if (/ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN/.test(text))
|
|
26
|
+
return 'npm could not be reached; try again from the Abilities tab';
|
|
27
|
+
const line = text.split('\n').map((l) => l.trim()).find((l) => l && !/^Command failed:/.test(l)) ?? text.split('\n')[0];
|
|
28
|
+
return line.length > 200 ? `${line.slice(0, 200)}…` : line;
|
|
29
|
+
}
|
|
30
|
+
function templateAt(id) {
|
|
31
|
+
const template = starterTemplate(id);
|
|
32
|
+
if (!template)
|
|
33
|
+
throw new TemplateError('No such template');
|
|
34
|
+
return template;
|
|
35
|
+
}
|
|
36
|
+
const UNITS = { s: ['second', 'seconds'], m: ['minute', 'minutes'], h: ['hour', 'hours'], d: ['day', 'days'] };
|
|
37
|
+
const WEEKDAYS = ['Sundays', 'Mondays', 'Tuesdays', 'Wednesdays', 'Thursdays', 'Fridays', 'Saturdays'];
|
|
38
|
+
export function describeTemplateSchedule(schedule) {
|
|
39
|
+
if (schedule.at && schedule.weekday !== undefined && schedule.every === '7d')
|
|
40
|
+
return `${WEEKDAYS[schedule.weekday]} at ${schedule.at}`;
|
|
41
|
+
if (schedule.at && schedule.every === '1d')
|
|
42
|
+
return `daily at ${schedule.at}`;
|
|
43
|
+
const match = /^(\d+)([smhd])$/.exec(schedule.every);
|
|
44
|
+
if (!match)
|
|
45
|
+
return `every ${schedule.every}`;
|
|
46
|
+
const n = Number(match[1]);
|
|
47
|
+
const [one, many] = UNITS[match[2]];
|
|
48
|
+
return `every ${n === 1 ? one : `${n} ${many}`}${schedule.at ? `, from ${schedule.at}` : ''}`;
|
|
49
|
+
}
|
|
50
|
+
export function listTemplates() {
|
|
51
|
+
return STARTER_TEMPLATES.map(({ id, category, candidate: c, setup }) => ({
|
|
52
|
+
id,
|
|
53
|
+
category,
|
|
54
|
+
title: c.title,
|
|
55
|
+
summary: c.summary,
|
|
56
|
+
trigger: setup.schedule ? describeTemplateSchedule(setup.schedule) : c.trigger.description,
|
|
57
|
+
asksFirst: c.asksFirst,
|
|
58
|
+
abilities: setup.abilities,
|
|
59
|
+
schedule: setup.schedule ? describeTemplateSchedule(setup.schedule) : null,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
function keyGroups(template) {
|
|
63
|
+
return template.setup.keys.map((k) => (Array.isArray(k) ? k : [k]));
|
|
64
|
+
}
|
|
65
|
+
export function previewTemplate(id, deps) {
|
|
66
|
+
const template = templateAt(id);
|
|
67
|
+
const name = freeAgentName(template.candidate.agent.name, deps.agentExists);
|
|
68
|
+
const envValue = deps.envValue ?? (() => undefined);
|
|
69
|
+
const { setup } = template;
|
|
70
|
+
return {
|
|
71
|
+
name,
|
|
72
|
+
systemPrompt: template.candidate.agent.systemPrompt,
|
|
73
|
+
abilities: setup.abilities,
|
|
74
|
+
skills: (setup.skills ?? []).map((s) => s.id),
|
|
75
|
+
rules: setup.rules.map((r) => ({ tool: r.tool, decision: r.decision })),
|
|
76
|
+
// A key set for the whole project already counts for a new agent.
|
|
77
|
+
keys: keyGroups(template).map((names) => ({ names, set: names.some((n) => Boolean(envValue(name, n))) })),
|
|
78
|
+
settings: setup.env ?? {},
|
|
79
|
+
schedule: setup.schedule ? `${describeTemplateSchedule(setup.schedule)} (created paused)` : null,
|
|
80
|
+
needsApprovals: setup.needsApprovals,
|
|
81
|
+
notes: setup.notes ?? [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** What's left once the agent exists, each linked to where it's done. */
|
|
85
|
+
export function templateChecklist(template, name, deps, failed = []) {
|
|
86
|
+
const tab = (t) => `/agents/config?agent=${encodeURIComponent(name)}&tab=${t}`;
|
|
87
|
+
const items = [];
|
|
88
|
+
for (const f of failed)
|
|
89
|
+
items.push({ label: `Install the ${f.name} ability (it couldn't be installed: ${f.error})`, href: tab('abilities') });
|
|
90
|
+
const missing = keyGroups(template).filter((names) => !names.some((n) => deps.envValue(name, n)));
|
|
91
|
+
const single = missing.filter((g) => g.length === 1).map((g) => g[0]);
|
|
92
|
+
if (single.length)
|
|
93
|
+
items.push({ label: `Add ${single.length === 1 ? 'its key' : 'its keys'}: ${single.join(', ')}`, href: tab('env') });
|
|
94
|
+
for (const group of missing.filter((g) => g.length > 1)) {
|
|
95
|
+
items.push({ label: `Add one of ${group.slice(0, -1).join(', ')} or ${group.at(-1)}`, href: tab('env') });
|
|
96
|
+
}
|
|
97
|
+
if (template.setup.needsApprovals && !deps.messagingConfigured(name)) {
|
|
98
|
+
items.push({ label: 'Choose where approval requests go: Slack, email, Lark or a webhook', href: tab('messaging') });
|
|
99
|
+
}
|
|
100
|
+
for (const note of template.setup.notes ?? [])
|
|
101
|
+
items.push({ label: note });
|
|
102
|
+
if (template.setup.schedule) {
|
|
103
|
+
items.push({ label: `Turn on its schedule (${describeTemplateSchedule(template.setup.schedule)}) once it works in Chat`, href: tab('schedules') });
|
|
104
|
+
}
|
|
105
|
+
return items;
|
|
106
|
+
}
|
|
107
|
+
/** Creates a new agent from the template, with its whole setup, under the
|
|
108
|
+
* first free name, so the same template can be used more than once. */
|
|
109
|
+
export async function createFromTemplate(id, deps) {
|
|
110
|
+
if (!('scaffoldAgent' in deps))
|
|
111
|
+
return createFromTemplateLegacy(id, deps);
|
|
112
|
+
const template = templateAt(id);
|
|
113
|
+
const { candidate, setup } = template;
|
|
114
|
+
const name = freeAgentName(candidate.agent.name, deps.agentExists);
|
|
115
|
+
// 1. The agent, and its permissions: everything asks unless a rule says
|
|
116
|
+
// otherwise, and the template's rules come first.
|
|
117
|
+
await deps.scaffoldAgent(name, candidate.agent.systemPrompt);
|
|
118
|
+
setDefaultDecision(name, 'ask');
|
|
119
|
+
for (const rule of setup.rules) {
|
|
120
|
+
try {
|
|
121
|
+
addActauthRule(name, { name: rule.name, scope: '*/*', tool: rule.tool, decision: rule.decision });
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (!(err instanceof ActauthRuleExistsError))
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// 2. Abilities: their tools, skills and rules. One failing doesn't stop
|
|
129
|
+
// the rest; it heads the checklist.
|
|
130
|
+
const installed = [];
|
|
131
|
+
const failed = [];
|
|
132
|
+
for (const spec of setup.abilities) {
|
|
133
|
+
if (!deps.installAbility) {
|
|
134
|
+
failed.push({ name: spec, error: 'installing abilities needs LOOPENGINE_ADMIN_AUTH set on the server' });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await deps.installAbility(name, spec);
|
|
139
|
+
installed.push(spec);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
failed.push({ name: spec, error: installFailureReason(err) });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// 3. Its own skills and non-secret settings.
|
|
146
|
+
for (const skill of setup.skills ?? [])
|
|
147
|
+
writeSkill(name, skill.id, { description: skill.description, body: skill.body });
|
|
148
|
+
for (const [variable, value] of Object.entries(setup.env ?? {}))
|
|
149
|
+
await setAgentEnvVar(name, variable, value);
|
|
150
|
+
// 4. Load it, now that everything is in place.
|
|
151
|
+
let loadError;
|
|
152
|
+
try {
|
|
153
|
+
await deps.registerAgent(name);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
loadError = err instanceof Error ? err.message : String(err);
|
|
157
|
+
}
|
|
158
|
+
// 5. Its schedule, paused until the person turns it on.
|
|
159
|
+
let schedule;
|
|
160
|
+
let scheduleError;
|
|
161
|
+
if (setup.schedule) {
|
|
162
|
+
try {
|
|
163
|
+
const startAt = templateScheduleStart(setup.schedule);
|
|
164
|
+
const task = await createScheduledTask({
|
|
165
|
+
agent: name,
|
|
166
|
+
message: setup.schedule.message,
|
|
167
|
+
schedule: { every: setup.schedule.every, ...(startAt ? { start_at: startAt } : {}) },
|
|
168
|
+
task_name: setup.schedule.name,
|
|
169
|
+
});
|
|
170
|
+
await pauseScheduledTask(task.task_id);
|
|
171
|
+
schedule = { description: describeTemplateSchedule(setup.schedule), ...(startAt ? { next_run_at: startAt } : {}) };
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
scheduleError = err instanceof Error ? err.message : String(err);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
name,
|
|
179
|
+
abilities: { installed, failed },
|
|
180
|
+
checklist: templateChecklist(template, name, deps, failed),
|
|
181
|
+
...(candidate.agent.exampleMessage ? { exampleMessage: candidate.agent.exampleMessage } : {}),
|
|
182
|
+
...(schedule ? { schedule } : {}),
|
|
183
|
+
...(scheduleError ? { scheduleError } : {}),
|
|
184
|
+
...(loadError ? { loadError } : {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** An older project adapter's create: the agent with its instructions,
|
|
188
|
+
* permissions and paused schedule, as before; its abilities head the
|
|
189
|
+
* checklist with how to get them installed automatically. */
|
|
190
|
+
async function createFromTemplateLegacy(id, deps) {
|
|
191
|
+
const template = templateAt(id);
|
|
192
|
+
const { candidate, setup } = template;
|
|
193
|
+
const name = freeAgentName(candidate.agent.name, deps.agentExists);
|
|
194
|
+
await deps.createAgent(name, candidate.agent.systemPrompt);
|
|
195
|
+
setDefaultDecision(name, 'ask');
|
|
196
|
+
for (const rule of setup.rules) {
|
|
197
|
+
try {
|
|
198
|
+
addActauthRule(name, { name: rule.name, scope: '*/*', tool: rule.tool, decision: rule.decision });
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
if (!(err instanceof ActauthRuleExistsError))
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const failed = setup.abilities.map((spec) => ({ name: spec, error: "this project's server is from an older LoopEngine template; update it from Settings → Upgrade to have templates install abilities" }));
|
|
206
|
+
let schedule;
|
|
207
|
+
let scheduleError;
|
|
208
|
+
if (setup.schedule) {
|
|
209
|
+
try {
|
|
210
|
+
const startAt = templateScheduleStart(setup.schedule);
|
|
211
|
+
const task = await createScheduledTask({ agent: name, message: setup.schedule.message, schedule: { every: setup.schedule.every, ...(startAt ? { start_at: startAt } : {}) }, task_name: setup.schedule.name });
|
|
212
|
+
await pauseScheduledTask(task.task_id);
|
|
213
|
+
schedule = { description: describeTemplateSchedule(setup.schedule), ...(startAt ? { next_run_at: startAt } : {}) };
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
scheduleError = err instanceof Error ? err.message : String(err);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
name,
|
|
221
|
+
abilities: { installed: [], failed },
|
|
222
|
+
checklist: templateChecklist(template, name, { envValue: () => undefined, messagingConfigured: () => false }, failed),
|
|
223
|
+
...(candidate.agent.exampleMessage ? { exampleMessage: candidate.agent.exampleMessage } : {}),
|
|
224
|
+
...(schedule ? { schedule } : {}),
|
|
225
|
+
...(scheduleError ? { scheduleError } : {}),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const templatesPageHtml: string;
|