@framers/agentos 0.10.40 → 0.11.0

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.
Files changed (82) hide show
  1. package/dist/api/agency.d.ts.map +1 -1
  2. package/dist/api/agency.js +140 -114
  3. package/dist/api/agency.js.map +1 -1
  4. package/dist/api/agent.d.ts +9 -5
  5. package/dist/api/agent.d.ts.map +1 -1
  6. package/dist/api/agent.js +7 -5
  7. package/dist/api/agent.js.map +1 -1
  8. package/dist/api/agentExport.d.ts +60 -21
  9. package/dist/api/agentExport.d.ts.map +1 -1
  10. package/dist/api/agentExport.js +240 -41
  11. package/dist/api/agentExport.js.map +1 -1
  12. package/dist/api/agentExportCore.d.ts +44 -6
  13. package/dist/api/agentExportCore.d.ts.map +1 -1
  14. package/dist/api/agentExportCore.js +43 -8
  15. package/dist/api/agentExportCore.js.map +1 -1
  16. package/dist/api/agentExportRedact.d.ts +58 -0
  17. package/dist/api/agentExportRedact.d.ts.map +1 -0
  18. package/dist/api/agentExportRedact.js +219 -0
  19. package/dist/api/agentExportRedact.js.map +1 -0
  20. package/dist/api/generateText.d.ts +11 -0
  21. package/dist/api/generateText.d.ts.map +1 -1
  22. package/dist/api/generateText.js +27 -0
  23. package/dist/api/generateText.js.map +1 -1
  24. package/dist/api/model.d.ts +12 -0
  25. package/dist/api/model.d.ts.map +1 -1
  26. package/dist/api/model.js +11 -0
  27. package/dist/api/model.js.map +1 -1
  28. package/dist/api/runtime/approval-gate.d.ts +92 -0
  29. package/dist/api/runtime/approval-gate.d.ts.map +1 -0
  30. package/dist/api/runtime/approval-gate.js +182 -0
  31. package/dist/api/runtime/approval-gate.js.map +1 -0
  32. package/dist/api/runtime/tool-emulation/loop.d.ts +16 -0
  33. package/dist/api/runtime/tool-emulation/loop.d.ts.map +1 -1
  34. package/dist/api/runtime/tool-emulation/loop.js +25 -3
  35. package/dist/api/runtime/tool-emulation/loop.js.map +1 -1
  36. package/dist/api/streamText.d.ts.map +1 -1
  37. package/dist/api/streamText.js +33 -0
  38. package/dist/api/streamText.js.map +1 -1
  39. package/dist/api/types.d.ts +23 -4
  40. package/dist/api/types.d.ts.map +1 -1
  41. package/dist/api/types.js.map +1 -1
  42. package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts +120 -32
  43. package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts.map +1 -1
  44. package/dist/cognition/emergent/EmergentCapabilityEngine.js +602 -19
  45. package/dist/cognition/emergent/EmergentCapabilityEngine.js.map +1 -1
  46. package/dist/cognition/emergent/EmergentToolRegistry.d.ts +180 -38
  47. package/dist/cognition/emergent/EmergentToolRegistry.d.ts.map +1 -1
  48. package/dist/cognition/emergent/EmergentToolRegistry.js +645 -96
  49. package/dist/cognition/emergent/EmergentToolRegistry.js.map +1 -1
  50. package/dist/cognition/emergent/ForgeToolMetaTool.js +5 -5
  51. package/dist/cognition/emergent/ForgeToolMetaTool.js.map +1 -1
  52. package/dist/cognition/emergent/capabilities.d.ts +27 -0
  53. package/dist/cognition/emergent/capabilities.d.ts.map +1 -0
  54. package/dist/cognition/emergent/capabilities.js +50 -0
  55. package/dist/cognition/emergent/capabilities.js.map +1 -0
  56. package/dist/cognition/emergent/index.d.ts +4 -1
  57. package/dist/cognition/emergent/index.d.ts.map +1 -1
  58. package/dist/cognition/emergent/index.js +2 -0
  59. package/dist/cognition/emergent/index.js.map +1 -1
  60. package/dist/cognition/emergent/persisted-source.d.ts +106 -0
  61. package/dist/cognition/emergent/persisted-source.d.ts.map +1 -0
  62. package/dist/cognition/emergent/persisted-source.js +370 -0
  63. package/dist/cognition/emergent/persisted-source.js.map +1 -0
  64. package/dist/cognition/emergent/types.d.ts +91 -1
  65. package/dist/cognition/emergent/types.d.ts.map +1 -1
  66. package/dist/cognition/emergent/types.js.map +1 -1
  67. package/dist/cognition/emergent/wrapForgeTool.d.ts +4 -1
  68. package/dist/cognition/emergent/wrapForgeTool.d.ts.map +1 -1
  69. package/dist/cognition/emergent/wrapForgeTool.js +3 -0
  70. package/dist/cognition/emergent/wrapForgeTool.js.map +1 -1
  71. package/dist/core/llm/providers/url-secrets.d.ts +21 -0
  72. package/dist/core/llm/providers/url-secrets.d.ts.map +1 -1
  73. package/dist/core/llm/providers/url-secrets.js +72 -0
  74. package/dist/core/llm/providers/url-secrets.js.map +1 -1
  75. package/dist/core/tools/ToolOrchestrator.d.ts +2 -0
  76. package/dist/core/tools/ToolOrchestrator.d.ts.map +1 -1
  77. package/dist/core/tools/ToolOrchestrator.js +25 -2
  78. package/dist/core/tools/ToolOrchestrator.js.map +1 -1
  79. package/dist/index.d.ts +1 -1
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js.map +1 -1
  82. package/package.json +1 -1
@@ -24,6 +24,7 @@
24
24
  */
25
25
  import { randomUUID } from 'node:crypto';
26
26
  import { DEFAULT_EMERGENT_CONFIG } from './types.js';
27
+ import { parsePersistedSource, parseStoredRequest, sessionFromSource, stateSetterFromColumn } from './persisted-source.js';
27
28
  // ============================================================================
28
29
  // TIER ORDER
29
30
  // ============================================================================
@@ -32,33 +33,6 @@ import { DEFAULT_EMERGENT_CONFIG } from './types.js';
32
33
  * Higher index = broader scope = higher trust.
33
34
  */
34
35
  const TIER_ORDER = ['session', 'agent', 'shared'];
