loopengine 0.1.91 → 0.1.92

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 CHANGED
@@ -205,6 +205,14 @@ first, and a paused schedule if it runs on a timer, plus a checklist of
205
205
  what's left to connect. The interviewer runs on the model of one of your
206
206
  agents, can't see your systems, and creates nothing until you click.
207
207
 
208
+ Or skip the interview: **Templates** (`/templates`, linked from the
209
+ sidebar and the Agents page) are ready-made agents: answer customer
210
+ emails, triage incidents, check expense claims, and a weekly research
211
+ digest. Preview one's instructions and setup checklist, then create it
212
+ the same way, as many times as you like. Templates live in
213
+ `core/system-agents/templates.ts`, in the same shape the interview
214
+ suggests.
215
+
208
216
  Every notification links to a **decision page**: a signed, single-use
209
217
  link (`/decide/<token>`, valid 7 days) where whoever got the message can
210
218
  approve, deny, edit the arguments or answer a question, with no login and
package/dist/bin/cli.js CHANGED
@@ -37,9 +37,10 @@ const MODEL_ENV_VAR = {
37
37
  // literal in generated code — an operator-supplied systemPrompt (from
38
38
  // the admin UI's Create new agent form, say) containing a literal quote
39
39
  // or backslash would otherwise produce invalid, or silently different,
40
- // generated TypeScript.
40
+ // generated TypeScript. Line breaks too: a multi-line prompt (from a
41
+ // template or the interview) would otherwise end the literal mid-line.
41
42
  function tsStringLiteral(value) {
42
- return "'" + value.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'";
43
+ return "'" + value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\r/g, '\\r').replace(/\n/g, '\\n') + "'";
43
44
  }
