@framers/agentos 0.10.39 → 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.
- package/README.md +1 -1
- package/dist/api/agency.d.ts.map +1 -1
- package/dist/api/agency.js +140 -114
- package/dist/api/agency.js.map +1 -1
- package/dist/api/agent.d.ts +9 -5
- package/dist/api/agent.d.ts.map +1 -1
- package/dist/api/agent.js +7 -5
- package/dist/api/agent.js.map +1 -1
- package/dist/api/agentExport.d.ts +60 -21
- package/dist/api/agentExport.d.ts.map +1 -1
- package/dist/api/agentExport.js +240 -41
- package/dist/api/agentExport.js.map +1 -1
- package/dist/api/agentExportCore.d.ts +44 -6
- package/dist/api/agentExportCore.d.ts.map +1 -1
- package/dist/api/agentExportCore.js +43 -8
- package/dist/api/agentExportCore.js.map +1 -1
- package/dist/api/agentExportRedact.d.ts +58 -0
- package/dist/api/agentExportRedact.d.ts.map +1 -0
- package/dist/api/agentExportRedact.js +219 -0
- package/dist/api/agentExportRedact.js.map +1 -0
- package/dist/api/generateText.d.ts +11 -0
- package/dist/api/generateText.d.ts.map +1 -1
- package/dist/api/generateText.js +27 -0
- package/dist/api/generateText.js.map +1 -1
- package/dist/api/model.d.ts +12 -0
- package/dist/api/model.d.ts.map +1 -1
- package/dist/api/model.js +11 -0
- package/dist/api/model.js.map +1 -1
- package/dist/api/runtime/approval-gate.d.ts +92 -0
- package/dist/api/runtime/approval-gate.d.ts.map +1 -0
- package/dist/api/runtime/approval-gate.js +182 -0
- package/dist/api/runtime/approval-gate.js.map +1 -0
- package/dist/api/runtime/tool-emulation/loop.d.ts +16 -0
- package/dist/api/runtime/tool-emulation/loop.d.ts.map +1 -1
- package/dist/api/runtime/tool-emulation/loop.js +25 -3
- package/dist/api/runtime/tool-emulation/loop.js.map +1 -1
- package/dist/api/streamText.d.ts.map +1 -1
- package/dist/api/streamText.js +33 -0
- package/dist/api/streamText.js.map +1 -1
- package/dist/api/types.d.ts +23 -4
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/types.js.map +1 -1
- package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts +120 -32
- package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts.map +1 -1
- package/dist/cognition/emergent/EmergentCapabilityEngine.js +602 -19
- package/dist/cognition/emergent/EmergentCapabilityEngine.js.map +1 -1
- package/dist/cognition/emergent/EmergentToolRegistry.d.ts +180 -38
- package/dist/cognition/emergent/EmergentToolRegistry.d.ts.map +1 -1
- package/dist/cognition/emergent/EmergentToolRegistry.js +645 -96
- package/dist/cognition/emergent/EmergentToolRegistry.js.map +1 -1
- package/dist/cognition/emergent/ForgeToolMetaTool.js +5 -5
- package/dist/cognition/emergent/ForgeToolMetaTool.js.map +1 -1
- package/dist/cognition/emergent/capabilities.d.ts +27 -0
- package/dist/cognition/emergent/capabilities.d.ts.map +1 -0
- package/dist/cognition/emergent/capabilities.js +50 -0
- package/dist/cognition/emergent/capabilities.js.map +1 -0
- package/dist/cognition/emergent/index.d.ts +4 -1
- package/dist/cognition/emergent/index.d.ts.map +1 -1
- package/dist/cognition/emergent/index.js +2 -0
- package/dist/cognition/emergent/index.js.map +1 -1
- package/dist/cognition/emergent/persisted-source.d.ts +106 -0
- package/dist/cognition/emergent/persisted-source.d.ts.map +1 -0
- package/dist/cognition/emergent/persisted-source.js +370 -0
- package/dist/cognition/emergent/persisted-source.js.map +1 -0
- package/dist/cognition/emergent/types.d.ts +91 -1
- package/dist/cognition/emergent/types.d.ts.map +1 -1
- package/dist/cognition/emergent/types.js.map +1 -1
- package/dist/cognition/emergent/wrapForgeTool.d.ts +4 -1
- package/dist/cognition/emergent/wrapForgeTool.d.ts.map +1 -1
- package/dist/cognition/emergent/wrapForgeTool.js +3 -0
- package/dist/cognition/emergent/wrapForgeTool.js.map +1 -1
- package/dist/cognition/substrate/GMI.d.ts +2 -0
- package/dist/cognition/substrate/GMI.d.ts.map +1 -1
- package/dist/cognition/substrate/GMI.js +9 -3
- package/dist/cognition/substrate/GMI.js.map +1 -1
- package/dist/cognition/substrate/GMIManager.d.ts +1 -1
- package/dist/cognition/substrate/GMIManager.d.ts.map +1 -1
- package/dist/cognition/substrate/GMIManager.js +2 -0
- package/dist/cognition/substrate/GMIManager.js.map +1 -1
- package/dist/cognition/substrate/IGMI.d.ts +7 -0
- package/dist/cognition/substrate/IGMI.d.ts.map +1 -1
- package/dist/cognition/substrate/IGMI.js.map +1 -1
- package/dist/cognition/substrate/personas/IPersonaDefinition.d.ts +15 -0
- package/dist/cognition/substrate/personas/IPersonaDefinition.d.ts.map +1 -1
- package/dist/cognition/substrate/personas/PersonaValidation.d.ts +0 -12
- package/dist/cognition/substrate/personas/PersonaValidation.d.ts.map +1 -1
- package/dist/cognition/substrate/personas/PersonaValidation.js +40 -0
- package/dist/cognition/substrate/personas/PersonaValidation.js.map +1 -1
- package/dist/cognition/substrate/personas/SoulLoader.d.ts +5 -0
- package/dist/cognition/substrate/personas/SoulLoader.d.ts.map +1 -1
- package/dist/cognition/substrate/personas/SoulLoader.js +8 -0
- package/dist/cognition/substrate/personas/SoulLoader.js.map +1 -1
- package/dist/cognition/substrate/reasoningTraceLimits.d.ts +72 -0
- package/dist/cognition/substrate/reasoningTraceLimits.d.ts.map +1 -0
- package/dist/cognition/substrate/reasoningTraceLimits.js +105 -0
- package/dist/cognition/substrate/reasoningTraceLimits.js.map +1 -0
- package/dist/core/llm/providers/url-secrets.d.ts +21 -0
- package/dist/core/llm/providers/url-secrets.d.ts.map +1 -1
- package/dist/core/llm/providers/url-secrets.js +72 -0
- package/dist/core/llm/providers/url-secrets.js.map +1 -1
- package/dist/core/tools/ToolOrchestrator.d.ts +2 -0
- package/dist/core/tools/ToolOrchestrator.d.ts.map +1 -1
- package/dist/core/tools/ToolOrchestrator.js +25 -2
- package/dist/core/tools/ToolOrchestrator.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- 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
|
|
113
|
-
* tables along with their indexes. Safe to
|
|
114
|
-
* statements use `CREATE TABLE 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(
|
|
170
|
+
await this.db.exec(statements.join('\n'));
|
|
162
171
|
}
|
|
163
172
|
else {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
243
|
-
*
|
|
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
|
-
|
|
817
|
+
const usedAtMs = Date.now();
|
|
818
|
+
stats.lastUsedAt = new Date(usedAtMs).toISOString();
|
|
353
819
|
if (this.db && this.schemaReady) {
|
|
354
|
-
|
|
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)
|
|
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`
|
|
465
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
//
|
|
562
|
-
|
|
563
|
-
const sessionId =
|
|
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
|
|
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
|
|
575
|
-
?
|
|
1067
|
+
: typeof row.promoted_at === 'number'
|
|
1068
|
+
? row.promoted_at
|
|
576
1069
|
: Date.now();
|
|
577
1070
|
promotedBy =
|
|
578
1071
|
approvedBy ??
|
|
579
|
-
(
|
|
580
|
-
}
|
|
581
|
-
const implementationSource = tool
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
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
|