@framers/agentos 0.10.40 → 0.11.1
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 +44 -5
- package/dist/api/model.d.ts.map +1 -1
- package/dist/api/model.js +84 -8
- 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/strategies/shared.d.ts.map +1 -1
- package/dist/api/runtime/strategies/shared.js +21 -4
- package/dist/api/runtime/strategies/shared.js.map +1 -1
- 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/rag/multimodal/createMultimodalIndexerFromResolver.d.ts +5 -3
- package/dist/cognition/rag/multimodal/createMultimodalIndexerFromResolver.d.ts.map +1 -1
- package/dist/cognition/rag/multimodal/createMultimodalIndexerFromResolver.js +5 -3
- package/dist/cognition/rag/multimodal/createMultimodalIndexerFromResolver.js.map +1 -1
- package/dist/core/llm/providers/implementations/OpenRouterProvider.js +1 -1
- package/dist/core/llm/providers/implementations/OpenRouterProvider.js.map +1 -1
- 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/dist/io/hearing/providers/DeepgramBatchSTTProvider.d.ts +13 -5
- package/dist/io/hearing/providers/DeepgramBatchSTTProvider.d.ts.map +1 -1
- package/dist/io/hearing/providers/DeepgramBatchSTTProvider.js +4 -3
- package/dist/io/hearing/providers/DeepgramBatchSTTProvider.js.map +1 -1
- package/dist/io/hearing/providers/OpenAIWhisperSpeechToTextProvider.d.ts +40 -14
- package/dist/io/hearing/providers/OpenAIWhisperSpeechToTextProvider.d.ts.map +1 -1
- package/dist/io/hearing/providers/OpenAIWhisperSpeechToTextProvider.js +108 -18
- package/dist/io/hearing/providers/OpenAIWhisperSpeechToTextProvider.js.map +1 -1
- package/dist/io/media/images/imageToBuffer.d.ts +10 -4
- package/dist/io/media/images/imageToBuffer.d.ts.map +1 -1
- package/dist/io/media/images/imageToBuffer.js +62 -5
- package/dist/io/media/images/imageToBuffer.js.map +1 -1
- package/dist/io/media/video/VideoAnalyzer.d.ts.map +1 -1
- package/dist/io/media/video/VideoAnalyzer.js +4 -1
- package/dist/io/media/video/VideoAnalyzer.js.map +1 -1
- package/dist/io/speech/SpeechProviderResolver.d.ts +11 -9
- package/dist/io/speech/SpeechProviderResolver.d.ts.map +1 -1
- package/dist/io/speech/SpeechProviderResolver.js +18 -12
- package/dist/io/speech/SpeechProviderResolver.js.map +1 -1
- package/dist/io/speech/SpeechRuntime.d.ts.map +1 -1
- package/dist/io/speech/SpeechRuntime.js +9 -1
- package/dist/io/speech/SpeechRuntime.js.map +1 -1
- package/dist/io/speech/providerCatalog.d.ts.map +1 -1
- package/dist/io/speech/providerCatalog.js +6 -2
- package/dist/io/speech/providerCatalog.js.map +1 -1
- package/dist/io/speech/providers/OpenAITextToSpeechProvider.d.ts +5 -0
- package/dist/io/speech/providers/OpenAITextToSpeechProvider.d.ts.map +1 -1
- package/dist/io/speech/providers/OpenAITextToSpeechProvider.js.map +1 -1
- package/dist/io/voice-pipeline/providers/CartesiaBatchTTS.d.ts +11 -3
- package/dist/io/voice-pipeline/providers/CartesiaBatchTTS.d.ts.map +1 -1
- package/dist/io/voice-pipeline/providers/CartesiaBatchTTS.js +11 -3
- package/dist/io/voice-pipeline/providers/CartesiaBatchTTS.js.map +1 -1
- package/dist/io/voice-pipeline/providers/CartesiaStreamingTTS.d.ts +1 -1
- package/dist/io/voice-pipeline/providers/CartesiaStreamingTTS.d.ts.map +1 -1
- package/dist/io/voice-pipeline/providers/CartesiaStreamingTTS.js +2 -2
- package/dist/io/voice-pipeline/providers/CartesiaStreamingTTS.js.map +1 -1
- package/dist/io/voice-pipeline/providers/DeepgramPreRecordedBatchSTT.d.ts +8 -0
- package/dist/io/voice-pipeline/providers/DeepgramPreRecordedBatchSTT.d.ts.map +1 -1
- package/dist/io/voice-pipeline/providers/DeepgramPreRecordedBatchSTT.js +3 -0
- package/dist/io/voice-pipeline/providers/DeepgramPreRecordedBatchSTT.js.map +1 -1
- package/dist/io/voice-pipeline/providers/DeepgramStreamingSTT.d.ts +11 -3
- package/dist/io/voice-pipeline/providers/DeepgramStreamingSTT.d.ts.map +1 -1
- package/dist/io/voice-pipeline/providers/DeepgramStreamingSTT.js +22 -3
- package/dist/io/voice-pipeline/providers/DeepgramStreamingSTT.js.map +1 -1
- package/dist/io/voice-pipeline/providers/ElevenLabsStreamingSTT.d.ts +3 -2
- package/dist/io/voice-pipeline/providers/ElevenLabsStreamingSTT.d.ts.map +1 -1
- package/dist/io/voice-pipeline/providers/ElevenLabsStreamingSTT.js +7 -4
- package/dist/io/voice-pipeline/providers/ElevenLabsStreamingSTT.js.map +1 -1
- package/dist/io/voice-pipeline/providers/OpenAIWhisperBatchSTT.d.ts +5 -3
- package/dist/io/voice-pipeline/providers/OpenAIWhisperBatchSTT.d.ts.map +1 -1
- package/dist/io/voice-pipeline/providers/OpenAIWhisperBatchSTT.js +24 -6
- package/dist/io/voice-pipeline/providers/OpenAIWhisperBatchSTT.js.map +1 -1
- package/package.json +1 -1
|
@@ -13,6 +13,18 @@
|
|
|
13
13
|
* After registration the engine tracks usage and auto-promotes tools that
|
|
14
14
|
* meet the configured `EmergentConfig.promotionThreshold` criteria.
|
|
15
15
|
*/
|
|
16
|
+
import { parsePersistedSource, parseRowSchemas, parseStoredRequest, requestFromImplementation, sessionFromSource, requestFromSource, sourceFromImplementation, stateSetterFromColumn, toolFromRow, } from './persisted-source.js';
|
|
17
|
+
import { normalizeAllowlist, toSandboxApis } from './capabilities.js';
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// DEPENDENCY BUNDLE
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// STORED TOOLS
|
|
23
|
+
// ============================================================================
|
|
24
|
+
/** The configuration key behind a reason, named in the start-up line. */
|
|
25
|
+
const REASON_CONFIG_KEYS = {
|
|
26
|
+
source_not_persisted: 'emergent.persistSandboxSource',
|
|
27
|
+
};
|
|
16
28
|
// ============================================================================
|
|
17
29
|
// ENGINE
|
|
18
30
|
// ============================================================================
|
|
@@ -41,6 +53,19 @@
|
|
|
41
53
|
* }
|
|
42
54
|
* ```
|
|
43
55
|
*/
|
|
56
|
+
/** Whether two stored requests grant the same thing. */
|
|
57
|
+
function sameGrant(a, b) {
|
|
58
|
+
if (!a || !b) {
|
|
59
|
+
return !a && !b;
|
|
60
|
+
}
|
|
61
|
+
if (a.kind !== b.kind) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (a.kind === 'sandbox' && b.kind === 'sandbox') {
|
|
65
|
+
return a.capabilities.join(',') === b.capabilities.join(',');
|
|
66
|
+
}
|
|
67
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
68
|
+
}
|
|
44
69
|
export class EmergentCapabilityEngine {
|
|
45
70
|
/**
|
|
46
71
|
* Create a new EmergentCapabilityEngine.
|
|
@@ -136,7 +161,17 @@ export class EmergentCapabilityEngine {
|
|
|
136
161
|
};
|
|
137
162
|
}
|
|
138
163
|
source = request.implementation.code;
|
|
139
|
-
// Step 2a:
|
|
164
|
+
// Step 2a: the list must name catalogue capabilities (or the alias), so
|
|
165
|
+
// the request stored for the tool is the list the judge reviews, never a
|
|
166
|
+
// narrowed reading of it.
|
|
167
|
+
const list = normalizeAllowlist(request.implementation.allowlist);
|
|
168
|
+
if (list.unknown.length > 0) {
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
error: `allowlist names capabilities outside the catalogue: ${list.unknown.join(', ')}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// Step 2b: Static code validation before any execution.
|
|
140
175
|
const validation = this.sandboxForge.validateCode(request.implementation.code, request.implementation.allowlist);
|
|
141
176
|
if (!validation.valid) {
|
|
142
177
|
return {
|
|
@@ -199,6 +234,43 @@ export class EmergentCapabilityEngine {
|
|
|
199
234
|
source: `forged by agent ${context.agentId} during session ${context.sessionId}`,
|
|
200
235
|
};
|
|
201
236
|
this.registry.register(tool, 'session');
|
|
237
|
+
let written;
|
|
238
|
+
try {
|
|
239
|
+
written = await this.registry.setState(toolId, 'active', null, {
|
|
240
|
+
request: requestFromImplementation(request.implementation),
|
|
241
|
+
setBy: 'library',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
// The tool runs in this process either way; without the row its request
|
|
246
|
+
// is re-derived from its source at the next load.
|
|
247
|
+
console.warn(`[agentos:emergent] could not store the request of "${request.name}" (${toolId}):`, error instanceof Error ? error.message : error);
|
|
248
|
+
}
|
|
249
|
+
if (this.registry.wasRemoved(toolId)) {
|
|
250
|
+
// Removed while it was being forged: nothing is registered.
|
|
251
|
+
return { success: false, error: 'the tool was removed while it was being forged' };
|
|
252
|
+
}
|
|
253
|
+
if (written && written.state === 'demoted' && written.reason === 'removed') {
|
|
254
|
+
// The row did not take the state (its tool row never landed): the tool
|
|
255
|
+
// still runs in this process, held active here, and the next load
|
|
256
|
+
// re-derives its request from its source.
|
|
257
|
+
console.warn(`[agentos:emergent] the state of "${request.name}" (${toolId}) was not stored; ` +
|
|
258
|
+
'the tool runs in this process only');
|
|
259
|
+
this.registry.adopt(tool, {
|
|
260
|
+
toolId,
|
|
261
|
+
state: 'active',
|
|
262
|
+
reason: null,
|
|
263
|
+
setBy: 'library',
|
|
264
|
+
at: Date.now(),
|
|
265
|
+
request: requestFromImplementation(request.implementation),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
else if (written && written.state !== 'active') {
|
|
269
|
+
// Another word arrived first (a host's suspension): it holds, and the
|
|
270
|
+
// executable refuses calls until the host lifts it.
|
|
271
|
+
console.warn(`[agentos:emergent] "${request.name}" (${toolId}) was ${written.state} before its forge finished ` +
|
|
272
|
+
`(${written.reason ?? 'no reason recorded'})`);
|
|
273
|
+
}
|
|
202
274
|
this.indexTool(toolId, context.agentId, context.sessionId);
|
|
203
275
|
if (this.onToolForged) {
|
|
204
276
|
try {
|
|
@@ -334,38 +406,519 @@ export class EmergentCapabilityEngine {
|
|
|
334
406
|
return removedTools;
|
|
335
407
|
}
|
|
336
408
|
/**
|
|
337
|
-
* Hydrate
|
|
409
|
+
* Hydrate one stored tool and make it executable.
|
|
410
|
+
*
|
|
411
|
+
* @deprecated Use {@link loadPersistedTools}, which reads the rows itself.
|
|
412
|
+
* This applies the same checks to the one tool: a suspended or demoted tool
|
|
413
|
+
* is not registered, and a tool whose source cannot be rebuilt is suspended.
|
|
414
|
+
* When the tool has a stored row, that row is what is read, not the object
|
|
415
|
+
* passed in (a host-built object can carry a list the host made up), and a
|
|
416
|
+
* load never rewrites it. A tool with no row gets its row written first, as
|
|
417
|
+
* before, so its uses are recorded and the next load finds it.
|
|
338
418
|
*
|
|
339
|
-
*
|
|
340
|
-
* durable storage into a running ToolOrchestrator after promotion or restart.
|
|
419
|
+
* @returns what happened, so a host can tell a registered tool from a refused one.
|
|
341
420
|
*/
|
|
342
421
|
async syncPersistedTool(tool) {
|
|
343
|
-
this.registry.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
422
|
+
let row = await this.registry.loadRow(tool.id);
|
|
423
|
+
if (!row && this.registry.hasStorage()) {
|
|
424
|
+
// No stored row yet: the host hydrates from its own store. The row is
|
|
425
|
+
// written as it was before loading went through the stored row, so the
|
|
426
|
+
// tool's uses are recorded and the next load finds it.
|
|
427
|
+
await this.registry.writeToolRow(tool);
|
|
428
|
+
row = await this.registry.loadRow(tool.id);
|
|
348
429
|
}
|
|
349
|
-
if (
|
|
350
|
-
|
|
430
|
+
if (row) {
|
|
431
|
+
// With source persistence off the row holds a redacted record; the
|
|
432
|
+
// implementation the host supplied is what runs then.
|
|
433
|
+
return this.admitRow(row, { sourceFallback: sourceFromImplementation(tool.implementation) });
|
|
351
434
|
}
|
|
435
|
+
const stored = await this.registry.readState(tool.id);
|
|
436
|
+
return this.admit({
|
|
437
|
+
toolId: tool.id,
|
|
438
|
+
name: tool.name,
|
|
439
|
+
source: sourceFromImplementation(tool.implementation),
|
|
440
|
+
stored,
|
|
441
|
+
requestStored: stored?.request != null,
|
|
442
|
+
legacyActive: tool.isActive ?? true,
|
|
443
|
+
buildTool: () => tool,
|
|
444
|
+
});
|
|
352
445
|
}
|
|
353
446
|
/**
|
|
354
447
|
* Remove a previously synced tool from the live runtime and registry.
|
|
355
448
|
*/
|
|
356
449
|
async removeTool(toolId) {
|
|
357
450
|
const tool = this.registry.get(toolId);
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
451
|
+
// The rows go whether or not the tool is loaded here, after the tool's
|
|
452
|
+
// queued state writes; a sync of the same id after this returns finds no
|
|
453
|
+
// row, as a removal promises.
|
|
361
454
|
this.registry.remove(toolId);
|
|
455
|
+
await this.registry.settled(toolId);
|
|
362
456
|
this.removeIndexedToolEverywhere(toolId);
|
|
363
|
-
if (this.onToolRemoved) {
|
|
457
|
+
if (tool && this.onToolRemoved) {
|
|
364
458
|
await this.onToolRemoved(tool);
|
|
365
459
|
}
|
|
366
460
|
return tool;
|
|
367
461
|
}
|
|
368
462
|
// --------------------------------------------------------------------------
|
|
463
|
+
// PUBLIC: stored tools
|
|
464
|
+
// --------------------------------------------------------------------------
|
|
465
|
+
/**
|
|
466
|
+
* Load the stored tools of the given tiers into the running process.
|
|
467
|
+
*
|
|
468
|
+
* Call it at start, after the host's own tools are registered. For each row
|
|
469
|
+
* it reads the source (raw code, code with its list, a redacted record, or a
|
|
470
|
+
* composition), keeps a demoted row off, suspends a row that cannot be
|
|
471
|
+
* rebuilt, and registers the rest. A row is never rewritten by being loaded,
|
|
472
|
+
* and a stored request is never replaced by a derived one. One line is logged
|
|
473
|
+
* per tool that did not load. A row whose load throws is reported in
|
|
474
|
+
* `failed` and does not stop the others.
|
|
475
|
+
*
|
|
476
|
+
* `shared` rows load for every caller. `agent` rows are those of
|
|
477
|
+
* `options.agentId` and `session` rows those of `options.sessionId`, and
|
|
478
|
+
* naming either tier without its selector throws (`selector_required`), so
|
|
479
|
+
* a shared store never puts one agent's private tools in another's
|
|
480
|
+
* executor. A loaded or forged `agent` tool also refuses a call from any
|
|
481
|
+
* other agent (see {@link createExecutableTool}). The agent identity is the
|
|
482
|
+
* persona id: `forge_tool` records the forging caller's `personaId` as
|
|
483
|
+
* `created_by_agent`, and `agentId` here is that id. Rows written by
|
|
484
|
+
* releases before this one hold the forging GMI instance's id instead.
|
|
485
|
+
*/
|
|
486
|
+
async loadPersistedTools(options) {
|
|
487
|
+
const { tiers, agentId, sessionId } = options;
|
|
488
|
+
if (tiers.includes('agent') && agentId === undefined) {
|
|
489
|
+
throw new Error("selector_required: loadPersistedTools({ tiers: ['agent'] }) needs the agentId whose tools to load; " +
|
|
490
|
+
"an agent's tools are private to it.");
|
|
491
|
+
}
|
|
492
|
+
if (tiers.includes('session') && sessionId === undefined) {
|
|
493
|
+
throw new Error("selector_required: loadPersistedTools({ tiers: ['session'] }) needs the sessionId whose tools to load; " +
|
|
494
|
+
"a session's tools are private to it.");
|
|
495
|
+
}
|
|
496
|
+
const rows = await this.registry.loadRows(tiers, { agentId, sessionId });
|
|
497
|
+
const outcomes = [];
|
|
498
|
+
const failed = [];
|
|
499
|
+
for (const row of rows) {
|
|
500
|
+
try {
|
|
501
|
+
outcomes.push(await this.admitRow(row));
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
505
|
+
failed.push({ toolId: row.id, name: row.name, error: message });
|
|
506
|
+
console.warn(`[agentos:emergent] stored tool "${row.name}" (${row.id}) did not load: ${message}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for (const outcome of outcomes) {
|
|
510
|
+
if (outcome.state === 'active')
|
|
511
|
+
continue;
|
|
512
|
+
const key = outcome.reason ? REASON_CONFIG_KEYS[outcome.reason] : undefined;
|
|
513
|
+
console.warn(`[agentos:emergent] stored tool "${outcome.name}" (${outcome.toolId}) is ${outcome.state}: ` +
|
|
514
|
+
`${outcome.reason ?? 'no reason recorded'}${key ? ` (see ${key})` : ''}`);
|
|
515
|
+
}
|
|
516
|
+
const count = (state) => outcomes.filter((o) => o.state === state).length;
|
|
517
|
+
return {
|
|
518
|
+
active: count('active'),
|
|
519
|
+
suspended: count('suspended'),
|
|
520
|
+
demoted: count('demoted'),
|
|
521
|
+
outcomes,
|
|
522
|
+
failed,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Suspend a tool and take it out of the executor. The state write is
|
|
527
|
+
* awaited, so the suspension survives a restart, and no later use, rewrite
|
|
528
|
+
* or load turns the tool back on. The suspension is recorded as the host's,
|
|
529
|
+
* so it stays, whatever its reason says, until {@link reactivateTool} is
|
|
530
|
+
* called.
|
|
531
|
+
*
|
|
532
|
+
* @returns `false` when the tool is unknown.
|
|
533
|
+
*/
|
|
534
|
+
async suspendTool(toolId, reason) {
|
|
535
|
+
const tool = this.registry.get(toolId);
|
|
536
|
+
if (!tool) {
|
|
537
|
+
// Not loaded in this process: the stored row is still suspended, so the
|
|
538
|
+
// next load keeps it off. The stored request is left as it is.
|
|
539
|
+
const row = await this.registry.loadRow(toolId);
|
|
540
|
+
if (!row) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
await this.registry.setState(toolId, 'suspended', reason, { setBy: 'host' });
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
await this.registry.suspend(toolId, reason);
|
|
547
|
+
if (this.onToolRemoved) {
|
|
548
|
+
await this.onToolRemoved(tool);
|
|
549
|
+
}
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Demote a tool: its confidence is reset, it is taken out of the executor,
|
|
554
|
+
* and no later load brings it back. Only {@link reactivateTool} does.
|
|
555
|
+
*
|
|
556
|
+
* @returns `false` when the tool is unknown.
|
|
557
|
+
*/
|
|
558
|
+
async demoteTool(toolId, reason) {
|
|
559
|
+
const tool = this.registry.get(toolId);
|
|
560
|
+
if (!tool) {
|
|
561
|
+
const row = await this.registry.loadRow(toolId);
|
|
562
|
+
if (!row) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
await this.registry.setState(toolId, 'demoted', reason, { setBy: 'host' });
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
await this.registry.demote(toolId, reason);
|
|
569
|
+
if (this.onToolRemoved) {
|
|
570
|
+
await this.onToolRemoved(tool);
|
|
571
|
+
}
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Re-check one suspended or demoted tool against the configuration in force
|
|
576
|
+
* and, when it fits, register it again. This is how a host clears a
|
|
577
|
+
* suspension or a demotion it set; the library never does so on its own.
|
|
578
|
+
*
|
|
579
|
+
* @returns the outcome, or `undefined` when the tool is unknown.
|
|
580
|
+
*/
|
|
581
|
+
async reactivateTool(toolId) {
|
|
582
|
+
const row = await this.registry.loadRow(toolId);
|
|
583
|
+
if (row) {
|
|
584
|
+
return this.admitRow(row, { force: true });
|
|
585
|
+
}
|
|
586
|
+
const tool = this.registry.get(toolId);
|
|
587
|
+
if (!tool) {
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
const held = this.registry.getState(toolId);
|
|
591
|
+
return this.admit({
|
|
592
|
+
toolId,
|
|
593
|
+
name: tool.name,
|
|
594
|
+
source: sourceFromImplementation(tool.implementation),
|
|
595
|
+
stored: held,
|
|
596
|
+
requestStored: held?.request != null,
|
|
597
|
+
legacyActive: true,
|
|
598
|
+
buildTool: () => tool,
|
|
599
|
+
}, { force: true });
|
|
600
|
+
}
|
|
601
|
+
admitRow(row, options = {}) {
|
|
602
|
+
const stored = row.state
|
|
603
|
+
? {
|
|
604
|
+
toolId: row.id,
|
|
605
|
+
state: row.state,
|
|
606
|
+
reason: row.state_reason ?? null,
|
|
607
|
+
setBy: stateSetterFromColumn(row.set_by),
|
|
608
|
+
at: Number(row.state_at ?? 0),
|
|
609
|
+
request: parseStoredRequest(row.request_json),
|
|
610
|
+
...(row.write_id ? { writeId: row.write_id } : {}),
|
|
611
|
+
}
|
|
612
|
+
: undefined;
|
|
613
|
+
// A row whose schema columns do not read cannot be built: it is unreadable
|
|
614
|
+
// before its source is looked at, so a damaged input_schema never widens
|
|
615
|
+
// to a schema that accepts any input.
|
|
616
|
+
const schemas = parseRowSchemas(row);
|
|
617
|
+
const source = 'error' in schemas
|
|
618
|
+
? { format: 'unreadable', error: schemas.error }
|
|
619
|
+
: parsePersistedSource(row.implementation_mode, row.implementation_source);
|
|
620
|
+
// A redacted record cannot be rebuilt from the row; a host that supplies
|
|
621
|
+
// the implementation (syncPersistedTool) runs from what it supplied, as it
|
|
622
|
+
// did before loading went through the row. State, request and the flag
|
|
623
|
+
// still come from the row.
|
|
624
|
+
const sourceInUse = source.format === 'redacted' && options.sourceFallback && options.sourceFallback.format !== 'unreadable'
|
|
625
|
+
? options.sourceFallback
|
|
626
|
+
: source;
|
|
627
|
+
return this.admit({
|
|
628
|
+
toolId: row.id,
|
|
629
|
+
name: row.name,
|
|
630
|
+
source: sourceInUse,
|
|
631
|
+
stored,
|
|
632
|
+
requestStored: row.request_json != null,
|
|
633
|
+
legacyActive: !(row.is_active === 0 || row.is_active === false),
|
|
634
|
+
flagSynced: row.flag_synced == null ? undefined : !(row.flag_synced === 0 || row.flag_synced === false),
|
|
635
|
+
buildTool: (implementation) => toolFromRow(row, implementation),
|
|
636
|
+
}, options);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* The single-row path. Every stored tool goes through it, whether the
|
|
640
|
+
* library read its row or a host built the tool from its own.
|
|
641
|
+
*/
|
|
642
|
+
/**
|
|
643
|
+
* The row changed under an admission (another process wrote, or the host
|
|
644
|
+
* acted): admit the tool again from the row as it stands, up to twice. A
|
|
645
|
+
* row that is gone reads as removed; a row still changing after two
|
|
646
|
+
* re-admissions is left for the next load.
|
|
647
|
+
*/
|
|
648
|
+
async readmit(candidate, options) {
|
|
649
|
+
const { toolId, name } = candidate;
|
|
650
|
+
const depth = options.readmitted ?? 0;
|
|
651
|
+
if (depth < 2) {
|
|
652
|
+
const fresh = await this.registry.loadRow(toolId);
|
|
653
|
+
if (fresh) {
|
|
654
|
+
return this.admitRow(fresh, { ...options, readmitted: depth + 1 });
|
|
655
|
+
}
|
|
656
|
+
await this.unregisterIfLive(toolId);
|
|
657
|
+
return { toolId, name, state: 'demoted', reason: 'removed' };
|
|
658
|
+
}
|
|
659
|
+
// Nothing is written; the tool is held off here until the next load.
|
|
660
|
+
this.holdStored(toolId, {
|
|
661
|
+
toolId,
|
|
662
|
+
state: 'suspended',
|
|
663
|
+
reason: 'contended',
|
|
664
|
+
setBy: 'library',
|
|
665
|
+
at: Date.now(),
|
|
666
|
+
request: null,
|
|
667
|
+
});
|
|
668
|
+
await this.unregisterIfLive(toolId);
|
|
669
|
+
return { toolId, name, state: 'suspended', reason: 'contended' };
|
|
670
|
+
}
|
|
671
|
+
async admit(candidate, options = {}) {
|
|
672
|
+
const { toolId, name, source, stored, requestStored } = candidate;
|
|
673
|
+
let legacyActive = candidate.legacyActive;
|
|
674
|
+
// A row whose last state write did not finish its flag write: finish it
|
|
675
|
+
// first, so the flag reads what the state row says before anything is
|
|
676
|
+
// decided. A crash or a failed write between a state row and its flag
|
|
677
|
+
// never leaves the two apart beyond the next load.
|
|
678
|
+
if (stored && candidate.flagSynced === false) {
|
|
679
|
+
await this.registry.syncLegacyFlag(toolId, stored);
|
|
680
|
+
legacyActive = stored.state === 'active';
|
|
681
|
+
}
|
|
682
|
+
// The request this process works with: the stored one when it can be read,
|
|
683
|
+
// else one derived from the source. It is written to the row only when the
|
|
684
|
+
// row holds none; a stored request this release cannot read stays as it is.
|
|
685
|
+
const request = stored?.request ?? requestFromSource(source);
|
|
686
|
+
const requestToWrite = requestStored ? undefined : request;
|
|
687
|
+
// 1. A tool a host turned off stays off. A row with is_active = 0 and no
|
|
688
|
+
// suspension on record was turned off by a host (its own SQL, or
|
|
689
|
+
// demote()); loading never undoes that, so it is recorded as the host's.
|
|
690
|
+
// (A flag write the library had not finished was finished above, so a
|
|
691
|
+
// lowered flag here is the host's own.)
|
|
692
|
+
const hostTurnedOff = !legacyActive && stored?.state !== 'suspended';
|
|
693
|
+
if ((stored?.state === 'demoted' || hostTurnedOff) && !options.force) {
|
|
694
|
+
const reason = stored?.state === 'demoted' ? stored.reason : 'legacy_inactive';
|
|
695
|
+
if (stored?.state === 'demoted') {
|
|
696
|
+
this.holdStored(toolId, stored);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
// Written only while the row is as it was read: a host that
|
|
700
|
+
// reactivated the tool meanwhile is not written over.
|
|
701
|
+
const written = await this.registry.setState(toolId, 'demoted', 'legacy_inactive', {
|
|
702
|
+
request: requestToWrite,
|
|
703
|
+
setBy: 'host',
|
|
704
|
+
ifRow: stored ? { at: stored.at, state: stored.state, setBy: stored.setBy } : 'absent',
|
|
705
|
+
});
|
|
706
|
+
if (written.state !== 'demoted' || written.reason !== 'legacy_inactive') {
|
|
707
|
+
return this.readmit(candidate, options);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
await this.unregisterIfLive(toolId);
|
|
711
|
+
return { toolId, name, state: 'demoted', reason };
|
|
712
|
+
}
|
|
713
|
+
// 2. A suspension the host set is cleared only by the host, whatever its
|
|
714
|
+
// reason says. One the library set is re-checked below.
|
|
715
|
+
if (stored?.state === 'suspended' && !options.force && stored.setBy === 'host') {
|
|
716
|
+
// Another process may have set it: this one takes it in and lets go of
|
|
717
|
+
// the executable, so the suspension holds wherever the tool is loaded.
|
|
718
|
+
this.holdStored(toolId, stored);
|
|
719
|
+
await this.unregisterIfLive(toolId);
|
|
720
|
+
return { toolId, name, state: 'suspended', reason: stored.reason };
|
|
721
|
+
}
|
|
722
|
+
// 3. Can the source be rebuilt, and may it run under the configuration in force?
|
|
723
|
+
let implementation;
|
|
724
|
+
let refusal = null;
|
|
725
|
+
if (source.format === 'unreadable') {
|
|
726
|
+
refusal = 'source_unreadable';
|
|
727
|
+
}
|
|
728
|
+
else if (source.format === 'redacted') {
|
|
729
|
+
// The row cannot rebuild the tool, but this process may still hold it:
|
|
730
|
+
// with source persistence off, a tool forged here runs from memory until
|
|
731
|
+
// the process ends, and a load must not take it away.
|
|
732
|
+
const held = this.registry.get(toolId)?.implementation;
|
|
733
|
+
if (held && held.mode === 'sandbox' && held.code.trim() !== '') {
|
|
734
|
+
implementation = held;
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
refusal = 'source_not_persisted';
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
implementation = source.implementation;
|
|
742
|
+
}
|
|
743
|
+
if (implementation &&
|
|
744
|
+
implementation.mode === 'sandbox' &&
|
|
745
|
+
request?.kind === 'sandbox') {
|
|
746
|
+
// The request is what the tool was granted, whatever its text, its
|
|
747
|
+
// stored list or the list held in memory shows: for a raw-code row the
|
|
748
|
+
// text was read only to derive a request where none was stored, and a
|
|
749
|
+
// stored request narrower than a stored list is the grant. Never wider
|
|
750
|
+
// than a stored list: a request widened by hand grants nothing the list
|
|
751
|
+
// did not.
|
|
752
|
+
const listed = implementation;
|
|
753
|
+
const granted = toSandboxApis(request.capabilities);
|
|
754
|
+
implementation = {
|
|
755
|
+
...implementation,
|
|
756
|
+
allowlist: source.format === 'code-with-list' ? granted.filter((api) => listed.allowlist.includes(api)) : granted,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
if (implementation && !refusal) {
|
|
760
|
+
refusal = this.refusalFor(implementation);
|
|
761
|
+
}
|
|
762
|
+
if (refusal || !implementation) {
|
|
763
|
+
const reason = refusal ?? 'source_unreadable';
|
|
764
|
+
// Written as the library's even when a host's suspension already carries
|
|
765
|
+
// the same words, so a later load re-checks it (the words decide nothing).
|
|
766
|
+
if (stored?.state !== 'suspended' || stored.reason !== reason || stored.setBy !== 'library') {
|
|
767
|
+
// A load writes only while the row is as it was read (a host's
|
|
768
|
+
// reactivation is unconditional); a refused write re-admits from the
|
|
769
|
+
// row as it stands.
|
|
770
|
+
const written = await this.registry.setState(toolId, 'suspended', reason, {
|
|
771
|
+
request: requestToWrite,
|
|
772
|
+
setBy: 'library',
|
|
773
|
+
...(options.force
|
|
774
|
+
? {}
|
|
775
|
+
: { ifRow: stored ? { at: stored.at, state: stored.state, setBy: stored.setBy } : 'absent' }),
|
|
776
|
+
});
|
|
777
|
+
if (written.state !== 'suspended' || written.reason !== reason) {
|
|
778
|
+
return this.readmit(candidate, options);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
// The row already says so; this process holds it too when the tool is live here.
|
|
783
|
+
this.holdStored(toolId, stored);
|
|
784
|
+
}
|
|
785
|
+
await this.unregisterIfLive(toolId);
|
|
786
|
+
return { toolId, name, state: 'suspended', reason };
|
|
787
|
+
}
|
|
788
|
+
// 4. Active: the state row first, so a write that fails leaves the tool
|
|
789
|
+
// off; then into memory without rewriting the row; then into the executor.
|
|
790
|
+
const tool = candidate.buildTool(implementation);
|
|
791
|
+
// A reactivation always writes: the row may already read active while this
|
|
792
|
+
// process holds a restriction whose own write failed. A load writes only
|
|
793
|
+
// while the row is still as it read it, so a restriction another process
|
|
794
|
+
// stored in between is never written over.
|
|
795
|
+
let written;
|
|
796
|
+
if (options.force || stored?.state !== 'active' || !requestStored) {
|
|
797
|
+
written = await this.registry.setState(toolId, 'active', null, {
|
|
798
|
+
request: requestToWrite,
|
|
799
|
+
setBy: 'library',
|
|
800
|
+
...(options.force
|
|
801
|
+
? {}
|
|
802
|
+
: { ifRow: stored ? { at: stored.at, state: stored.state, setBy: stored.setBy } : 'absent' }),
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
// Nothing to write: the row read active with its request. It is read
|
|
807
|
+
// again here, because the write that would have caught a restriction
|
|
808
|
+
// another process stored since the first read is not made.
|
|
809
|
+
const now = await this.registry.readStoredState(toolId);
|
|
810
|
+
if (!now) {
|
|
811
|
+
// The row this load read is gone: another process removed the tool.
|
|
812
|
+
written = { toolId, state: 'demoted', reason: 'removed', setBy: 'host', at: Date.now(), request: null };
|
|
813
|
+
}
|
|
814
|
+
else if (now.state !== stored.state ||
|
|
815
|
+
now.setBy !== stored.setBy ||
|
|
816
|
+
now.at !== stored.at ||
|
|
817
|
+
(now.writeId !== undefined && now.writeId !== stored.writeId)) {
|
|
818
|
+
written = now;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// A suspension or demotion that arrived while the row was being written,
|
|
822
|
+
// in this process or in another, is the newer word: the tool is not
|
|
823
|
+
// registered. A restriction this process holds from an earlier read is
|
|
824
|
+
// older than the row and gives way to it.
|
|
825
|
+
const held = written && written.state !== 'active' ? written : this.newerRestrictionHeld(toolId, stored);
|
|
826
|
+
if (held && held.state !== 'active') {
|
|
827
|
+
this.holdStored(toolId, held);
|
|
828
|
+
await this.unregisterIfLive(toolId);
|
|
829
|
+
return { toolId, name, state: held.state, reason: held.reason };
|
|
830
|
+
}
|
|
831
|
+
// A first write refused by another process's active row: that row's
|
|
832
|
+
// request is the grant, not the one derived here, so the tool is admitted
|
|
833
|
+
// again from the row as it stands (once).
|
|
834
|
+
// The grant the row holds after the write is what the tool may reach: a
|
|
835
|
+
// request another process stored meanwhile, narrower or wider than the
|
|
836
|
+
// one this admission built with, admits the tool again from the row. A
|
|
837
|
+
// row whose request this release cannot read (or holds none) gives
|
|
838
|
+
// nothing to compare, and the derived grant stands as before.
|
|
839
|
+
if (written && written.request !== null && !sameGrant(written.request, request)) {
|
|
840
|
+
return this.readmit(candidate, options);
|
|
841
|
+
}
|
|
842
|
+
// A live tool under this id with another name (the host renamed the row):
|
|
843
|
+
// its executable goes first, so the old name stops running the old code.
|
|
844
|
+
const live = this.registry.get(toolId);
|
|
845
|
+
if (live && live.name !== tool.name) {
|
|
846
|
+
await this.unregisterIfLive(toolId);
|
|
847
|
+
}
|
|
848
|
+
const adopted = this.registry.adopt(tool, {
|
|
849
|
+
toolId,
|
|
850
|
+
state: 'active',
|
|
851
|
+
reason: null,
|
|
852
|
+
setBy: 'library',
|
|
853
|
+
at: Date.now(),
|
|
854
|
+
request,
|
|
855
|
+
});
|
|
856
|
+
if (!adopted) {
|
|
857
|
+
// Removed in this process while the row was being admitted.
|
|
858
|
+
return { toolId, name, state: 'demoted', reason: 'removed' };
|
|
859
|
+
}
|
|
860
|
+
this.indexTool(tool.id, tool.createdBy, this.extractSessionId(tool.source) ?? `persisted:${tool.id}`);
|
|
861
|
+
if (this.onToolForged) {
|
|
862
|
+
try {
|
|
863
|
+
await this.onToolForged(tool, this.createExecutableTool(tool));
|
|
864
|
+
}
|
|
865
|
+
catch (error) {
|
|
866
|
+
// The host did not register the executable: nothing here claims the
|
|
867
|
+
// tool, its row stays for the next load, and the failure is the
|
|
868
|
+
// load's report for this row.
|
|
869
|
+
this.registry.forget(toolId);
|
|
870
|
+
this.removeIndexedToolEverywhere(toolId);
|
|
871
|
+
throw error;
|
|
872
|
+
}
|
|
873
|
+
if (this.registry.wasRemoved(toolId)) {
|
|
874
|
+
// Removed while the host was registering it: the registration goes.
|
|
875
|
+
if (this.onToolRemoved) {
|
|
876
|
+
await this.onToolRemoved(tool);
|
|
877
|
+
}
|
|
878
|
+
this.removeIndexedToolEverywhere(toolId);
|
|
879
|
+
return { toolId, name, state: 'demoted', reason: 'removed' };
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return { toolId, name, state: 'active', reason: null };
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* The library's reason for not running an implementation under the
|
|
886
|
+
* configuration in force, or `null`. Later steps add their checks here, so
|
|
887
|
+
* forging, loading and promotion all ask the same question.
|
|
888
|
+
*/
|
|
889
|
+
refusalFor(implementation) {
|
|
890
|
+
if (implementation.mode === 'sandbox' && implementation.code.trim() === '') {
|
|
891
|
+
return 'source_not_persisted';
|
|
892
|
+
}
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
async unregisterIfLive(toolId) {
|
|
896
|
+
const live = this.registry.get(toolId);
|
|
897
|
+
if (live && this.onToolRemoved) {
|
|
898
|
+
await this.onToolRemoved(live);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* A restriction this process holds that is newer than the row it just read:
|
|
903
|
+
* its write is in flight, so the row does not yet show it.
|
|
904
|
+
*/
|
|
905
|
+
newerRestrictionHeld(toolId, stored) {
|
|
906
|
+
const memory = this.registry.getState(toolId);
|
|
907
|
+
if (!memory || memory.state === 'active') {
|
|
908
|
+
return undefined;
|
|
909
|
+
}
|
|
910
|
+
// At the row's own time the restriction is the later of the two: two
|
|
911
|
+
// writes in one millisecond are told apart by nothing else.
|
|
912
|
+
return !stored || memory.at >= stored.at ? memory : undefined;
|
|
913
|
+
}
|
|
914
|
+
/** A stored restriction, held in this process too when the tool is live here. */
|
|
915
|
+
holdStored(toolId, record) {
|
|
916
|
+
const live = this.registry.get(toolId);
|
|
917
|
+
if (live) {
|
|
918
|
+
this.registry.adopt(live, record);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
// --------------------------------------------------------------------------
|
|
369
922
|
// PUBLIC: createSelfImprovementTools
|
|
370
923
|
// --------------------------------------------------------------------------
|
|
371
924
|
/**
|
|
@@ -473,8 +1026,10 @@ export class EmergentCapabilityEngine {
|
|
|
473
1026
|
/**
|
|
474
1027
|
* Create an executable ITool wrapper for a forged emergent tool.
|
|
475
1028
|
*
|
|
476
|
-
* The wrapper
|
|
477
|
-
*
|
|
1029
|
+
* The wrapper refuses a call to a tool that is not active and, for an
|
|
1030
|
+
* `agent`-tier tool, a call from any agent but its own, before anything
|
|
1031
|
+
* runs or a use is recorded; it then performs runtime output validation,
|
|
1032
|
+
* usage tracking, and promotion checks after each successful execution.
|
|
478
1033
|
*/
|
|
479
1034
|
createExecutableTool(tool) {
|
|
480
1035
|
const baseTool = tool.implementation.mode === 'compose'
|
|
@@ -490,6 +1045,35 @@ export class EmergentCapabilityEngine {
|
|
|
490
1045
|
category: 'emergent',
|
|
491
1046
|
hasSideEffects: tool.implementation.mode === 'sandbox',
|
|
492
1047
|
execute: async (args, context) => {
|
|
1048
|
+
// The registry no longer holds the tool (removed, or its session
|
|
1049
|
+
// cleaned up) while the executable is still registered: refuse, do
|
|
1050
|
+
// not run the captured code. This comes before the state check, which
|
|
1051
|
+
// reads "no state" as active.
|
|
1052
|
+
const current = this.registry.get(tool.id);
|
|
1053
|
+
if (!current) {
|
|
1054
|
+
return { success: false, error: `Emergent tool "${tool.name}" is no longer registered.` };
|
|
1055
|
+
}
|
|
1056
|
+
if (!this.registry.isActive(tool.id)) {
|
|
1057
|
+
const held = this.registry.getState(tool.id);
|
|
1058
|
+
return {
|
|
1059
|
+
success: false,
|
|
1060
|
+
error: `Emergent tool "${tool.name}" is ${held?.state ?? 'inactive'}: ` +
|
|
1061
|
+
`${held?.reason ?? 'no reason recorded'}.`,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
// The owner is the persona that forged the tool, compared as it was
|
|
1065
|
+
// stored ('unknown' for a caller without one). A row from an earlier
|
|
1066
|
+
// release holds the forging GMI instance's id instead, which no
|
|
1067
|
+
// persona can match: such a tool runs for any caller of the host that
|
|
1068
|
+
// loaded it by that id, as it did before this change.
|
|
1069
|
+
const owner = current.createdBy;
|
|
1070
|
+
const caller = context.personaId ?? 'unknown';
|
|
1071
|
+
if (current.tier === 'agent' && !owner.startsWith('gmi-instance-') && caller !== owner) {
|
|
1072
|
+
return {
|
|
1073
|
+
success: false,
|
|
1074
|
+
error: `Emergent tool "${tool.name}" belongs to agent ${owner}; it is not callable as ${caller}.`,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
493
1077
|
const startTime = performance.now();
|
|
494
1078
|
const result = await baseTool.execute(args, context);
|
|
495
1079
|
const executionTimeMs = Math.round(performance.now() - startTime);
|
|
@@ -559,8 +1143,7 @@ export class EmergentCapabilityEngine {
|
|
|
559
1143
|
}
|
|
560
1144
|
}
|
|
561
1145
|
extractSessionId(source) {
|
|
562
|
-
|
|
563
|
-
return match?.[1] ?? null;
|
|
1146
|
+
return sessionFromSource(source);
|
|
564
1147
|
}
|
|
565
1148
|
buildSandboxExecutable(tool) {
|
|
566
1149
|
return {
|