44
45
  /** 'loopengine' for a real consumer project — the only thing that ever
45
46
  * resolves once this package is genuinely installed as a dependency. A
@@ -0,0 +1,10 @@
1
+ import type { AgentCandidate } from './interviewer.js';
2
+ export interface StarterTemplate {
3
+ /** Stable id, used in URLs: lowercase-hyphenated. */
4
+ id: string;
5
+ /** Groups templates on the page: "Customer service", "Operations"… */
6
+ category: string;
7
+ candidate: AgentCandidate;
8
+ }
9
+ export declare const STARTER_TEMPLATES: readonly StarterTemplate[];
10
+ export declare function starterTemplate(id: string): StarterTemplate | undefined;
@@ -0,0 +1,146 @@
1
+ export const STARTER_TEMPLATES = [
2
+ {
3
+ id: 'customer-email-replies',
4
+ category: 'Customer service',
5
+ candidate: {
6
+ title: 'Answer customer emails',
7
+ summary: 'Reads each new customer email, looks up what it needs, and drafts a reply. Routine answers go out once you allow them; refunds and anything unusual wait for you.',
8
+ trigger: { kind: 'event', description: 'each new customer email' },
9
+ hoursPerWeek: 5,
10
+ fit: 7,
11
+ risk: 'medium',
12
+ asksFirst: ['sending a reply', 'refunds', 'discounts'],
13
+ needs: [
14
+ { kind: 'api', name: 'Email', description: 'your support inbox' },
15
+ { kind: 'ability', name: 'lp-docs-search', description: 'your help articles and policies, so replies follow them' },
16
+ { kind: 'messaging', name: 'Slack', description: 'Slack, to approve replies and refunds' },
17
+ ],
18
+ agent: {
19
+ name: 'customer-email-replies',
20
+ systemPrompt: `You answer customer emails for our business.
21
+
22
+ For each email:
23
+ 1. Work out what the customer wants: order status, a product question, a return or refund, a complaint, or something else.
24
+ 2. Look up what you need before answering, such as the order, its shipping status, or our policies. Never guess an order detail, a date or a policy.
25
+ 3. Draft a short, friendly reply in the customer's language. Use their name, answer the question first, and say what happens next.
26
+ 4. Send it with the email tool.
27
+
28
+ Rules:
29
+ - Refunds, discounts, replacements and anything that costs money: draft it, explain your reasoning in one line, and let a person approve it.
30
+ - If the customer is angry, mentions a lawyer, a chargeback or a safety problem, don't promise anything; draft a calm reply for a person to review.
31
+ - If you can't find the order or the answer, say so in the draft rather than inventing one.
32
+ - Never share another customer's information.`,
33
+ exampleMessage: 'New email from Sam Lee: "Hi, my order #1042 still hasn\'t arrived and it\'s been 10 days. Where is it?"',
34
+ },
35
+ },
36
+ },
37
+ {
38
+ id: 'incident-triage',
39
+ category: 'Operations',
40
+ candidate: {
41
+ title: 'Triage incidents',
42
+ summary: 'Takes an alert or a bug report, works out what broke and how bad it is, finds related recent changes, and proposes the next step for the on-call person to approve.',
43
+ trigger: { kind: 'on_request', description: 'when an alert or report is passed to it in Chat or Slack' },
44
+ hoursPerWeek: 3,
45
+ fit: 6,
46
+ risk: 'medium',
47
+ asksFirst: ['opening or changing issues', 'posting to channels', 'any action on a system'],
48
+ needs: [
49
+ { kind: 'api', name: 'GitHub', description: 'GitHub or your issue tracker' },
50
+ { kind: 'messaging', name: 'Slack', description: 'Slack, so on-call can approve from the alert thread' },
51
+ ],
52
+ agent: {
53
+ name: 'incident-triage',
54
+ systemPrompt: `You help the on-call engineer triage incidents.
55
+
56
+ When you get an alert, error or bug report:
57
+ 1. Summarise it in two sentences: what is failing and who is affected.
58
+ 2. Rate the severity: SEV1 (customers can't use the product), SEV2 (a major feature is broken), SEV3 (degraded or a workaround exists), SEV4 (minor).
59
+ 3. Look for likely causes: recent deploys, merged changes and open issues touching the same area. List the top two or three with links.
60
+ 4. Propose one next step: who should look, what to check first, and whether to roll back.
61
+
62
+ Rules:
63
+ - Only read on your own. Opening or editing an issue, posting to a channel, paging someone or any rollback needs a person's approval.
64
+ - Say how sure you are. If the evidence is thin, say what would confirm it instead of guessing.
65
+ - Keep the summary short enough to read on a phone.`,
66
+ exampleMessage: 'Alert: checkout-api error rate 18% (normally under 1%) since 14:05 UTC. Errors: "timeout connecting to payments".',
67
+ },
68
+ },
69
+ },
70
+ {
71
+ id: 'expense-approvals',
72
+ category: 'Finance',
73
+ candidate: {
74
+ title: 'Check expense claims',
75
+ summary: 'Reads each expense claim and its receipt, checks it against your policy, and routes it: claims within policy are prepared for approval, anything outside policy is flagged with the reason.',
76
+ trigger: { kind: 'event', description: 'each new expense claim' },
77
+ hoursPerWeek: 3,
78
+ fit: 7,
79
+ risk: 'high',
80
+ asksFirst: ['approving a claim', 'paying anything out', 'replying to the claimant'],
81
+ needs: [
82
+ { kind: 'api', name: 'Email', description: 'the inbox or form where claims arrive' },
83
+ { kind: 'ability', name: 'lp-docs-search', description: 'your expense policy, so checks follow it' },
84
+ { kind: 'messaging', name: 'Slack', description: 'Slack, so approvers decide in one click' },
85
+ ],
86
+ agent: {
87
+ name: 'expense-approvals',
88
+ systemPrompt: `You check expense claims against our expense policy.
89
+
90
+ For each claim:
91
+ 1. Read the amount, currency, date, merchant, category and who submitted it, from the claim and its receipt.
92
+ 2. Check it against the policy: the category is allowed, it's under the limit, the receipt matches the amount and date, and it isn't a duplicate of an earlier claim.
93
+ 3. Write a one-line verdict: "Within policy" or "Outside policy: <reason>", followed by the details you checked.
94
+ 4. Send the verdict to the approver. Within-policy claims are prepared for approval; outside-policy ones are flagged.
95
+
96
+ Rules:
97
+ - Never approve or pay a claim yourself. A person always decides.
98
+ - If the receipt is missing or unreadable, say so and ask the claimant for it; don't fill in values.
99
+ - If the policy doesn't cover the case, say which part is unclear rather than deciding.`,
100
+ exampleMessage: 'New claim from Priya: client dinner, Bistro 21, 2026-10-20, $186.40 for 3 people. Receipt attached.',
101
+ },
102
+ },
103
+ },
104
+ {
105
+ id: 'weekly-research-digest',
106
+ category: 'Research',
107
+ candidate: {
108
+ title: 'Weekly research digest',
109
+ summary: 'Once a week, searches the web for news on the topics you choose and writes a short digest with links.',
110
+ trigger: { kind: 'schedule', description: 'once a week', every: '7d' },
111
+ hoursPerWeek: 2,
112
+ fit: 6,
113
+ risk: 'low',
114
+ asksFirst: ['sending the digest to anyone'],
115
+ needs: [
116
+ { kind: 'ability', name: 'lp-web-search', description: 'web search' },
117
+ { kind: 'key', name: 'TAVILY_API_KEY', description: 'a web search key (Tavily, Brave or Firecrawl)' },
118
+ ],
119
+ agent: {
120
+ name: 'weekly-research-digest',
121
+ systemPrompt: `You write a weekly research digest.
122
+
123
+ Topics to follow (edit this list):
124
+ - [Your industry or market]
125
+ - [Competitors, by name]
126
+ - [Technologies or regulations you care about]
127
+
128
+ Each week:
129
+ 1. Search the web for what happened in the last 7 days on each topic.
130
+ 2. Open the most relevant pages and read them; don't rely on search snippets.
131
+ 3. Write the digest: for each topic, up to three items, each with a one-sentence summary, why it matters to us, and the link.
132
+ 4. End with "Worth a closer look": the single most important item, and why.
133
+
134
+ Rules:
135
+ - Only include things published in the last 7 days, and give each item's date.
136
+ - Prefer original sources over aggregators. Never invent a link.
137
+ - If a topic had no real news, say so in one line.`,
138
+ scheduleMessage: 'Write this week\'s research digest.',
139
+ exampleMessage: 'Write this week\'s research digest.',
140
+ },
141
+ },
142
+ },
143
+ ];
144
+ export function starterTemplate(id) {
145
+ return STARTER_TEMPLATES.find((t) => t.id === id);
146
+ }
package/dist/index.d.ts CHANGED
@@ -71,8 +71,11 @@ export type { HttpToolSpec, HttpToolField } from './web/http-tool-admin.js';
71
71
  export { agentsConfigPageHtml } from './web/agents-config-page.js';
