loopengine 0.1.8 → 0.1.10

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/dist/index.d.ts CHANGED
@@ -33,7 +33,7 @@ export { listQuestions, answerQuestion as resolvePendingQuestion, findQuestion,
33
33
  export type { PendingQuestion } from './core/system-tools/index.js';
34
34
  export { editAgentFile, AgentEditNotSupportedError, AgentFileNotFoundError } from './web/agent-file-admin.js';
35
35
  export type { AgentEditableFields, AgentEditResult } from './web/agent-file-admin.js';
36
- export { createHttpTool, HttpToolNameError, HttpToolExistsError, HttpToolIndexShapeError } from './web/http-tool-admin.js';
36
+ export { createHttpTool, updateHttpTool, readHttpToolSpec, listEditableHttpToolNames, HttpToolNameError, HttpToolExistsError, HttpToolIndexShapeError, HttpToolNotFoundError, HttpToolNotEditableError, } from './web/http-tool-admin.js';
37
37
  export type { HttpToolSpec, HttpToolField } from './web/http-tool-admin.js';
38
38
  export { agentsConfigPageHtml } from './web/agents-config-page.js';
39
39
  export { agentsListPageHtml } from './web/agents-list-page.js';
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ export { WebhookNotifier } from './core/http-notify-triggers/webhook.js';
37
37
  // question in-process, aliased to avoid colliding with that name.
38
38
  export { listQuestions, answerQuestion as resolvePendingQuestion, findQuestion, createAskUserTool } from './core/system-tools/index.js';
39
39
  export { editAgentFile, AgentEditNotSupportedError, AgentFileNotFoundError } from './web/agent-file-admin.js';
40
- export { createHttpTool, HttpToolNameError, HttpToolExistsError, HttpToolIndexShapeError } from './web/http-tool-admin.js';
40
+ export { createHttpTool, updateHttpTool, readHttpToolSpec, listEditableHttpToolNames, HttpToolNameError, HttpToolExistsError, HttpToolIndexShapeError, HttpToolNotFoundError, HttpToolNotEditableError, } from './web/http-tool-admin.js';
41
41
  export { agentsConfigPageHtml } from './web/agents-config-page.js';
42
42
  export { agentsListPageHtml } from './web/agents-list-page.js';
43
43
  export { globalConfigPageHtml } from './web/global-config-page.js';
@@ -581,6 +581,28 @@ export const agentsConfigPageHtml = `<!doctype html>
581
581
  return '<table><thead><tr><th>Name</th><th>Description</th><th>Parallel-safe</th></tr></thead><tbody>' + rows + '</tbody></table>';
582
582
  }
583
583
 
584
+ // Local tools' own table, unlike renderTools above (shared by Agent as
585
+ // Tools, which has nothing editable through this UI) — adds an Actions
586
+ // column with an Edit button, but only for tools describeAgent flagged
587
+ // httpToolEditable: those created through this same Tools tab's HTTP
588
+ // tool builder (web/http-tool-admin.ts's own sidecar spec file is what
589
+ // that flag is actually derived from). A hand-written tools/<name>.ts
590
+ // has no spec to repopulate an edit form from, so it just gets a blank
591
+ // cell — not a button that would 404 the moment it's clicked.
592
+ function renderLocalTools(tools) {
593
+ if (!tools.length) return '<p class="muted">No tools.</p>';
594
+ var rows = tools.map(function (t) {
595
+ var actions = t.httpToolEditable
596
+ ? '<button type="button" class="edit-http-tool-btn" data-name="' + escapeHtml(t.name) + '">Edit</button>'
597
+ : '';
598
+ return '<tr><td><code>' + escapeHtml(t.name) + '</code></td>' +
599
+ '<td>' + escapeHtml(t.description) + '</td>' +
600
+ '<td>' + (t.safe ? 'yes' : 'no') + '</td>' +
601
+ '<td>' + actions + '</td></tr>';
602
+ }).join('');
603
+ return '<table><thead><tr><th>Name</th><th>Description</th><th>Parallel-safe</th><th></th></tr></thead><tbody>' + rows + '</tbody></table>';
604
+ }
605
+
584
606
  // Overview's own tools table, unlike the Tools tab's three separately-
585
607
  // headed sections (Local tools / Gateway Tools / Agent as Tools), is
586
608
  // one flat list (cfg.tools) — without a Type column there's no way to
@@ -1474,8 +1496,8 @@ export const agentsConfigPageHtml = `<!doctype html>
1474
1496
  // describeGatewayTools), a real cost not worth paying up front. ----
1475
1497
 
1476
1498
  function renderToolsTabHtml(cfg) {
1477
- return '<section><h3>Local tools (' + cfg.localTools.length + ')</h3>' + renderTools(cfg.localTools) +
1478
- '<div class="admin-subsection"><h4>Create an HTTP tool</h4>' + renderHttpToolFormHtml() + '</div>' +
1499
+ return '<section><h3>Local tools (' + cfg.localTools.length + ')</h3>' + renderLocalTools(cfg.localTools) +
1500
+ '<div class="admin-subsection"><h4 id="httpToolFormHeading">Create an HTTP tool</h4>' + renderHttpToolFormHtml() + '</div>' +
1479
1501
  '</section>' +
1480
1502
  '<section id="gatewayToolsSection">' +
1481
1503
  '<h3>Gateway Tools <button type="button" id="gatewayToolsRefreshBtn" class="hint-btn">Refresh</button></h3>' +
@@ -1521,7 +1543,7 @@ export const agentsConfigPageHtml = `<!doctype html>
1521
1543
  httpToolHeaderRowCount = 0;
1522
1544
  return '<form class="add-source" id="httpToolForm">' +
1523
1545
  '<label>Name <span class="hint">(snake_case, e.g. lookup_order_status)</span>' +
1524
- '<input type="text" name="name" required pattern="[a-z][a-z0-9_]*" placeholder="lookup_order_status">' +
1546
+ '<input type="text" name="name" id="httpToolNameInput" required pattern="[a-z][a-z0-9_]*" placeholder="lookup_order_status">' +
1525
1547
  '</label>' +
1526
1548
  '<label>Description' +
1527
1549
  '<textarea name="description" id="httpToolDescriptionInput" required placeholder="What this tool does"></textarea>' +
@@ -1547,10 +1569,11 @@ export const agentsConfigPageHtml = `<!doctype html>
1547
1569
  '<button type="button" id="httpToolAddHeaderBtn" class="hint-btn">Add header</button>' +
1548
1570
  '</label>' +
1549
1571
  '<label class="http-tool-inline-label"><input type="checkbox" name="sendFieldsAsJsonBody"> Send fields as a JSON body (POST/PUT/PATCH only)</label>' +
1572
+ '<label class="http-tool-inline-label"><input type="checkbox" name="safe"> Parallel-safe <span class="hint">(read-only, no side effects &mdash; safe to run alongside other calls)</span></label>' +
1550
1573
  '<label>Response JSON path <span class="hint">(optional &mdash; e.g. data.status; leave blank to return the raw response text)</span>' +
1551
1574
  '<input type="text" name="responseJsonPath" placeholder="data.status">' +
1552
1575
  '</label>' +
1553
- '<label>Grant permission <span class="hint">(optional &mdash; leave blank to leave this tool at actauth&#39;s own default, typically deny)</span>' +
1576
+ '<label id="httpToolDecisionField">Grant permission <span class="hint">(optional &mdash; leave blank to leave this tool at actauth&#39;s own default, typically deny)</span>' +
1554
1577
  '<select name="decision">' +
1555
1578
  '<option value="">(leave unset)</option>' +
1556
1579
  '<option value="allow">allow</option>' +
@@ -1559,6 +1582,7 @@ export const agentsConfigPageHtml = `<!doctype html>
1559
1582
  '</select>' +
1560
1583
  '</label>' +
1561
1584
  '<button type="submit" id="httpToolSubmitBtn">Create tool</button>' +
1585
+ '<button type="button" id="httpToolCancelBtn" style="display:none">Cancel</button>' +
1562
1586
  '<div id="httpToolError" class="error"></div>' +
1563
1587
  '</form>';
1564
1588
  }
@@ -1580,6 +1604,15 @@ export const agentsConfigPageHtml = `<!doctype html>
1580
1604
  var addHeaderBtn = detail.querySelector('#httpToolAddHeaderBtn');
1581
1605
  var errorEl = detail.querySelector('#httpToolError');
1582
1606
  var submitBtn = detail.querySelector('#httpToolSubmitBtn');
1607
+ var cancelBtn = detail.querySelector('#httpToolCancelBtn');
1608
+ var nameInput = detail.querySelector('#httpToolNameInput');
1609
+ var decisionField = detail.querySelector('#httpToolDecisionField');
1610
+ var headingEl = detail.querySelector('#httpToolFormHeading');
1611
+ // Non-null only while editing an existing tool — the name field is
1612
+ // disabled then (renaming isn't supported, see updateHttpTool's own
1613
+ // doc comment), so the submitted name always has to come from here,
1614
+ // never from the (excluded-from-FormData) disabled input.
1615
+ var editingHttpToolName = null;
1583
1616
 
1584
1617
  addFieldBtn.addEventListener('click', function () {
1585
1618
  fieldRows.insertAdjacentHTML('beforeend', renderHttpToolFieldRow(httpToolFieldRowCount++));
@@ -1590,6 +1623,85 @@ export const agentsConfigPageHtml = `<!doctype html>
1590
1623
  wireHttpToolRemoveButton(headerRows.lastElementChild);
1591
1624
  });
1592
1625
 
1626
+ // Repopulates the (blank, create-mode-shaped) form from a saved spec
1627
+ // — same field/header row-building addFieldBtn/addHeaderBtn already
1628
+ // do, just filled in immediately instead of left blank for typing.
1629
+ function populateHttpToolForm(spec) {
1630
+ nameInput.value = spec.name;
1631
+ form.querySelector('#httpToolDescriptionInput').value = spec.description;
1632
+
1633
+ fieldRows.innerHTML = '';
1634
+ for (var i = 0; i < spec.fields.length; i++) {
1635
+ var f = spec.fields[i];
1636
+ fieldRows.insertAdjacentHTML('beforeend', renderHttpToolFieldRow(httpToolFieldRowCount++));
1637
+ var row = fieldRows.lastElementChild;
1638
+ row.querySelector('.http-tool-field-name').value = f.name;
1639
+ row.querySelector('.http-tool-field-type').value = f.type;
1640
+ row.querySelector('.http-tool-field-description').value = f.description || '';
1641
+ row.querySelector('.http-tool-field-required').checked = f.required;
1642
+ wireHttpToolRemoveButton(row);
1643
+ }
1644
+
1645
+ form.querySelector('[name="method"]').value = spec.method;
1646
+ form.querySelector('[name="url"]').value = spec.url;
1647
+
1648
+ headerRows.innerHTML = '';
1649
+ for (var j = 0; j < spec.headers.length; j++) {
1650
+ var h = spec.headers[j];
1651
+ headerRows.insertAdjacentHTML('beforeend', renderHttpToolHeaderRow(httpToolHeaderRowCount++));
1652
+ var hrow = headerRows.lastElementChild;
1653
+ hrow.querySelector('.http-tool-header-key').value = h.key;
1654
+ hrow.querySelector('.http-tool-header-value').value = h.value;
1655
+ wireHttpToolRemoveButton(hrow);
1656
+ }
1657
+
1658
+ form.querySelector('[name="sendFieldsAsJsonBody"]').checked = spec.sendFieldsAsJsonBody;
1659
+ form.querySelector('[name="safe"]').checked = spec.safe === true;
1660
+ form.querySelector('[name="responseJsonPath"]').value = spec.responseJsonPath || '';
1661
+ }
1662
+
1663
+ function resetHttpToolForm() {
1664
+ form.reset();
1665
+ fieldRows.innerHTML = '';
1666
+ headerRows.innerHTML = '';
1667
+ nameInput.disabled = false;
1668
+ if (headingEl) headingEl.textContent = 'Create an HTTP tool';
1669
+ submitBtn.textContent = 'Create tool';
1670
+ cancelBtn.style.display = 'none';
1671
+ decisionField.style.display = '';
1672
+ errorEl.textContent = '';
1673
+ editingHttpToolName = null;
1674
+ }
1675
+
1676
+ cancelBtn.addEventListener('click', resetHttpToolForm);
1677
+
1678
+ // Local tools table's own Edit buttons (renderLocalTools) — only
1679
+ // present for tools describeAgent flagged httpToolEditable, so this
1680
+ // GET should never actually 404 in normal use.
1681
+ var editButtons = detail.querySelectorAll('.edit-http-tool-btn');
1682
+ for (var k = 0; k < editButtons.length; k++) {
1683
+ editButtons[k].addEventListener('click', function (ev) {
1684
+ var toolName = ev.currentTarget.getAttribute('data-name');
1685
+ errorEl.textContent = '';
1686
+ fetch('/agents/' + encodeURIComponent(name) + '/tools/http/' + encodeURIComponent(toolName))
1687
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
1688
+ .then(function (result) {
1689
+ if (!result.ok) throw new Error(result.body.error || 'request failed');
1690
+ populateHttpToolForm(result.body);
1691
+ nameInput.disabled = true;
1692
+ if (headingEl) headingEl.textContent = 'Edit HTTP tool "' + toolName + '"';
1693
+ submitBtn.textContent = 'Save';
1694
+ cancelBtn.style.display = '';
1695
+ decisionField.style.display = 'none';
1696
+ editingHttpToolName = toolName;
1697
+ form.scrollIntoView({ block: 'nearest' });
1698
+ })
1699
+ .catch(function (err) {
1700
+ errorEl.textContent = 'Could not load tool "' + toolName + '": ' + err.message;
1701
+ });
1702
+ });
1703
+ }
1704
+
1593
1705
  form.addEventListener('submit', function (ev) {
1594
1706
  ev.preventDefault();
1595
1707
  errorEl.textContent = '';
@@ -1619,20 +1731,24 @@ export const agentsConfigPageHtml = `<!doctype html>
1619
1731
 
1620
1732
  var formData = new FormData(form);
1621
1733
  var payload = {
1622
- name: formData.get('name'),
1734
+ name: editingHttpToolName || formData.get('name'),
1623
1735
  description: formData.get('description'),
1624
1736
  fields: fields,
1625
1737
  method: formData.get('method'),
1626
1738
  url: formData.get('url'),
1627
1739
  headers: headers,
1628
1740
  sendFieldsAsJsonBody: formData.get('sendFieldsAsJsonBody') === 'on',
1741
+ safe: formData.get('safe') === 'on',
1629
1742
  responseJsonPath: formData.get('responseJsonPath') ? formData.get('responseJsonPath') : undefined,
1630
1743
  decision: formData.get('decision') ? formData.get('decision') : undefined,
1631
1744
  };
1632
1745
 
1746
+ var isEdit = !!editingHttpToolName;
1747
+ var url = '/agents/' + encodeURIComponent(name) + '/tools/http' + (isEdit ? '/' + encodeURIComponent(editingHttpToolName) : '');
1748
+
1633
1749
  submitBtn.disabled = true;
1634
- fetch('/agents/' + encodeURIComponent(name) + '/tools/http', {
1635
- method: 'POST',
1750
+ fetch(url, {
1751
+ method: isEdit ? 'PUT' : 'POST',
1636
1752
  headers: { 'content-type': 'application/json' },
1637
1753
  body: JSON.stringify(payload),
1638
1754
  })
@@ -5,6 +5,10 @@ export declare class HttpToolExistsError extends Error {
5
5
  }
6
6
  export declare class HttpToolIndexShapeError extends Error {
7
7
  }
8
+ export declare class HttpToolNotFoundError extends Error {
9
+ }
10
+ export declare class HttpToolNotEditableError extends Error {
11
+ }
8
12
  export interface HttpToolField {
9
13
  name: string;
10
14
  type: 'string' | 'number' | 'boolean';
@@ -23,6 +27,14 @@ export interface HttpToolSpec {
23
27
  }[];
24
28
  sendFieldsAsJsonBody: boolean;
25
29
  responseJsonPath?: string;
30
+ /** Same meaning as ToolDefinition.safe (core/agent-config.ts) — true
31
+ * only for a read-only call with no side effects, so ToolLane can run
32
+ * it in its parallel lane. Defaults to false/omitted (unsafe) rather
33
+ * than inferring from `method`, since a GET can still have side
34
+ * effects (e.g. a webhook-triggering endpoint) and the operator who
35
+ * knows the real API is in a better position to say than a guess
36
+ * based on HTTP verb alone. */
37
+ safe?: boolean;
26
38
  }
27
39
  /** Generates agents/<agentName>/tools/<spec.name>.ts, registers it in
28
40
  * that agent's tools/index.ts, then imports the file it just wrote and
@@ -35,3 +47,26 @@ export declare function createHttpTool(agentName: string, spec: HttpToolSpec): P
35
47
  path: string;
36
48
  tool: ToolDefinition;
37
49
  }>;
50
+ /** Reads back the spec a previous createHttpTool/updateHttpTool call
51
+ * persisted, for the Tools tab's Edit form to repopulate itself from —
52
+ * or null if this tool was never created through this module (a
53
+ * hand-written tools/<name>.ts, or one from before this sidecar existed),
54
+ * which is exactly the signal the route handler uses to decide whether
55
+ * to even show an Edit button for it. */
56
+ export declare function readHttpToolSpec(agentName: string, toolName: string): HttpToolSpec | null;
57
+ /** Every tool name in this agent's tools/ dir with a saved spec — i.e.
58
+ * every tool the Tools tab can offer an Edit button for. Cheap (one
59
+ * readdirSync), safe to call on every GET /agents/:name/config. */
60
+ export declare function listEditableHttpToolNames(agentName: string): string[];
61
+ /** Overwrites an existing admin-created tool in place — same generated-
62
+ * code shape createHttpTool produces, just written to the file that's
63
+ * already there instead of a new one, and without touching tools/index.ts
64
+ * (the import/export names are unchanged, since renaming isn't supported
65
+ * here: spec.name must match toolName). Refuses a tool this module didn't
66
+ * create (no sidecar spec) rather than silently clobbering hand-written
67
+ * logic — same "refuse rather than guess" rule addToolToIndex's own doc
68
+ * comment already applies to index.ts. */
69
+ export declare function updateHttpTool(agentName: string, toolName: string, spec: HttpToolSpec): Promise<{
70
+ path: string;
71
+ tool: ToolDefinition;
72
+ }>;
@@ -21,7 +21,7 @@
21
21
  // generated file, or echoed back through the admin API, the same
22
22
  // "always env-sourced, never a config literal" rule SendEmail/
23
23
  // HttpNotifierConfig's own secrets already follow (core/agent-config.ts).
24
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
24
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
25
25
  import { join } from 'node:path';
26
26
  import { pathToFileURL } from 'node:url';
27
27
  import { agentDir } from '../core/gateway-tools.js';
@@ -31,6 +31,10 @@ export class HttpToolExistsError extends Error {
31
31
  }
32
32
  export class HttpToolIndexShapeError extends Error {
33
33
  }
34
+ export class HttpToolNotFoundError extends Error {
35
+ }
36
+ export class HttpToolNotEditableError extends Error {
37
+ }
34
38
  const TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
35
39
  const FIELD_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
36
40
  const ENV_VAR_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
@@ -142,9 +146,13 @@ function generateToolCode(spec) {
142
146
  : ' return await res.text()\n';
143
147
  return `// Generated by the Admin UI's HTTP tool builder (Tools tab) — a
144
148
  // parameterized HTTP call, not hand-written logic. Edit freely for
145
- // anything this template can't express; the Admin UI only ever
146
- // regenerates a tool it created itself, never overwrites a file it
147
- // doesn't recognize (see web/http-tool-admin.ts's own HttpToolExistsError).
149
+ // anything this template can't express, but note that hand-editing this
150
+ // file directly (rather than through the Tools tab's own Edit button)
151
+ // leaves the sidecar ${spec.name}.http-tool.json out of sync — the next
152
+ // edit through the UI will overwrite your hand-edits with whatever that
153
+ // sidecar still says. The Admin UI only ever regenerates a tool it
154
+ // created itself, never overwrites a file it doesn't recognize (see
155
+ // web/http-tool-admin.ts's own HttpToolExistsError/HttpToolNotEditableError).
148
156
  import type { ToolDefinition } from 'loopengine'
149
157
 
150
158
  export const ${toCamelCase(spec.name)}: ToolDefinition = {
@@ -163,6 +171,7 @@ ${envReads} const res = await fetch(\`${urlInner}\`, {
163
171
  ${headersObj}${bodyLine} })
164
172
  if (!res.ok) throw new Error(\`${spec.name}: HTTP \${res.status}\`)
165
173
  ${responseHandling} },
174
+ safe: ${spec.safe === true},
166
175
  }
167
176
  `;
168
177
  }
@@ -191,6 +200,15 @@ function addToolToIndex(toolsIndexPath, toolFileName, exportName) {
191
200
  const withEntry = withImport.replace(/export const tools: ToolDefinition\[\] = \[([^\]]*)\]/, `export const tools: ToolDefinition[] = [${newEntries}]`);
192
201
  writeFileSync(toolsIndexPath, withEntry);
193
202
  }
203
+ // The spec is persisted alongside the generated .ts file, under the same
204
+ // name — this is what lets the Tools tab's Edit button repopulate the
205
+ // form later without reverse-parsing generated code back into a spec,
206
+ // and (its second job) is how readHttpToolSpec/listEditableHttpToolNames
207
+ // tell an admin-created tool apart from a hand-written one: only a tool
208
+ // this module itself created has one.
209
+ function specSidecarPath(toolsDir, name) {
210
+ return join(toolsDir, `${name}.http-tool.json`);
211
+ }
194
212
  /** Generates agents/<agentName>/tools/<spec.name>.ts, registers it in
195
213
  * that agent's tools/index.ts, then imports the file it just wrote and
196
214
  * returns the real, callable ToolDefinition object — so the route
@@ -210,6 +228,7 @@ export async function createHttpTool(agentName, spec) {
210
228
  const code = generateToolCode(spec);
211
229
  mkdirSync(toolsDir, { recursive: true });
212
230
  writeFileSync(toolPath, code);
231
+ writeFileSync(specSidecarPath(toolsDir, spec.name), JSON.stringify(spec, null, 2));
213
232
  const indexPath = join(toolsDir, 'index.ts');
214
233
  if (existsSync(indexPath)) {
215
234
  addToolToIndex(indexPath, spec.name, toCamelCase(spec.name));
@@ -221,3 +240,62 @@ export async function createHttpTool(agentName, spec) {
221
240
  const mod = (await import(pathToFileURL(toolPath).href));
222
241
  return { path: toolPath, tool: mod[exportName] };
223
242
  }
243
+ /** Reads back the spec a previous createHttpTool/updateHttpTool call
244
+ * persisted, for the Tools tab's Edit form to repopulate itself from —
245
+ * or null if this tool was never created through this module (a
246
+ * hand-written tools/<name>.ts, or one from before this sidecar existed),
247
+ * which is exactly the signal the route handler uses to decide whether
248
+ * to even show an Edit button for it. */
249
+ export function readHttpToolSpec(agentName, toolName) {
250
+ const sidecarPath = specSidecarPath(join(agentDir(agentName), 'tools'), toolName);
251
+ if (!existsSync(sidecarPath))
252
+ return null;
253
+ return JSON.parse(readFileSync(sidecarPath, 'utf8'));
254
+ }
255
+ /** Every tool name in this agent's tools/ dir with a saved spec — i.e.
256
+ * every tool the Tools tab can offer an Edit button for. Cheap (one
257
+ * readdirSync), safe to call on every GET /agents/:name/config. */
258
+ export function listEditableHttpToolNames(agentName) {
259
+ const toolsDir = join(agentDir(agentName), 'tools');
260
+ if (!existsSync(toolsDir))
261
+ return [];
262
+ return readdirSync(toolsDir)
263
+ .filter((f) => f.endsWith('.http-tool.json'))
264
+ .map((f) => f.slice(0, -'.http-tool.json'.length));
265
+ }
266
+ /** Overwrites an existing admin-created tool in place — same generated-
267
+ * code shape createHttpTool produces, just written to the file that's
268
+ * already there instead of a new one, and without touching tools/index.ts
269
+ * (the import/export names are unchanged, since renaming isn't supported
270
+ * here: spec.name must match toolName). Refuses a tool this module didn't
271
+ * create (no sidecar spec) rather than silently clobbering hand-written
272
+ * logic — same "refuse rather than guess" rule addToolToIndex's own doc
273
+ * comment already applies to index.ts. */
274
+ export async function updateHttpTool(agentName, toolName, spec) {
275
+ if (spec.name !== toolName) {
276
+ throw new HttpToolNameError(`A tool's name can't be changed by editing it — "${toolName}" vs "${spec.name}".`);
277
+ }
278
+ if (!TOOL_NAME_PATTERN.test(spec.name)) {
279
+ throw new HttpToolNameError(`Tool name must be lowercase snake_case (e.g. "lookup_order_status") — got "${spec.name}"`);
280
+ }
281
+ const toolsDir = join(agentDir(agentName), 'tools');
282
+ const toolPath = join(toolsDir, `${spec.name}.ts`);
283
+ if (!existsSync(toolPath)) {
284
+ throw new HttpToolNotFoundError(`agents/${agentName}/tools/${spec.name}.ts doesn't exist — nothing to edit.`);
285
+ }
286
+ const sidecarPath = specSidecarPath(toolsDir, spec.name);
287
+ if (!existsSync(sidecarPath)) {
288
+ throw new HttpToolNotEditableError(`agents/${agentName}/tools/${spec.name}.ts wasn't created by this form — edit the file directly instead.`);
289
+ }
290
+ const code = generateToolCode(spec);
291
+ writeFileSync(toolPath, code);
292
+ writeFileSync(sidecarPath, JSON.stringify(spec, null, 2));
293
+ const exportName = toCamelCase(spec.name);
294
+ // Cache-busted: this exact file URL was already imported once before
295
+ // (at creation, or whenever this agent's tools were last resolved), and
296
+ // Node's ESM cache is keyed by the literal URL string — a plain
297
+ // re-import would silently hand back the stale pre-edit module instead
298
+ // of the code just written above.
299
+ const mod = (await import(`${pathToFileURL(toolPath).href}?edited=${Date.now()}`));
300
+ return { path: toolPath, tool: mod[exportName] };
301
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",