35
- // ============================================================================
36
- // EMERGENT TOOL REGISTRY
37
- // ============================================================================
38
- /**
39
- * Manages the lifecycle of emergent tools across three trust tiers.
40
- *
41
- * The registry stores session-tier tools in an in-memory Map (keyed by tool ID)
42
- * and mirrors them to SQLite when available for audit/inspection. Agent/shared
43
- * tier tools live in the persisted map and are written to SQLite (when a
44
- * storage adapter is provided) or kept in-memory as fallback.
45
- *
46
- * Key responsibilities:
47
- * - **Registration**: Accept new tools at a given tier, enforcing config limits.
48
- * - **Lookup**: Retrieve tools by ID or filter by tier with optional scope.
49
- * - **Usage tracking**: Record invocations and update rolling statistics.
50
- * - **Promotion / demotion**: Move tools between tiers with audit logging.
51
- * - **Session cleanup**: Bulk-remove all session-scoped tools for a given session.
52
- * - **Audit trail**: Log every state change for observability and debugging.
53
- *
54
- * @example
55
- * ```ts
56
- * const registry = new EmergentToolRegistry({ ...DEFAULT_EMERGENT_CONFIG, enabled: true });
57
- * registry.register(tool, 'session');
58
- * registry.recordUse(tool.id, { x: 1 }, { y: 2 }, true, 42);
59
- * const stats = registry.getUsageStats(tool.id);
60
- * ```
61
- */
62
36
  export class EmergentToolRegistry {
63
37
  /**
64
38
  * Create a new EmergentToolRegistry.
@@ -76,8 +50,22 @@ export class EmergentToolRegistry {
76
50
  this.persistedTools = new Map();
77
51
  /** In-memory audit log. Always populated regardless of DB availability. */
78
52
  this.auditLog = [];
53
+ /** Held state per tool. A tool with no entry counts as active. */
54
+ this.states = new Map();
55
+ /**
56
+ * One chain of state writes per tool. A later `setState` starts its writes
57
+ * after an earlier one's have finished, so storage sees state changes in
58
+ * call order whatever the adapter's connections do.
59
+ */
60
+ this.stateWrites = new Map();
79
61
  /** Whether `ensureSchema()` has been called and completed. */
80
62
  this.schemaReady = false;
63
+ /**
64
+ * Ids removed in this process whose rows are not yet gone: an admission or
65
+ * a forge that was mid-flight does not put one back. Cleared when the
66
+ * deletes land, or when a new row is written for the id.
67
+ */
68
+ this.removedIds = new Set();
81
69
  /**
82
70
  * Cached promise from the first `ensureSchemaReady()` call.
83
71
  * Guards against the race condition where multiple callers invoke
@@ -109,9 +97,10 @@ export class EmergentToolRegistry {
109
97
  /**
110
98
  * Initialize the database schema for emergent tool persistence.
111
99
  *
112
- * Creates the `agentos_emergent_tools` and `agentos_emergent_audit_log`
113
- * tables along with their indexes. Safe to call multiple times — all
114
- * statements use `CREATE TABLE IF NOT EXISTS` / `CREATE INDEX IF NOT EXISTS`.
100
+ * Creates the `agentos_emergent_tools`, `agentos_emergent_audit_log` and
101
+ * `agentos_emergent_tool_state` tables along with their indexes. Safe to
102
+ * call multiple times — all statements use `CREATE TABLE IF NOT EXISTS` /
103
+ * `CREATE INDEX IF NOT EXISTS`.
115
104
  *
116
105
  * This method is a no-op when no storage adapter was provided.
117
106
  *
@@ -156,16 +145,34 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
156
145
  timestamp BIGINT NOT NULL
157
146
  );`;
158
147
  const auditIndex = `CREATE INDEX IF NOT EXISTS idx_emergent_audit_tool ON agentos_emergent_audit_log(tool_id, timestamp);`;
148
+ // State and the stored request live in their own table: persistToolToDb
149
+ // rewrites the whole tool row with INSERT OR REPLACE, which would reset any
150
+ // column added to agentos_emergent_tools on every call.
151
+ const stateTable = `
152
+ CREATE TABLE IF NOT EXISTS agentos_emergent_tool_state (
153
+ tool_id TEXT PRIMARY KEY,
154
+ state TEXT NOT NULL,
155
+ state_reason TEXT,
156
+ set_by TEXT NOT NULL DEFAULT 'host',
157
+ state_at BIGINT NOT NULL,
158
+ request_json TEXT,
159
+ updated_at BIGINT NOT NULL,
160
+ flag_synced INTEGER NOT NULL DEFAULT 1,
161
+ write_id TEXT
162
+ );`;
163
+ // Tables and indexes only: the flag on the tool row is written by the
164
+ // library's own statements, never by a trigger, so the schema stays
165
+ // portable (PostgreSQL has no SQLite trigger syntax) and every write to the
166
+ // tool row passes through the storage adapter's hooks.
167
+ const statements = [toolsTable, toolsTierIndex, toolsAgentIndex, auditTable, auditIndex, stateTable];
159
168
  // Prefer `exec` for multi-statement DDL; fall back to individual `run` calls.
160
169
  if (this.db.exec) {
161
- await this.db.exec([toolsTable, toolsTierIndex, toolsAgentIndex, auditTable, auditIndex].join('\n'));
170
+ await this.db.exec(statements.join('\n'));
162
171
  }
163
172
  else {
164
- await this.db.run(toolsTable);
165
- await this.db.run(toolsTierIndex);
166
- await this.db.run(toolsAgentIndex);
167
- await this.db.run(auditTable);
168
- await this.db.run(auditIndex);
173
+ for (const statement of statements) {
174
+ await this.db.run(statement);
175
+ }
169
176
  }
170
177
  this.schemaReady = true;
171
178
  }
@@ -207,17 +214,32 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
207
214
  `Remove or promote existing tools before registering new ones.`);
208
215
  }
209
216
  }
210
- // Stamp the tier on the tool object.
217
+ this.removedIds.delete(tool.id);
218
+ // Stamp the tier on the tool object; a fresh registration is active, so the
219
+ // convention property says so whatever the given object carried.
211
220
  const registered = { ...tool, tier };
221
+ registered.isActive = true;
212
222
  if (tier === 'session') {
213
223
  this.sessionTools.set(registered.id, registered);
214
224
  }
215
225
  else {
216
226
  this.persistedTools.set(registered.id, registered);
217
227
  }
218
- if (this.db && this.schemaReady) {
219
- this.persistToolToDb(registered).catch(() => {
220
- // Best-effort persistence mirror. In-memory state remains authoritative.
228
+ this.states.set(registered.id, {
229
+ toolId: registered.id,
230
+ state: 'active',
231
+ reason: null,
232
+ setBy: 'library',
233
+ at: Date.now(),
234
+ request: null,
235
+ });
236
+ if (this.db) {
237
+ // In the tool's write queue, so its first state write (which inserts a
238
+ // state row only while the tool row exists) runs after the row lands;
239
+ // persistToolToDb waits for the schema itself. Best-effort: in-memory
240
+ // state remains authoritative.
241
+ this.queueStateWrite(registered.id, () => this.persistToolToDb(registered)).catch(() => {
242
+ // Best-effort persistence mirror.
221
243
  });
222
244
  }
223
245
  this.logAudit(registered.id, 'register', { tier });
@@ -236,16 +258,454 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
236
258
  get(toolId) {
237
259
  return this.sessionTools.get(toolId) ?? this.persistedTools.get(toolId);
238
260
  }
261
+ // --------------------------------------------------------------------------
262
+ // STATE
263
+ // --------------------------------------------------------------------------
264
+ /** The held state of a tool, or `undefined` when none has been recorded. */
265
+ getState(toolId) {
266
+ return this.states.get(toolId);
267
+ }
268
+ /** Whether a tool may run. A tool with no recorded state is active. */
269
+ isActive(toolId) {
270
+ return (this.states.get(toolId)?.state ?? 'active') === 'active';
271
+ }
239
272
  /**
240
- * Upsert a tool into the registry, replacing any prior in-memory copy.
273
+ * Record a tool's state, awaiting the write.
274
+ *
275
+ * `options.request` left out leaves the stored request alone: a new row
276
+ * stores none, and an existing row's `request_json` is not named by the
277
+ * update, so a request this process could not read (one a newer release
278
+ * wrote, say) is still there afterwards. A request writes it; `null` clears
279
+ * it.
280
+ *
281
+ * `options.setBy` records who set the state (`'host'` when left out). The
282
+ * library re-checks only its own suspensions at the next load; a host's
283
+ * stays until the host clears it, whatever words its reason uses.
284
+ *
285
+ * Writes for one tool run in call order, one after another. The `isActive`
286
+ * convention property of a tool held in memory follows the state.
287
+ *
288
+ * A restriction (`suspended`, `demoted`) is held in memory at call time, so
289
+ * the running process honours it whatever its write does. A reactivation
290
+ * (`active`) is held only once both writes have succeeded, and only when no
291
+ * other state change arrived while they ran: a suspension or demotion made
292
+ * meanwhile is the newer word, it stays, and this call returns that record
293
+ * instead of its own.
241
294
  *
242
- * Used to hydrate persisted/shared tools back into a live runtime so they can
243
- * become executable again after process restart or admin promotion.
295
+ * `options.ifRow` makes the write conditional: an existing row is changed
296
+ * only while its state, setter and time are still the ones given, and
297
+ * `'absent'` lets the write create a row but never change one. The loader
298
+ * uses it for its active write, so a restriction another process stored
299
+ * after the row was read is never written over; a refused write returns the
300
+ * row's own record, and a restriction read that way is held here as well.
301
+ * Every write, conditional or not, reads the row back after it: a state
302
+ * another process stored meanwhile is what is returned and held, and a tool
303
+ * another process removed reads as `demoted` with the reason `removed`.
304
+ *
305
+ * @returns the record now in force for the tool.
306
+ * @throws If the storage adapter rejects.
307
+ */
308
+ async setState(toolId, state, reason, options = {}) {
309
+ const previous = this.states.get(toolId);
310
+ const setBy = options.setBy ?? 'host';
311
+ const named = options.request !== undefined;
312
+ const record = {
313
+ toolId,
314
+ state,
315
+ reason,
316
+ setBy,
317
+ at: Date.now(),
318
+ request: named ? (options.request ?? null) : (previous?.request ?? null),
319
+ };
320
+ const hold = () => {
321
+ this.states.set(toolId, record);
322
+ const held = this.get(toolId);
323
+ if (held) {
324
+ held.isActive = state === 'active';
325
+ }
326
+ this.logAudit(toolId, 'state', { state, reason, setBy });
327
+ };
328
+ if (state !== 'active') {
329
+ hold();
330
+ }
331
+ let inForce = record;
332
+ if (this.db) {
333
+ inForce = await this.queueStateWrite(toolId, () => this.writeStateRow(toolId, record, named, options.ifRow));
334
+ }
335
+ if (inForce !== record) {
336
+ // The row changed under the condition: the write was refused, and the
337
+ // row's own state is what holds. A restriction read that way is held here.
338
+ this.logAudit(toolId, 'state_refused', { state, reason, setBy, by: inForce.state });
339
+ // The row's word is held here too, active or not: it is stored, and it
340
+ // is the newer one.
341
+ this.states.set(toolId, inForce);
342
+ const held = this.get(toolId);
343
+ if (held) {
344
+ held.isActive = inForce.state === 'active';
345
+ }
346
+ return inForce;
347
+ }
348
+ if (state === 'active') {
349
+ const current = this.states.get(toolId);
350
+ if (current !== previous && current?.state !== 'active' && current?.writeId === undefined) {
351
+ // A restriction requested in this process while this reactivation
352
+ // was being written: its own write is queued after this one, so it is
353
+ // the newer word and stays. (A restriction observed from another
354
+ // process's row carries that write's id; this write landed after it
355
+ // and is the newer word, as is another activation held meanwhile.)
356
+ this.logAudit(toolId, 'state_superseded', { state, reason, setBy, by: current?.state ?? null });
357
+ return current ?? record;
358
+ }
359
+ hold();
360
+ }
361
+ return record;
362
+ }
363
+ /** Runs `write` after every earlier state write of the same tool has settled. */
364
+ queueStateWrite(toolId, write) {
365
+ const prior = this.stateWrites.get(toolId) ?? Promise.resolve();
366
+ const next = prior.catch(() => undefined).then(write);
367
+ this.stateWrites.set(toolId, next);
368
+ next
369
+ .finally(() => {
370
+ if (this.stateWrites.get(toolId) === next) {
371
+ this.stateWrites.delete(toolId);
372
+ }
373
+ })
374
+ .catch(() => undefined);
375
+ return next;
376
+ }
377
+ /**
378
+ * The statements of a state change: the state row upsert, with its flag
379
+ * write marked pending; the legacy flag, read from the state row inside its
380
+ * own statement so the two agree whatever other processes write in between;
381
+ * then the mark cleared. A failure after the state row leaves the mark
382
+ * pending, and the next state write or load finishes the flag write, so a
383
+ * crash or a failed write between the two never leaves the pair apart
384
+ * beyond the next load. With `ifRow`, an existing row is changed only while
385
+ * its state, setter and time are the ones given (`'absent'`: never); a
386
+ * refused write changes nothing, flag included, and the row as it stands
387
+ * afterwards is returned, so it shows as a record other than the one given.
388
+ */
389
+ async writeStateRow(toolId, record, named, ifRow) {
390
+ const db = this.db;
391
+ if (!db) {
392
+ return record;
393
+ }
394
+ await this.ensureSchemaReady();
395
+ const requestJson = named && record.request ? JSON.stringify(record.request) : null;
396
+ const setList = named
397
+ ? `state = excluded.state,
398
+ state_reason = excluded.state_reason,
399
+ set_by = excluded.set_by,
400
+ state_at = excluded.state_at,
401
+ request_json = excluded.request_json,
402
+ updated_at = excluded.updated_at,
403
+ write_id = excluded.write_id`
404
+ : `state = excluded.state,
405
+ state_reason = excluded.state_reason,
406
+ set_by = excluded.set_by,
407
+ state_at = excluded.state_at,
408
+ updated_at = excluded.updated_at,
409
+ write_id = excluded.write_id`;
410
+ let guard = '';
411
+ // state_at is the call's time; updated_at the write's own.
412
+ // Every write has an id of its own: the row read back tells this write
413
+ // from another of the same content in the same millisecond.
414
+ const writeId = randomUUID();
415
+ record.writeId = writeId;
416
+ const values = [toolId, record.state, record.reason, record.setBy, record.at, requestJson, Date.now(), writeId];
417
+ const guardParams = [];
418
+ if (ifRow === 'absent') {
419
+ guard = `
420
+ WHERE 0 = 1`;
421
+ }
422
+ else if (ifRow !== undefined) {
423
+ guard = `
424
+ WHERE agentos_emergent_tool_state.state = ?
425
+ AND agentos_emergent_tool_state.set_by = ?
426
+ AND agentos_emergent_tool_state.state_at = ?`;
427
+ guardParams.push(ifRow.state, ifRow.setBy, ifRow.at);
428
+ }
429
+ const readRow = async () => (await db.get(`SELECT s.state, s.state_reason, s.set_by, s.state_at, s.request_json, s.write_id,
430
+ EXISTS (SELECT 1 FROM agentos_emergent_tools t WHERE t.id = s.tool_id) AS tool_exists
431
+ FROM agentos_emergent_tool_state s
432
+ WHERE s.tool_id = ?`, [toolId]));
433
+ const rowRecord = (row) => ({
434
+ toolId,
435
+ state: row.state,
436
+ reason: row.state_reason ?? null,
437
+ setBy: stateSetterFromColumn(row.set_by),
438
+ at: Number(row.state_at ?? 0),
439
+ request: parseStoredRequest(row.request_json),
440
+ ...(row.write_id ? { writeId: row.write_id } : {}),
441
+ });
442
+ // Applied when the row carries this write's id and its tool row is still
443
+ // there; content and time cannot tell two writes of the same record in one
444
+ // millisecond apart. A row that is gone, or whose tool row is gone, was
445
+ // removed by another process: not applied.
446
+ const matches = (row) => !!row?.state && !!row.tool_exists && row.write_id === writeId;
447
+ // What a caller gets for a tool another process removed meanwhile: off,
448
+ // with the reason, so a load does not register it.
449
+ const removed = () => ({
450
+ toolId,
451
+ state: 'demoted',
452
+ reason: 'removed',
453
+ setBy: 'host',
454
+ at: Date.now(),
455
+ request: null,
456
+ });
457
+ const before = await readRow();
458
+ if (ifRow === 'absent' && before?.state) {
459
+ // "No row yet" was the condition and a row exists: refused, without a
460
+ // statement; the row and its flag are as another process left them, or
461
+ // the tool is on its way out.
462
+ return before.tool_exists ? rowRecord(before) : removed();
463
+ }
464
+ // The state row, its flag write marked pending until it is done. A new
465
+ // state row is inserted only while the tool row exists, so a tool another
466
+ // process removed meanwhile gets no orphan state row; an existing state
467
+ // row can still be changed, and the read below reports a removal.
468
+ await db.run(`INSERT INTO agentos_emergent_tool_state
469
+ (tool_id, state, state_reason, set_by, state_at, request_json, updated_at, write_id, flag_synced)
470
+ SELECT ?, ?, ?, ?, ?, ?, ?, ?, 0
471
+ WHERE EXISTS (SELECT 1 FROM agentos_emergent_tools WHERE id = ?)
472
+ OR EXISTS (SELECT 1 FROM agentos_emergent_tool_state WHERE tool_id = ?)
473
+ ON CONFLICT (tool_id) DO UPDATE SET
474
+ ${setList},
475
+ flag_synced = 0${guard}`, [...values, toolId, toolId, ...guardParams]);
476
+ const row = await readRow();
477
+ if (!matches(row)) {
478
+ // Refused, or the tool is gone: the row and its flag are as another
479
+ // process left them.
480
+ return row?.state && row.tool_exists ? rowRecord(row) : removed();
481
+ }
482
+ if (!named) {
483
+ // The record returned carries the request the row holds, not what this
484
+ // process happened to hold, so a caller can compare grants.
485
+ record.request = parseStoredRequest(row?.request_json);
486
+ }
487
+ // The legacy flag follows the state row, then the mark is cleared. A
488
+ // failure here leaves the mark pending for the next write or load.
489
+ await this.writeLegacyFlag(toolId, record.state);
490
+ await db.run(`UPDATE agentos_emergent_tool_state
491
+ SET flag_synced = 1
492
+ WHERE tool_id = ? AND ${record.writeId !== undefined ? 'write_id = ?' : 'state = ? AND set_by = ? AND state_at = ?'}`, record.writeId !== undefined ? [toolId, record.writeId] : [toolId, record.state, record.setBy, record.at]);
493
+ // The row's word after the flag write: a restriction another process
494
+ // stored meanwhile, or a removal, is what holds, not the record written
495
+ // here. Read for every write, conditional or not, so a host's reactivation
496
+ // overtaken by another process's suspension yields to it in memory too.
497
+ const after = await readRow();
498
+ if (!matches(after)) {
499
+ return after?.state && after.tool_exists ? rowRecord(after) : removed();
500
+ }
501
+ return record;
502
+ }
503
+ /** The legacy flag hosts query, read from the state row inside the statement. */
504
+ async writeLegacyFlag(toolId, state) {
505
+ await this.db.run(`UPDATE agentos_emergent_tools
506
+ SET is_active = COALESCE((SELECT CASE WHEN state = 'active' THEN 1 ELSE 0 END
507
+ FROM agentos_emergent_tool_state WHERE tool_id = ?), ?)
508
+ WHERE id = ?`, [toolId, state === 'active' ? 1 : 0, toolId]);
509
+ }
510
+ /**
511
+ * Finish a state write whose flag write did not: the legacy flag from the
512
+ * state row, then the row's mark cleared, under the record the row holds so
513
+ * a newer write's mark is left alone. The loader calls it for a row whose
514
+ * mark is pending, before anything about the row is decided.
515
+ */
516
+ async syncLegacyFlag(toolId, record) {
517
+ const db = this.db;
518
+ if (!db) {
519
+ return;
520
+ }
521
+ await this.ensureSchemaReady();
522
+ await this.writeLegacyFlag(toolId, record.state);
523
+ await db.run(`UPDATE agentos_emergent_tool_state
524
+ SET flag_synced = 1
525
+ WHERE tool_id = ? AND ${record.writeId !== undefined ? 'write_id = ?' : 'state = ? AND set_by = ? AND state_at = ?'}`, record.writeId !== undefined ? [toolId, record.writeId] : [toolId, record.state, record.setBy, record.at]);
526
+ }
527
+ /**
528
+ * Suspend a tool on the host's word: an awaited write of its state and of
529
+ * `is_active = 0`, recorded as the host's, so no load lifts it whatever the
530
+ * reason says; only a reactivation does. Usage statistics are left alone.
531
+ * The caller unregisters the executable.
532
+ *
533
+ * @throws If the tool is unknown or the write fails.
534
+ */
535
+ async suspend(toolId, reason) {
536
+ if (!this.get(toolId)) {
537
+ throw new Error(`Cannot suspend: tool "${toolId}" not found.`);
538
+ }
539
+ await this.setState(toolId, 'suspended', reason, { setBy: 'host' });
540
+ }
541
+ /** Resolves once every queued state write and deletion of the tool has run. */
542
+ async settled(toolId) {
543
+ await (this.stateWrites.get(toolId) ?? Promise.resolve()).catch(() => undefined);
544
+ }
545
+ /**
546
+ * Drop a tool from memory without touching its rows: for a load whose
547
+ * registration with the host failed, so the row is there for the next load
548
+ * and nothing here claims a tool the executor does not run.
549
+ */
550
+ forget(toolId) {
551
+ this.sessionTools.delete(toolId);
552
+ this.persistedTools.delete(toolId);
553
+ this.states.delete(toolId);
554
+ }
555
+ /** Whether a storage adapter is configured. */
556
+ hasStorage() {
557
+ return this.db !== undefined;
558
+ }
559
+ /**
560
+ * Write a tool's row from the object given, awaited. For a host that
561
+ * hydrates a tool from its own store through `syncPersistedTool` and has no
562
+ * row for it yet; a tool that has a row is loaded from the row, never
563
+ * rewritten by a load.
564
+ */
565
+ async writeToolRow(tool) {
566
+ this.removedIds.delete(tool.id);
567
+ await this.persistToolToDb(tool);
568
+ }
569
+ /** Whether the tool was removed in this process and its rows are not yet gone. */
570
+ wasRemoved(toolId) {
571
+ return this.removedIds.has(toolId);
572
+ }
573
+ /**
574
+ * The rows of a tool, deleted after every queued state write of the tool,
575
+ * so a write still in the queue cannot recreate the state row once it is
576
+ * deleted; the removal is forgotten once they are gone. Best-effort.
577
+ */
578
+ queueRowDeletes(toolId) {
579
+ const db = this.db;
580
+ if (!db) {
581
+ this.removedIds.delete(toolId);
582
+ return;
583
+ }
584
+ this.queueStateWrite(toolId, async () => {
585
+ await db.run(`DELETE FROM agentos_emergent_tools WHERE id = ?`, [toolId]);
586
+ await db.run(`DELETE FROM agentos_emergent_tool_state WHERE tool_id = ?`, [toolId]);
587
+ })
588
+ .catch(() => {
589
+ // Best-effort cleanup only.
590
+ })
591
+ .finally(() => {
592
+ this.removedIds.delete(toolId);
593
+ });
594
+ }
595
+ /**
596
+ * Take a tool read from storage into memory without rewriting its row.
597
+ * `upsert` re-serialises the source; a loaded tool must keep the row it has.
598
+ */
599
+ adopt(tool, record) {
600
+ if (this.removedIds.has(tool.id)) {
601
+ // Removed in this process while the caller had the tool in hand.
602
+ return false;
603
+ }
604
+ this.sessionTools.delete(tool.id);
605
+ this.persistedTools.delete(tool.id);
606
+ if (tool.tier === 'session') {
607
+ this.sessionTools.set(tool.id, tool);
608
+ }
609
+ else {
610
+ this.persistedTools.set(tool.id, tool);
611
+ }
612
+ tool.isActive = record.state === 'active';
613
+ this.states.set(tool.id, record);
614
+ return true;
615
+ }
616
+ /**
617
+ * The stored rows of the given tiers, in creation order, with their state
618
+ * rows: every `shared` row, the `agent` rows of `scope.agentId` and the
619
+ * `session` rows of `scope.sessionId`. A tier named without its selector
620
+ * contributes no rows; the engine refuses such a call before it gets here.
621
+ */
622
+ async loadRows(tiers, scope = {}) {
623
+ if (!this.db || tiers.length === 0) {
624
+ return [];
625
+ }
626
+ const clauses = [];
627
+ const params = [];
628
+ if (tiers.includes('shared')) {
629
+ clauses.push(`t.tier = 'shared'`);
630
+ }
631
+ if (tiers.includes('agent') && scope.agentId !== undefined) {
632
+ clauses.push(`(t.tier = 'agent' AND t.created_by_agent = ?)`);
633
+ params.push(scope.agentId);
634
+ }
635
+ if (tiers.includes('session') && scope.sessionId !== undefined) {
636
+ clauses.push(`(t.tier = 'session' AND t.created_by_session = ?)`);
637
+ params.push(scope.sessionId);
638
+ }
639
+ if (clauses.length === 0) {
640
+ return [];
641
+ }
642
+ await this.ensureSchemaReady();
643
+ const rows = await this.db.all(`SELECT ${EmergentToolRegistry.ROW_COLUMNS}
644
+ WHERE ${clauses.join(' OR ')}
645
+ ORDER BY t.created_at ASC`, params);
646
+ return rows;
647
+ }
648
+ /** One stored row with its state row, or `undefined`. */
649
+ async loadRow(toolId) {
650
+ if (!this.db) {
651
+ return undefined;
652
+ }
653
+ await this.ensureSchemaReady();
654
+ const row = await this.db.get(`SELECT ${EmergentToolRegistry.ROW_COLUMNS}
655
+ WHERE t.id = ?`, [toolId]);
656
+ return row ?? undefined;
657
+ }
658
+ /** A tool's state: the held one, else the stored one, else `undefined`. */
659
+ async readState(toolId) {
660
+ const held = this.states.get(toolId);
661
+ if (held || !this.db) {
662
+ return held;
663
+ }
664
+ return this.readStoredState(toolId);
665
+ }
666
+ /**
667
+ * A tool's state as its row reads now, or `undefined`; what this process
668
+ * holds is not consulted. The loader reads it before adopting a row it has
669
+ * nothing to write for, since a restriction another process stored after
670
+ * the row was read is the newer word.
671
+ */
672
+ async readStoredState(toolId) {
673
+ if (!this.db) {
674
+ return undefined;
675
+ }
676
+ await this.ensureSchemaReady();
677
+ const row = (await this.db.get(`SELECT s.state, s.state_reason, s.set_by, s.state_at, s.request_json, s.write_id,
678
+ EXISTS (SELECT 1 FROM agentos_emergent_tools t WHERE t.id = s.tool_id) AS tool_exists
679
+ FROM agentos_emergent_tool_state s
680
+ WHERE s.tool_id = ?`, [toolId]));
681
+ if (!row?.state || !row.tool_exists) {
682
+ // No state row, or a tool row already gone: nothing stored to hold.
683
+ return undefined;
684
+ }
685
+ return {
686
+ toolId,
687
+ state: row.state,
688
+ reason: row.state_reason ?? null,
689
+ setBy: stateSetterFromColumn(row.set_by),
690
+ at: Number(row.state_at ?? 0),
691
+ request: parseStoredRequest(row.request_json),
692
+ ...(row.write_id ? { writeId: row.write_id } : {}),
693
+ };
694
+ }
695
+ /**
696
+ * Replace the in-memory copy of a tool and mirror it to storage.
697
+ *
698
+ * The row is rewritten from the object given. To bring stored tools back
699
+ * after a restart, call the engine's `loadPersistedTools`, which reads each
700
+ * row, checks it and never rewrites it. A tool whose state this process does
701
+ * not hold keeps the `is_active` its row has.
244
702
  */
245
703
  upsert(tool) {
246
704
  this.sessionTools.delete(tool.id);
247
705
  this.persistedTools.delete(tool.id);
248
706
  const normalized = { ...tool };
707
+ // The convention property follows the held state, not the given object.
708
+ normalized.isActive = this.isActive(normalized.id);
249
709
  if (normalized.tier === 'session') {
250
710
  this.sessionTools.set(normalized.id, normalized);
251
711
  }
@@ -266,14 +726,13 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
266
726
  */
267
727
  remove(toolId) {
268
728
  const removed = this.sessionTools.delete(toolId) || this.persistedTools.delete(toolId);
729
+ this.states.delete(toolId);
730
+ // The rows go whether or not this process held the tool, so a host can
731
+ // remove a stored tool it never loaded; the removal is remembered until
732
+ // they are gone.
733
+ this.removedIds.add(toolId);
734
+ this.queueRowDeletes(toolId);
269
735
  if (removed) {
270
- if (this.db && this.schemaReady) {
271
- this.db
272
- .run(`DELETE FROM agentos_emergent_tools WHERE id = ?`, [toolId])
273
- .catch(() => {
274
- // Best-effort cleanup only.
275
- });
276
- }
277
736
  this.logAudit(toolId, 'remove');
278
737
  }
279
738
  return removed;
@@ -327,6 +786,7 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
327
786
  * @param _output - The output returned by the tool (logged for audit).
328
787
  * @param success - Whether the invocation completed successfully.
329
788
  * @param executionTimeMs - Wall-clock execution time in milliseconds.
789
+ * @returns `false` when the tool is suspended or demoted and nothing was recorded.
330
790
  *
331
791
  * @throws {Error} If no tool with the given ID is registered.
332
792
  */
@@ -335,6 +795,11 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
335
795
  if (!tool) {
336
796
  throw new Error(`Cannot record use: tool "${toolId}" not found.`);
337
797
  }
798
+ // A suspended or demoted tool records nothing: its statistics must not
799
+ // move while it cannot run, and nothing here may write it back as active.
800
+ if (!this.isActive(toolId)) {
801
+ return false;
802
+ }
338
803
  const stats = tool.usageStats;
339
804
  const prevTotal = stats.totalUses;
340
805
  stats.totalUses += 1;
@@ -349,13 +814,31 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
349
814
  (stats.avgExecutionTimeMs * prevTotal + executionTimeMs) / stats.totalUses;
350
815
  // Confidence is success rate.
351
816
  stats.confidenceScore = stats.successCount / stats.totalUses;
352
- stats.lastUsedAt = new Date().toISOString();
817
+ const usedAtMs = Date.now();
818
+ stats.lastUsedAt = new Date(usedAtMs).toISOString();
353
819
  if (this.db && this.schemaReady) {
354
- this.persistToolToDb(tool).catch(() => {
820
+ // Usage columns only. Rewriting the whole row re-serialises the source,
821
+ // which replaced a stored source with the redacted record whenever
822
+ // persistSandboxSource was off.
823
+ this.db
824
+ .run(`UPDATE agentos_emergent_tools
825
+ SET confidence_score = ?, total_uses = ?, success_count = ?,
826
+ failure_count = ?, avg_execution_ms = ?, last_used_at = ?
827
+ WHERE id = ?`, [
828
+ stats.confidenceScore,
829
+ stats.totalUses,
830
+ stats.successCount,
831
+ stats.failureCount,
832
+ stats.avgExecutionTimeMs,
833
+ usedAtMs,
834
+ toolId,
835
+ ])
836
+ .catch(() => {
355
837
  // Best-effort persistence mirror. Usage stats still live in memory.
356
838
  });
357
839
  }
358
840
  this.logAudit(toolId, 'use', { success, executionTimeMs });
841
+ return true;
359
842
  }
360
843
  // --------------------------------------------------------------------------
361
844
  // GET USAGE STATS
@@ -428,15 +911,21 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
428
911
  * Demote or deactivate a tool.
429
912
  *
430
913
  * Marks the tool as inactive by setting a sentinel on its usage stats
431
- * (`confidenceScore` set to 0) and logs the demotion event with a reason.
914
+ * (`confidenceScore` set to 0), records the `demoted` state and logs the
915
+ * demotion event with a reason.
432
916
  *
433
917
  * Inactive tools are still retrievable via `get()` but should be filtered
434
918
  * out by callers when building tool lists for the LLM.
435
919
  *
920
+ * Returns a promise for the state write, which rejects when the write fails;
921
+ * await it when the demotion must be durable. An un-awaited call cannot
922
+ * become an unhandled rejection. The in-memory effects have happened by the
923
+ * time this returns.
924
+ *
436
925
  * @param toolId - The ID of the tool to demote.
437
926
  * @param reason - Human-readable explanation for why the tool is being demoted.
438
927
  *
439
- * @throws {Error} If the tool is not found.
928
+ * @throws {Error} If the tool is not found (thrown synchronously).
440
929
  */
441
930
  demote(toolId, reason) {
442
931
  const tool = this.get(toolId);
@@ -444,16 +933,15 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
444
933
  throw new Error(`Cannot demote: tool "${toolId}" not found.`);
445
934
  }
446
935
  tool.usageStats.confidenceScore = 0;
447
- // Mark as inactive via a convention property.
448
- tool.isActive = false;
449
- if (this.db && this.schemaReady) {
450
- this.db
451
- .run(`UPDATE agentos_emergent_tools SET is_active = 0 WHERE id = ?`, [toolId])
452
- .catch(() => {
453
- // Best-effort persistence only.
454
- });
455
- }
456
936
  this.logAudit(toolId, 'demote', { reason });
937
+ // The state row is what keeps a demoted tool off at the next load; it is
938
+ // the host's word, so no load lifts it. setState sets the convention
939
+ // property `isActive` before it writes.
940
+ const write = this.setState(toolId, 'demoted', reason, { setBy: 'host' }).then(() => undefined);
941
+ // Handled here so a caller that does not await is not left with an
942
+ // unhandled rejection; an awaiting caller still sees the failure.
943
+ write.catch(() => { });
944
+ return write;
457
945
  }
458
946
  // --------------------------------------------------------------------------
459
947
  // CLEANUP SESSION
@@ -461,9 +949,9 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
461
949
  /**
462
950
  * Remove all session-tier tools associated with a specific session.
463
951
  *
464
- * Iterates the session map and deletes every tool whose `source` string
465
- * contains the given session ID. Logs a cleanup audit event for each
466
- * removed tool.
952
+ * Iterates the session map and deletes every tool whose `source` names the
953
+ * given session; the rows go after the tool's queued writes. Logs a cleanup
954
+ * audit event for each removed tool.
467
955
  *
468
956
  * @param sessionId - The session identifier to match against tool `source`
469
957
  * strings.
@@ -472,15 +960,12 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
472
960
  cleanupSession(sessionId) {
473
961
  let removedCount = 0;
474
962
  for (const [id, tool] of this.sessionTools) {
475
- if (tool.source.includes(sessionId)) {
963
+ // The session the source names, whole: "sess-1" is not "sess-10".
964
+ if (sessionFromSource(tool.source) === sessionId) {
476
965
  this.sessionTools.delete(id);
477
- if (this.db && this.schemaReady) {
478
- this.db
479
- .run(`DELETE FROM agentos_emergent_tools WHERE id = ?`, [id])
480
- .catch(() => {
481
- // Best-effort cleanup only.
482
- });
483
- }
966
+ this.states.delete(id);
967
+ this.removedIds.add(id);
968
+ this.queueRowDeletes(id);
484
969
  this.logAudit(id, 'cleanup', { sessionId });
485
970
  removedCount += 1;
486
971
  }
@@ -558,36 +1043,64 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
558
1043
  // Without this, a race between the fire-and-forget mirror write in
559
1044
  // register() and a slow ensureSchema() could produce "table not found".
560
1045
  await this.ensureSchemaReady();
561
- // Extract the session ID from the source string if present.
562
- const sessionMatch = tool.source.match(/session\s+([\w-]+)/);
563
- const sessionId = sessionMatch?.[1] ?? 'unknown';
1046
+ // The session the tool was forged in, from its source line, kept whole:
1047
+ // the loader's session selector compares it with the id the host gives.
1048
+ const sessionId = sessionFromSource(tool.source) ?? 'unknown';
564
1049
  let promotedAt = null;
565
1050
  let promotedBy = approvedBy ?? null;
1051
+ // The row as it is, read when the write depends on it: a promoted tool
1052
+ // keeps its promotion, and a tool whose state this process does not hold
1053
+ // keeps the is_active flag its row has.
1054
+ const held = this.states.get(tool.id);
1055
+ let existing;
1056
+ if (tool.tier !== 'session' || !held) {
1057
+ existing = (await this.db.get(`SELECT promoted_at, promoted_by, is_active
1058
+ FROM agentos_emergent_tools
1059
+ WHERE id = ?
1060
+ LIMIT 1`, [tool.id]));
1061
+ }
566
1062
  if (tool.tier !== 'session') {
567
- const existing = (await this.db.get(`SELECT promoted_at, promoted_by
568
- FROM agentos_emergent_tools
569
- WHERE id = ?
570
- LIMIT 1`, [tool.id])) ?? { promoted_at: null, promoted_by: null };
1063
+ const row = existing ?? { promoted_at: null, promoted_by: null };
571
1064
  promotedAt =
572
1065
  approvedBy != null
573
1066
  ? Date.now()
574
- : typeof existing.promoted_at === 'number'
575
- ? existing.promoted_at
1067
+ : typeof row.promoted_at === 'number'
1068
+ ? row.promoted_at
576
1069
  : Date.now();
577
1070
  promotedBy =
578
1071
  approvedBy ??
579
- (existing.promoted_by != null ? String(existing.promoted_by) : null);
580
- }
581
- const implementationSource = tool.implementation.mode === 'sandbox'
582
- ? this.serializeSandboxImplementation(tool)
583
- : JSON.stringify(tool.implementation);
584
- await this.db.run(`INSERT OR REPLACE INTO agentos_emergent_tools
585
- (id, name, description, input_schema, output_schema, implementation_mode,
586
- implementation_source, tier, created_by_agent, created_by_session,
587
- created_at, promoted_at, promoted_by, judge_verdicts, confidence_score,
588
- total_uses, success_count, failure_count, avg_execution_ms, last_used_at,
589
- is_active)
590
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1072
+ (row.promoted_by != null ? String(row.promoted_by) : null);
1073
+ }
1074
+ const implementationSource = await this.resolveSourceToStore(tool);
1075
+ // The legacy flag. For a state this process holds it follows the state
1076
+ // row, read inside the statement (a state change that landed during the
1077
+ // reads above is the newer word), and a flag the host lowered with its own
1078
+ // SQL stays lowered until a load records the host's decision or the host
1079
+ // reactivates the tool. A process that holds no state writes 1 for a new
1080
+ // row and leaves an existing row's flag as it stands at write time, not
1081
+ // as it read it earlier: a whole-row write is never a reactivation, and
1082
+ // never a disable either.
1083
+ const heldNow = this.states.get(tool.id);
1084
+ const flagExpr = heldNow
1085
+ ? `COALESCE((SELECT CASE WHEN state = 'active' THEN 1 ELSE 0 END
1086
+ FROM agentos_emergent_tool_state WHERE tool_id = ?), ?)`
1087
+ : `1`;
1088
+ const flagParams = heldNow ? [tool.id, heldNow.state === 'active' ? 1 : 0] : [];
1089
+ const flagOnConflict = heldNow
1090
+ ? `CASE WHEN agentos_emergent_tools.is_active = 0 THEN 0 ELSE excluded.is_active END`
1091
+ : `agentos_emergent_tools.is_active`;
1092
+ const columns = [
1093
+ 'id', 'name', 'description', 'input_schema', 'output_schema', 'implementation_mode',
1094
+ 'implementation_source', 'tier', 'created_by_agent', 'created_by_session',
1095
+ 'created_at', 'promoted_at', 'promoted_by', 'judge_verdicts', 'confidence_score',
1096
+ 'total_uses', 'success_count', 'failure_count', 'avg_execution_ms', 'last_used_at',
1097
+ ];
1098
+ await this.db.run(`INSERT INTO agentos_emergent_tools
1099
+ (${columns.join(', ')}, is_active)
1100
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${flagExpr})
1101
+ ON CONFLICT (id) DO UPDATE SET
1102
+ ${columns.slice(1).map((column) => `${column} = excluded.${column}`).join(',\n ')},
1103
+ is_active = ${flagOnConflict}`, [
591
1104
  tool.id,
592
1105
  tool.name,
593
1106
  tool.description,
@@ -610,9 +1123,34 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
610
1123
  tool.usageStats.lastUsedAt
611
1124
  ? new Date(tool.usageStats.lastUsedAt).getTime()
612
1125
  : null,
613
- 1,
1126
+ // The flag of a new row; on conflict, see flagOnConflict above.
1127
+ ...flagParams,
614
1128
  ]);
615
1129
  }
1130
+ /**
1131
+ * The `implementation_source` to write for a tool. With source persistence
1132
+ * off the registry writes the redacted record for a fresh forge, or over an
1133
+ * earlier redacted record, and never over any other stored source: that
1134
+ * source was put there by a host, or while persistence was on, and
1135
+ * replacing it would destroy the tool.
1136
+ */
1137
+ async resolveSourceToStore(tool) {
1138
+ if (tool.implementation.mode !== 'sandbox') {
1139
+ return JSON.stringify(tool.implementation);
1140
+ }
1141
+ if (this.config.persistSandboxSource || !this.db) {
1142
+ return this.serializeSandboxImplementation(tool);
1143
+ }
1144
+ const existing = (await this.db.get(`SELECT implementation_source
1145
+ FROM agentos_emergent_tools
1146
+ WHERE id = ?
1147
+ LIMIT 1`, [tool.id]));
1148
+ const stored = existing?.implementation_source;
1149
+ if (typeof stored === 'string' && parsePersistedSource('sandbox', stored).format !== 'redacted') {
1150
+ return stored;
1151
+ }
1152
+ return this.serializeSandboxImplementation(tool);
1153
+ }
616
1154
  serializeSandboxImplementation(tool) {
617
1155
  if (tool.implementation.mode !== 'sandbox') {
618
1156
  return JSON.stringify(tool.implementation);
@@ -629,4 +1167,15 @@ CREATE TABLE IF NOT EXISTS agentos_emergent_audit_log (
629
1167
  return JSON.stringify(metadata);
630
1168
  }
631
1169
  }
1170
+ EmergentToolRegistry.ROW_COLUMNS = `
1171
+ t.id, t.name, t.description, t.input_schema, t.output_schema,
1172
+ t.implementation_mode, t.implementation_source, t.tier,
1173
+ t.created_by_agent, t.created_by_session, t.created_at,
1174
+ t.judge_verdicts, t.confidence_score, t.total_uses, t.success_count,
1175
+ t.failure_count, t.avg_execution_ms, t.last_used_at, t.is_active,
1176
+ s.state AS state, s.state_reason AS state_reason, s.set_by AS set_by,
1177
+ s.state_at AS state_at, s.request_json AS request_json,
1178
+ s.flag_synced AS flag_synced, s.write_id AS write_id
1179
+ FROM agentos_emergent_tools t
1180
+ LEFT JOIN agentos_emergent_tool_state s ON s.tool_id = t.id`;
632
1181
  //# sourceMappingURL=EmergentToolRegistry.js.map