72
72
  export { agentsListPageHtml } from './web/agents-list-page.js';
73
73
  export { interviewPageHtml } from './web/interview-page.js';
74
- export { InterviewError, answerInterview, createFromCandidate, deleteInterview, finishInterview, getInterview, interviewTools, listInterviews, pickInterviewModel, previewCandidate, startInterview, setInterviewProject, setupChecklist, type Interview, type InterviewAgentSource, type CandidatePreview, type CreatedAgent, type ChecklistItem, } from './web/interview-admin.js';
74
+ export { InterviewError, answerInterview, createFromCandidate, createAgentFromCandidate, describeCandidate, deleteInterview, finishInterview, getInterview, interviewTools, listInterviews, pickAgentModel, pickInterviewModel, previewCandidate, startInterview, setInterviewProject, setupChecklist, type Interview, type InterviewAgentSource, type CandidatePreview, type CreatedAgent, type ChecklistItem, type CreateDeps, } from './web/interview-admin.js';
75
75
  export { runInterviewTurn, rankCandidates, validateCandidate, INTERVIEWER_PROMPT, type AgentCandidate, type InterviewStage } from './core/system-agents/interviewer.js';
76
+ export { templatesPageHtml } from './web/templates-page.js';
77
+ export { TemplateError, createFromTemplate, listTemplates, previewTemplate, type TemplateSummary } from './web/templates-admin.js';
78
+ export { STARTER_TEMPLATES, starterTemplate, type StarterTemplate } from './core/system-agents/templates.js';
76
79
  export { globalConfigPageHtml } from './web/global-config-page.js';
77
80
  export { playgroundHtml } from './web/playground.js';
78
81
  export { readSkill, writeSkill, deleteSkill, SkillInvalidIdError, SkillNotFoundError } from './web/skills-admin.js';
package/dist/index.js CHANGED
@@ -66,8 +66,11 @@ export { createHttpTool, updateHttpTool, readHttpToolSpec, listEditableHttpToolN
66
66
  export { agentsConfigPageHtml } from './web/agents-config-page.js';
67
67
  export { agentsListPageHtml } from './web/agents-list-page.js';
68
68
  export { interviewPageHtml } from './web/interview-page.js';
69
- export { InterviewError, answerInterview, createFromCandidate, deleteInterview, finishInterview, getInterview, interviewTools, listInterviews, pickInterviewModel, previewCandidate, startInterview, setInterviewProject, setupChecklist, } from './web/interview-admin.js';
69
+ export { InterviewError, answerInterview, createFromCandidate, createAgentFromCandidate, describeCandidate, deleteInterview, finishInterview, getInterview, interviewTools, listInterviews, pickAgentModel, pickInterviewModel, previewCandidate, startInterview, setInterviewProject, setupChecklist, } from './web/interview-admin.js';
70
70
  export { runInterviewTurn, rankCandidates, validateCandidate, INTERVIEWER_PROMPT } from './core/system-agents/interviewer.js';
71
+ export { templatesPageHtml } from './web/templates-page.js';
72
+ export { TemplateError, createFromTemplate, listTemplates, previewTemplate } from './web/templates-admin.js';
73
+ export { STARTER_TEMPLATES, starterTemplate } from './core/system-agents/templates.js';
71
74
  export { globalConfigPageHtml } from './web/global-config-page.js';
72
75
  export { playgroundHtml } from './web/playground.js';
73
76
  export { readSkill, writeSkill, deleteSkill, SkillInvalidIdError, SkillNotFoundError } from './web/skills-admin.js';
@@ -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">&#9776;</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">Find your first agent</a>: a short interview that suggests one and creates it.</p>
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/&lt;name&gt;/)</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' ? '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
- const candidate = candidateAt(getInterview(id), index);
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 name = freeAgentName(candidate.agent.name, deps.agentExists);
163
- await deps.createAgent(name, candidate.agent.systemPrompt);
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 { name, checklist: setupChecklist(candidate, name), ...(candidate.agent.exampleMessage ? { exampleMessage: candidate.agent.exampleMessage } : {}), ...(scheduleError ? { scheduleError } : {}) };
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 { modelCall: chosen.createModelCall(), model: { provider: m.provider, ...(m.model ? { model: m.model } : {}) } };
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) {
@@ -0,0 +1,19 @@
1
+ import { type CandidatePreview, type CreateDeps, type CreatedAgent } from './interview-admin.js';
2
+ export declare class TemplateError extends Error {
3
+ }
4
+ /** A template as the page's cards show it. */
5
+ export interface TemplateSummary {
6
+ id: string;
7
+ category: string;
8
+ title: string;
9
+ summary: string;
10
+ trigger: string;
11
+ asksFirst: string[];
12
+ needs: string[];
13
+ schedule: boolean;
14
+ }
15
+ export declare function listTemplates(): TemplateSummary[];
16
+ export declare function previewTemplate(id: string, deps: Pick<CreateDeps, 'agentExists'>): CandidatePreview;
17
+ /** Creates a new agent from the template, under the first free name, so
18
+ * the same template can be used more than once. */
19
+ export declare function createFromTemplate(id: string, deps: CreateDeps): Promise<CreatedAgent>;
@@ -0,0 +1,37 @@
1
+ // Backs the console's Templates page (/templates): the ready-made agents
2
+ // in core/system-agents/templates.ts, previewed and created the same way
3
+ // as a "Find your first agent" suggestion (web/interview-admin.ts), so a
4
+ // template agent starts exactly like one: its instructions, every tool
5
+ // asking first, a paused schedule when it runs on a timer, and a Finish
6
+ // setup checklist. Unlike the interview it needs no model call, so it
7
+ // works before any agent has a model or key.
8
+ import { STARTER_TEMPLATES, starterTemplate } from '../core/system-agents/templates.js';
9
+ import { createAgentFromCandidate, describeCandidate } from './interview-admin.js';
10
+ export class TemplateError extends Error {
11
+ }
12
+ function templateAt(id) {
13
+ const template = starterTemplate(id);
14
+ if (!template)
15
+ throw new TemplateError('No such template');
16
+ return template;
17
+ }
18
+ export function listTemplates() {
19
+ return STARTER_TEMPLATES.map(({ id, category, candidate: c }) => ({
20
+ id,
21
+ category,
22
+ title: c.title,
23
+ summary: c.summary,
24
+ trigger: c.trigger.description,
25
+ asksFirst: c.asksFirst,
26
+ needs: c.needs.map((n) => (n.kind === 'key' ? n.description : n.name ?? n.description)),
27
+ schedule: c.trigger.kind === 'schedule',
28
+ }));
29
+ }
30
+ export function previewTemplate(id, deps) {
31
+ return describeCandidate(templateAt(id).candidate, deps);
32
+ }
33
+ /** Creates a new agent from the template, under the first free name, so
34
+ * the same template can be used more than once. */
35
+ export async function createFromTemplate(id, deps) {
36
+ return createAgentFromCandidate(templateAt(id).candidate, deps);
37
+ }
@@ -0,0 +1 @@
1
+ export declare const templatesPageHtml: string;
@@ -0,0 +1,130 @@
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';
9
+ export const templatesPageHtml = `<!doctype html>
10
+ <html lang="en">
11
+ <head>
12
+ <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>
29
+ </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
+ function cardHtml(t) {
67
+ return '<div class="tp-card" data-id="' + esc(t.id) + '">' +
68
+ '<span class="tp-cat">' + esc(t.category) + '</span>' +
69
+ '<h3>' + esc(t.title) + '</h3>' +
70
+ '<div>' + esc(t.summary) + '</div>' +
71
+ '<div class="meta">' +
72
+ '<div><strong>Starts:</strong> ' + esc(t.trigger) + '</div>' +
73
+ (t.asksFirst.length ? '<div><strong>Asks you first:</strong> ' + esc(t.asksFirst.join(', ')) + '</div>' : '') +
74
+ (t.needs.length ? '<div><strong>Needs:</strong> ' + esc(t.needs.join(', ')) + '</div>' : '') +
75
+ '</div>' +
76
+ '<div class="row"><button type="button" class="primary tp-use">Use this template</button></div>' +
77
+ '<div class="tp-slot"></div></div>';
78
+ }
79
+
80
+ function show() {
81
+ api('GET', '/starter-templates').then(function (r) {
82
+ if (!r.ok) { page.innerHTML = '<p class="error">' + esc(r.body.error || 'Could not load the templates') + '</p>'; return; }
83
+ page.innerHTML = '<div class="tp-grid">' + r.body.templates.map(cardHtml).join('') + '</div>' +
84
+ '<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>';
85
+ Array.prototype.forEach.call(page.querySelectorAll('.tp-use'), function (btn) {
86
+ btn.addEventListener('click', function () { preview(btn.closest('.tp-card')); });
87
+ });
88
+ });
89
+ }
90
+
91
+ function preview(card) {
92
+ var id = card.getAttribute('data-id');
93
+ var slot = card.querySelector('.tp-slot');
94
+ slot.innerHTML = '<p class="hint">Loading' + ELLIPSIS + '</p>';
95
+ api('GET', '/starter-templates/' + encodeURIComponent(id) + '/preview').then(function (r) {
96
+ if (!r.ok) { slot.innerHTML = '<p class="error">' + esc(r.body.error || 'Could not prepare it') + '</p>'; return; }
97
+ var p = r.body;
98
+ slot.innerHTML = '<div class="tp-preview">' +
99
+ '<div><strong>Agent:</strong> <code>' + esc(p.name) + '</code></div>' +
100
+ '<div><strong>Instructions</strong> <span class="hint">(edit them later in its Overview tab)</span><pre>' + esc(p.systemPrompt) + '</pre></div>' +
101
+ '<div><strong>Permissions:</strong> ' + esc(p.permissions) + '</div>' +
102
+ (p.schedule ? '<div><strong>Schedule:</strong> ' + esc(p.schedule) + '</div>' : '') +
103
+ '<div><strong>Then, to finish setup:</strong>' + checklistHtml(p.checklist) + '</div>' +
104
+ '<div class="row"><button type="button" class="primary tp-create">Create it</button><button type="button" class="tp-cancel">Cancel</button></div>' +
105
+ '<div class="hint tp-status"></div></div>';
106
+ slot.querySelector('.tp-cancel').addEventListener('click', function () { slot.innerHTML = ''; });
107
+ slot.querySelector('.tp-create').addEventListener('click', function (ev) {
108
+ var btn = ev.currentTarget;
109
+ btn.disabled = true;
110
+ slot.querySelector('.tp-status').textContent = 'Creating' + ELLIPSIS;
111
+ api('POST', '/starter-templates/' + encodeURIComponent(id) + '/create').then(function (r2) {
112
+ if (!r2.ok) { btn.disabled = false; slot.querySelector('.tp-status').textContent = r2.body.error || 'Could not create it'; return; }
113
+ var c = r2.body;
114
+ slot.innerHTML = '<div class="tp-preview">' +
115
+ '<div><strong>Created <code>' + esc(c.name) + '</code>.</strong> It asks you before every tool until you change that in its Permissions tab.</div>' +
116
+ (c.scheduleError ? '<div class="error">Its schedule wasn' + String.fromCharCode(8217) + 't created: ' + esc(c.scheduleError) + '. Add one in its Schedules tab.</div>' : '') +
117
+ '<div><strong>Finish setup:</strong>' + checklistHtml(c.checklist) + '</div>' +
118
+ (c.exampleMessage ? '<div><strong>Try it</strong> in Chat with: <em>' + esc(c.exampleMessage) + '</em></div>' : '') +
119
+ '<div class="row"><a class="tp-btn" href="/agents/config?agent=' + encodeURIComponent(c.name) + '">Open ' + esc(c.name) + '</a>' +
120
+ '<a class="tp-btn plain" href="' + esc(leShell.tabHref(c.name, 'chat')) + '">Try it in Chat</a></div></div>';
121
+ }).catch(function () { btn.disabled = false; slot.querySelector('.tp-status').textContent = 'Could not reach the server; try again.'; });
122
+ });
123
+ });
124
+ }
125
+
126
+ show();
127
+ })();
128
+ </script>
129
+ </body>
130
+ </html>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.91",
3
+ "version": "0.1.92",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",