@framers/agentos 0.11.5 → 0.12.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/dist/api/runtime/SelfImprovementSessionManager.d.ts.map +1 -1
- package/dist/api/runtime/SelfImprovementSessionManager.js +22 -8
- package/dist/api/runtime/SelfImprovementSessionManager.js.map +1 -1
- package/dist/cognition/emergent/ComposableToolBuilder.d.ts +59 -17
- package/dist/cognition/emergent/ComposableToolBuilder.d.ts.map +1 -1
- package/dist/cognition/emergent/ComposableToolBuilder.js +173 -53
- package/dist/cognition/emergent/ComposableToolBuilder.js.map +1 -1
- package/dist/cognition/emergent/CreateWorkflowTool.d.ts +19 -2
- package/dist/cognition/emergent/CreateWorkflowTool.d.ts.map +1 -1
- package/dist/cognition/emergent/CreateWorkflowTool.js +30 -2
- package/dist/cognition/emergent/CreateWorkflowTool.js.map +1 -1
- package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts +68 -3
- package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts.map +1 -1
- package/dist/cognition/emergent/EmergentCapabilityEngine.js +441 -78
- package/dist/cognition/emergent/EmergentCapabilityEngine.js.map +1 -1
- package/dist/cognition/emergent/EmergentJudge.d.ts +2 -0
- package/dist/cognition/emergent/EmergentJudge.d.ts.map +1 -1
- package/dist/cognition/emergent/EmergentJudge.js +3 -1
- package/dist/cognition/emergent/EmergentJudge.js.map +1 -1
- package/dist/cognition/emergent/EmergentToolRegistry.d.ts +43 -5
- package/dist/cognition/emergent/EmergentToolRegistry.d.ts.map +1 -1
- package/dist/cognition/emergent/EmergentToolRegistry.js +98 -19
- package/dist/cognition/emergent/EmergentToolRegistry.js.map +1 -1
- package/dist/cognition/emergent/ForgeToolMetaTool.d.ts +1 -0
- package/dist/cognition/emergent/ForgeToolMetaTool.d.ts.map +1 -1
- package/dist/cognition/emergent/ForgeToolMetaTool.js +7 -0
- package/dist/cognition/emergent/ForgeToolMetaTool.js.map +1 -1
- package/dist/cognition/emergent/StepGate.d.ts +73 -0
- package/dist/cognition/emergent/StepGate.d.ts.map +1 -0
- package/dist/cognition/emergent/StepGate.js +124 -0
- package/dist/cognition/emergent/StepGate.js.map +1 -0
- package/dist/cognition/emergent/index.d.ts +3 -1
- package/dist/cognition/emergent/index.d.ts.map +1 -1
- package/dist/cognition/emergent/index.js +1 -0
- package/dist/cognition/emergent/index.js.map +1 -1
- package/dist/cognition/emergent/types.d.ts +32 -2
- package/dist/cognition/emergent/types.d.ts.map +1 -1
- package/dist/cognition/emergent/types.js +10 -0
- package/dist/cognition/emergent/types.js.map +1 -1
- package/dist/core/llm/providers/implementations/OpenRouterProvider.d.ts.map +1 -1
- package/dist/core/llm/providers/implementations/OpenRouterProvider.js +4 -2
- package/dist/core/llm/providers/implementations/OpenRouterProvider.js.map +1 -1
- package/dist/core/tools/ITool.d.ts +6 -0
- package/dist/core/tools/ITool.d.ts.map +1 -1
- package/dist/core/tools/ToolExecutor.d.ts +19 -0
- package/dist/core/tools/ToolExecutor.d.ts.map +1 -1
- package/dist/core/tools/ToolExecutor.js +9 -0
- package/dist/core/tools/ToolExecutor.js.map +1 -1
- package/dist/core/tools/ToolOrchestrator.d.ts +13 -0
- package/dist/core/tools/ToolOrchestrator.d.ts.map +1 -1
- package/dist/core/tools/ToolOrchestrator.js +129 -13
- package/dist/core/tools/ToolOrchestrator.js.map +1 -1
- package/dist/extensions/ExtensionRegistry.d.ts +7 -0
- package/dist/extensions/ExtensionRegistry.d.ts.map +1 -1
- package/dist/extensions/ExtensionRegistry.js +24 -0
- package/dist/extensions/ExtensionRegistry.js.map +1 -1
- package/dist/io/media/images/imageToBuffer.d.ts.map +1 -1
- package/dist/io/media/images/imageToBuffer.js +3 -2
- package/dist/io/media/images/imageToBuffer.js.map +1 -1
- package/dist/io/speech/SpeechProviderResolver.d.ts.map +1 -1
- package/dist/io/speech/SpeechProviderResolver.js +13 -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 +16 -8
- package/dist/io/speech/SpeechRuntime.js.map +1 -1
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@
|
|
|
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 { GMI_INSTANCE_ID_PREFIX } from './types.js';
|
|
16
17
|
import { parsePersistedSource, parseRowSchemas, parseStoredRequest, requestFromImplementation, sessionFromSource, requestFromSource, sourceFromImplementation, stateSetterFromColumn, toolFromRow, } from './persisted-source.js';
|
|
17
18
|
import { normalizeAllowlist, toSandboxApis } from './capabilities.js';
|
|
18
19
|
// ============================================================================
|
|
@@ -24,6 +25,35 @@ import { normalizeAllowlist, toSandboxApis } from './capabilities.js';
|
|
|
24
25
|
/** The configuration key behind a reason, named in the start-up line. */
|
|
25
26
|
const REASON_CONFIG_KEYS = {
|
|
26
27
|
source_not_persisted: 'emergent.persistSandboxSource',
|
|
28
|
+
sandbox_tools_off: 'emergent.allowSandboxTools',
|
|
29
|
+
compose_needs_gate: 'a StepGate (deps.stepGate, or the ComposableToolBuilder)',
|
|
30
|
+
step_not_chainable: 'emergent.compose.sideEffectingTools',
|
|
31
|
+
side_effects_undeclared: "the step tool's hasSideEffects",
|
|
32
|
+
step_cycle: "the composition's steps (a step reaches the composition itself)",
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The library's suspensions of a composition that a change in what is
|
|
36
|
+
* registered can lift: the composition is checked again when a tool one of
|
|
37
|
+
* its steps names is registered (`onHostToolRegistered`).
|
|
38
|
+
*/
|
|
39
|
+
const STEP_SUSPENSION_REASONS = new Set([
|
|
40
|
+
'compose_needs_gate',
|
|
41
|
+
'step_missing',
|
|
42
|
+
'step_not_chainable',
|
|
43
|
+
'side_effects_undeclared',
|
|
44
|
+
'step_replaced',
|
|
45
|
+
'step_cycle',
|
|
46
|
+
]);
|
|
47
|
+
/** Step refusals a composed call can meet at run time, and the suspension each leads to. */
|
|
48
|
+
const RUN_REFUSAL_REASONS = {
|
|
49
|
+
step_missing: 'step_missing',
|
|
50
|
+
step_not_chainable: 'step_replaced',
|
|
51
|
+
side_effects_undeclared: 'step_replaced',
|
|
52
|
+
compose_needs_gate: 'compose_needs_gate',
|
|
53
|
+
step_cycle: 'step_cycle',
|
|
54
|
+
// The step's tool was replaced between its check and its run: nothing ran,
|
|
55
|
+
// and the composition waits for a re-check.
|
|
56
|
+
step_replaced: 'step_replaced',
|
|
27
57
|
};
|
|
28
58
|
// ============================================================================
|
|
29
59
|
// ENGINE
|
|
@@ -86,6 +116,12 @@ export class EmergentCapabilityEngine {
|
|
|
86
116
|
this.onToolForged = deps.onToolForged;
|
|
87
117
|
this.onToolPromoted = deps.onToolPromoted;
|
|
88
118
|
this.onToolRemoved = deps.onToolRemoved;
|
|
119
|
+
if (deps.stepGate) {
|
|
120
|
+
this.composableBuilder.bind({ gate: deps.stepGate });
|
|
121
|
+
}
|
|
122
|
+
this.composableBuilder.bind({
|
|
123
|
+
sideEffectingTools: this.config.compose?.sideEffectingTools ?? [],
|
|
124
|
+
});
|
|
89
125
|
}
|
|
90
126
|
// --------------------------------------------------------------------------
|
|
91
127
|
// PUBLIC: forge
|
|
@@ -106,7 +142,9 @@ export class EmergentCapabilityEngine {
|
|
|
106
142
|
* 6. If rejected: return failure with the judge's reasoning.
|
|
107
143
|
*
|
|
108
144
|
* @param request - The forge request describing the desired tool.
|
|
109
|
-
* @param context -
|
|
145
|
+
* @param context - The agent and session ids. `caller` is the forging
|
|
146
|
+
* call's own context: a composition's test steps run as that caller, so
|
|
147
|
+
* they meet the checks the caller's direct calls meet.
|
|
110
148
|
* @returns A {@link ForgeResult} indicating success or failure.
|
|
111
149
|
*/
|
|
112
150
|
async forge(request, context) {
|
|
@@ -122,34 +160,68 @@ export class EmergentCapabilityEngine {
|
|
|
122
160
|
if (request.implementation.mode === 'compose') {
|
|
123
161
|
// ---- COMPOSE MODE ----
|
|
124
162
|
source = JSON.stringify(request.implementation);
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
163
|
+
// Every step must be chainable before any test case runs.
|
|
164
|
+
for (const step of request.implementation.steps) {
|
|
165
|
+
const verdict = this.composableBuilder.check(step.tool);
|
|
166
|
+
if (!verdict.ok) {
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
error: `${verdict.code}: step "${step.name}" (tool "${step.tool}"): ${verdict.message}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// A composition must not reach itself, through any nesting.
|
|
174
|
+
const cycle = this.compositionCycle(request.name, request.implementation.steps);
|
|
175
|
+
if (cycle) {
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
error: `step_cycle: "${request.name}" reaches itself through ${cycle.join(' -> ')}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// The test steps run as the forging caller, so they meet the checks the
|
|
182
|
+
// caller's direct calls meet; a host that builds the engine itself and
|
|
183
|
+
// passes no caller gets a context of its own.
|
|
184
|
+
const testContext = context.caller ?? {
|
|
129
185
|
gmiId: context.agentId,
|
|
130
186
|
personaId: 'emergent-forge',
|
|
131
187
|
userContext: { userId: 'system' },
|
|
132
188
|
correlationId: context.sessionId,
|
|
133
189
|
};
|
|
134
190
|
for (const tc of request.testCases) {
|
|
191
|
+
let result;
|
|
135
192
|
try {
|
|
136
|
-
|
|
137
|
-
testResults.push({
|
|
138
|
-
input: tc.input,
|
|
139
|
-
output: result.output,
|
|
140
|
-
success: result.success,
|
|
141
|
-
error: result.error,
|
|
142
|
-
});
|
|
193
|
+
result = await this.composableBuilder.runPipeline(request.implementation, tc.input, testContext, { dry: { stepOutputs: tc.stepOutputs ?? {} } });
|
|
143
194
|
}
|
|
144
195
|
catch (err) {
|
|
145
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
146
196
|
testResults.push({
|
|
147
197
|
input: tc.input,
|
|
148
198
|
output: undefined,
|
|
149
199
|
success: false,
|
|
150
|
-
error: message,
|
|
200
|
+
error: err instanceof Error ? err.message : String(err),
|
|
151
201
|
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const details = result.details;
|
|
205
|
+
if (details?.code === 'dry_run_needs_output') {
|
|
206
|
+
return { success: false, error: result.error };
|
|
152
207
|
}
|
|
208
|
+
if (details?.code === 'permission_denied' && !context.caller) {
|
|
209
|
+
// A step needs a capability the stand-in context does not carry:
|
|
210
|
+
// the test says nothing about the tool, so the forge asks for the
|
|
211
|
+
// caller instead of recording a failed test.
|
|
212
|
+
return {
|
|
213
|
+
success: false,
|
|
214
|
+
error: 'caller_context_required: a step of this composition needs a capability; ' +
|
|
215
|
+
"pass the forging caller's context (forge(request, { ..., caller }))",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
testResults.push({
|
|
219
|
+
input: tc.input,
|
|
220
|
+
output: result.output,
|
|
221
|
+
success: result.success,
|
|
222
|
+
error: result.error,
|
|
223
|
+
...(details?.effects && details.effects.length > 0 ? { effects: details.effects } : {}),
|
|
224
|
+
});
|
|
153
225
|
}
|
|
154
226
|
}
|
|
155
227
|
else {
|
|
@@ -234,6 +306,7 @@ export class EmergentCapabilityEngine {
|
|
|
234
306
|
source: `forged by agent ${context.agentId} during session ${context.sessionId}`,
|
|
235
307
|
};
|
|
236
308
|
this.registry.register(tool, 'session');
|
|
309
|
+
const registered = this.registry.generation(toolId);
|
|
237
310
|
let written;
|
|
238
311
|
try {
|
|
239
312
|
written = await this.registry.setState(toolId, 'active', null, {
|
|
@@ -246,7 +319,7 @@ export class EmergentCapabilityEngine {
|
|
|
246
319
|
// is re-derived from its source at the next load.
|
|
247
320
|
console.warn(`[agentos:emergent] could not store the request of "${request.name}" (${toolId}):`, error instanceof Error ? error.message : error);
|
|
248
321
|
}
|
|
249
|
-
if (this.registry.
|
|
322
|
+
if (this.registry.generation(toolId) !== registered) {
|
|
250
323
|
// Removed while it was being forged: nothing is registered.
|
|
251
324
|
return { success: false, error: 'the tool was removed while it was being forged' };
|
|
252
325
|
}
|
|
@@ -273,8 +346,12 @@ export class EmergentCapabilityEngine {
|
|
|
273
346
|
}
|
|
274
347
|
this.indexTool(toolId, context.agentId, context.sessionId);
|
|
275
348
|
if (this.onToolForged) {
|
|
349
|
+
// The object the registry holds (a registration stores a stamped copy;
|
|
350
|
+
// an adoption above stores the object itself), so the settlement
|
|
351
|
+
// below compares like with like.
|
|
352
|
+
const live = this.registry.get(toolId) ?? tool;
|
|
276
353
|
try {
|
|
277
|
-
await this.onToolForged(
|
|
354
|
+
await this.onToolForged(live, this.createExecutableTool(live));
|
|
278
355
|
}
|
|
279
356
|
catch (error) {
|
|
280
357
|
this.registry.remove(toolId);
|
|
@@ -284,6 +361,20 @@ export class EmergentCapabilityEngine {
|
|
|
284
361
|
error: error instanceof Error ? error.message : 'Failed to activate forged tool.',
|
|
285
362
|
};
|
|
286
363
|
}
|
|
364
|
+
let settled;
|
|
365
|
+
try {
|
|
366
|
+
settled = await this.settleRegistration(live);
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
return {
|
|
370
|
+
success: false,
|
|
371
|
+
error: error instanceof Error ? error.message : 'the forged tool could not be registered',
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (settled && settled.reason === 'removed') {
|
|
375
|
+
// Removed while the host was registering it: nothing is registered.
|
|
376
|
+
return { success: false, error: 'the tool was removed while it was being forged' };
|
|
377
|
+
}
|
|
287
378
|
}
|
|
288
379
|
return {
|
|
289
380
|
success: true,
|
|
@@ -324,10 +415,21 @@ export class EmergentCapabilityEngine {
|
|
|
324
415
|
if (!tool) {
|
|
325
416
|
return null;
|
|
326
417
|
}
|
|
418
|
+
// A tool that no longer fits what is in force is suspended, not promoted.
|
|
419
|
+
const refusal = this.refusalFor(tool.implementation, tool.name);
|
|
420
|
+
if (refusal) {
|
|
421
|
+
await this.suspendAsLibrary(toolId, refusal);
|
|
422
|
+
return { success: false, error: `${refusal}: the tool no longer fits and was suspended.` };
|
|
423
|
+
}
|
|
327
424
|
// Only session-tier tools are eligible for auto-promotion.
|
|
328
425
|
if (tool.tier !== 'session') {
|
|
329
426
|
return null;
|
|
330
427
|
}
|
|
428
|
+
// An owner with the reserved instance-id prefix is never promoted: an
|
|
429
|
+
// agent-tier row with that owner reads as one an earlier release wrote.
|
|
430
|
+
if (tool.createdBy.startsWith(GMI_INSTANCE_ID_PREFIX)) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
331
433
|
// Check thresholds.
|
|
332
434
|
const { uses, confidence } = this.config.promotionThreshold;
|
|
333
435
|
if (tool.usageStats.totalUses < uses || tool.usageStats.confidenceScore < confidence) {
|
|
@@ -403,6 +505,17 @@ export class EmergentCapabilityEngine {
|
|
|
403
505
|
const removedTools = this.getSessionTools(sessionId);
|
|
404
506
|
this.registry.cleanupSession(sessionId);
|
|
405
507
|
this.index.bySession.delete(sessionId);
|
|
508
|
+
for (const tool of removedTools) {
|
|
509
|
+
// The agent index too, so a lookup by agent never names a tool that is gone.
|
|
510
|
+
this.removeIndexedToolEverywhere(tool.id);
|
|
511
|
+
}
|
|
512
|
+
if (this.onToolRemoved) {
|
|
513
|
+
// The executables go with the tools, as removeTool does. A host that
|
|
514
|
+
// cleans up through the orchestrator unregisters the same names; a
|
|
515
|
+
// second unregistration of a name finds nothing to do.
|
|
516
|
+
const unregister = this.onToolRemoved;
|
|
517
|
+
void Promise.allSettled(removedTools.map((tool) => unregister(tool)));
|
|
518
|
+
}
|
|
406
519
|
return removedTools;
|
|
407
520
|
}
|
|
408
521
|
/**
|
|
@@ -419,28 +532,34 @@ export class EmergentCapabilityEngine {
|
|
|
419
532
|
* @returns what happened, so a host can tell a registered tool from a refused one.
|
|
420
533
|
*/
|
|
421
534
|
async syncPersistedTool(tool) {
|
|
422
|
-
|
|
423
|
-
if (!
|
|
535
|
+
const existing = await this.registry.loadRow(tool.id);
|
|
536
|
+
if (!existing && this.registry.hasStorage()) {
|
|
424
537
|
// No stored row yet: the host hydrates from its own store. The row is
|
|
425
538
|
// written as it was before loading went through the stored row, so the
|
|
426
539
|
// tool's uses are recorded and the next load finds it.
|
|
427
540
|
await this.registry.writeToolRow(tool);
|
|
428
|
-
row = await this.registry.loadRow(tool.id);
|
|
429
541
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
542
|
+
// The row is read for the admission after any write above, so the write
|
|
543
|
+
// is not a change the admission's read missed.
|
|
544
|
+
return this.withRead(async (readAt) => {
|
|
545
|
+
const row = await this.registry.loadRow(tool.id);
|
|
546
|
+
if (row) {
|
|
547
|
+
// With source persistence off the row holds a redacted record; the
|
|
548
|
+
// implementation the host supplied is what runs then.
|
|
549
|
+
return this.admitRow(row, { readAt, sourceFallback: sourceFromImplementation(tool.implementation) });
|
|
550
|
+
}
|
|
551
|
+
const stored = await this.registry.readState(tool.id);
|
|
552
|
+
return this.admit({
|
|
553
|
+
toolId: tool.id,
|
|
554
|
+
name: tool.name,
|
|
555
|
+
source: sourceFromImplementation(tool.implementation),
|
|
556
|
+
stored,
|
|
557
|
+
requestStored: stored?.request != null,
|
|
558
|
+
legacyActive: tool.isActive ?? true,
|
|
559
|
+
tier: tool.tier,
|
|
560
|
+
createdBy: tool.createdBy,
|
|
561
|
+
buildTool: () => tool,
|
|
562
|
+
}, { readAt });
|
|
444
563
|
});
|
|
445
564
|
}
|
|
446
565
|
/**
|
|
@@ -493,17 +612,47 @@ export class EmergentCapabilityEngine {
|
|
|
493
612
|
throw new Error("selector_required: loadPersistedTools({ tiers: ['session'] }) needs the sessionId whose tools to load; " +
|
|
494
613
|
"a session's tools are private to it.");
|
|
495
614
|
}
|
|
496
|
-
const rows = await this.registry.loadRows(tiers, { agentId, sessionId });
|
|
497
615
|
const outcomes = [];
|
|
498
616
|
const failed = [];
|
|
499
|
-
|
|
617
|
+
// One read for the rows and their admissions: a tool that changes in this
|
|
618
|
+
// process after the rows were read (a removal, a registration) is read
|
|
619
|
+
// again before it is adopted.
|
|
620
|
+
await this.withRead(async (readAt) => {
|
|
621
|
+
// Code tools first: a stored composition may chain a stored code tool.
|
|
622
|
+
const rows = (await this.registry.loadRows(tiers, { agentId, sessionId })).sort((a, b) => Number(a.implementation_mode === 'compose') - Number(b.implementation_mode === 'compose'));
|
|
623
|
+
for (const row of rows) {
|
|
624
|
+
try {
|
|
625
|
+
outcomes.push(await this.admitRow(row, { readAt }));
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
629
|
+
failed.push({ toolId: row.id, name: row.name, error: message });
|
|
630
|
+
console.warn(`[agentos:emergent] stored tool "${row.name}" (${row.id}) did not load: ${message}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
// A composition can chain another composition loaded after it: those
|
|
635
|
+
// suspended because a step was missing are admitted once more.
|
|
636
|
+
for (let i = 0; i < outcomes.length; i += 1) {
|
|
637
|
+
const outcome = outcomes[i];
|
|
638
|
+
if (outcome.state !== 'suspended' || outcome.reason !== 'step_missing') {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
500
641
|
try {
|
|
501
|
-
|
|
642
|
+
const again = await this.withRead(async (readAt) => {
|
|
643
|
+
const row = await this.registry.loadRow(outcome.toolId);
|
|
644
|
+
return row ? this.admitRow(row, { readAt }) : undefined;
|
|
645
|
+
});
|
|
646
|
+
if (again) {
|
|
647
|
+
outcomes[i] = again;
|
|
648
|
+
}
|
|
502
649
|
}
|
|
503
650
|
catch (error) {
|
|
504
651
|
const message = error instanceof Error ? error.message : String(error);
|
|
505
|
-
|
|
506
|
-
|
|
652
|
+
outcomes.splice(i, 1);
|
|
653
|
+
i -= 1;
|
|
654
|
+
failed.push({ toolId: outcome.toolId, name: outcome.name, error: message });
|
|
655
|
+
console.warn(`[agentos:emergent] stored tool "${outcome.name}" (${outcome.toolId}) did not load: ${message}`);
|
|
507
656
|
}
|
|
508
657
|
}
|
|
509
658
|
for (const outcome of outcomes) {
|
|
@@ -579,24 +728,69 @@ export class EmergentCapabilityEngine {
|
|
|
579
728
|
* @returns the outcome, or `undefined` when the tool is unknown.
|
|
580
729
|
*/
|
|
581
730
|
async reactivateTool(toolId) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
731
|
+
return this.recheck(toolId, true);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Called by the host when any tool is registered. A composition the library
|
|
735
|
+
* suspended because a step was missing, refused, replaced or circular is
|
|
736
|
+
* checked again when a tool one of its steps names arrives, and registered
|
|
737
|
+
* when it fits. A host's own suspension is left to the host.
|
|
738
|
+
*/
|
|
739
|
+
async onHostToolRegistered(toolName) {
|
|
740
|
+
for (const record of this.registry.listStates()) {
|
|
741
|
+
if (record.state !== 'suspended' ||
|
|
742
|
+
record.setBy !== 'library' ||
|
|
743
|
+
!STEP_SUSPENSION_REASONS.has(record.reason ?? '')) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (record.request?.kind !== 'compose' || !record.request.steps.some((step) => step.tool === toolName)) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
await this.recheck(record.toolId, false);
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
console.warn(`[agentos:emergent] could not re-check "${record.toolId}" after "${toolName}" was registered:`, error instanceof Error ? error.message : error);
|
|
754
|
+
}
|
|
585
755
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
756
|
+
}
|
|
757
|
+
async recheck(toolId, force) {
|
|
758
|
+
return this.withRead(async (readAt) => {
|
|
759
|
+
const row = await this.registry.loadRow(toolId);
|
|
760
|
+
if (row) {
|
|
761
|
+
return this.admitRow(row, { force, readAt });
|
|
762
|
+
}
|
|
763
|
+
const tool = this.registry.get(toolId);
|
|
764
|
+
if (!tool) {
|
|
765
|
+
return undefined;
|
|
766
|
+
}
|
|
767
|
+
const held = this.registry.getState(toolId);
|
|
768
|
+
return this.admit({
|
|
769
|
+
toolId,
|
|
770
|
+
name: tool.name,
|
|
771
|
+
source: sourceFromImplementation(tool.implementation),
|
|
772
|
+
stored: held,
|
|
773
|
+
requestStored: held?.request != null,
|
|
774
|
+
legacyActive: true,
|
|
775
|
+
tier: tool.tier,
|
|
776
|
+
createdBy: tool.createdBy,
|
|
777
|
+
buildTool: () => tool,
|
|
778
|
+
}, { force, readAt });
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Run a read of stored rows and the admissions that use it. `readAt` is the
|
|
783
|
+
* point the read started at; an admission adopts only what has not changed
|
|
784
|
+
* since (see `EmergentToolRegistry.adopt`).
|
|
785
|
+
*/
|
|
786
|
+
async withRead(read) {
|
|
787
|
+
const readAt = this.registry.beginRead();
|
|
788
|
+
try {
|
|
789
|
+
return await read(readAt);
|
|
790
|
+
}
|
|
791
|
+
finally {
|
|
792
|
+
this.registry.endRead();
|
|
589
793
|
}
|
|
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
794
|
}
|
|
601
795
|
admitRow(row, options = {}) {
|
|
602
796
|
const stored = row.state
|
|
@@ -632,6 +826,8 @@ export class EmergentCapabilityEngine {
|
|
|
632
826
|
requestStored: row.request_json != null,
|
|
633
827
|
legacyActive: !(row.is_active === 0 || row.is_active === false),
|
|
634
828
|
flagSynced: row.flag_synced == null ? undefined : !(row.flag_synced === 0 || row.flag_synced === false),
|
|
829
|
+
tier: row.tier,
|
|
830
|
+
createdBy: row.created_by_agent,
|
|
635
831
|
buildTool: (implementation) => toolFromRow(row, implementation),
|
|
636
832
|
}, options);
|
|
637
833
|
}
|
|
@@ -649,12 +845,17 @@ export class EmergentCapabilityEngine {
|
|
|
649
845
|
const { toolId, name } = candidate;
|
|
650
846
|
const depth = options.readmitted ?? 0;
|
|
651
847
|
if (depth < 2) {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
848
|
+
// The tool's queued writes and deletes land first, so the row read next
|
|
849
|
+
// is the one they leave.
|
|
850
|
+
await this.registry.settled(toolId);
|
|
851
|
+
return this.withRead(async (readAt) => {
|
|
852
|
+
const fresh = await this.registry.loadRow(toolId);
|
|
853
|
+
if (fresh) {
|
|
854
|
+
return this.admitRow(fresh, { ...options, readmitted: depth + 1, readAt });
|
|
855
|
+
}
|
|
856
|
+
await this.unregisterIfLive(toolId);
|
|
857
|
+
return { toolId, name, state: 'demoted', reason: 'removed' };
|
|
858
|
+
});
|
|
658
859
|
}
|
|
659
860
|
// Nothing is written; the tool is held off here until the next load.
|
|
660
861
|
this.holdStored(toolId, {
|
|
@@ -670,6 +871,9 @@ export class EmergentCapabilityEngine {
|
|
|
670
871
|
}
|
|
671
872
|
async admit(candidate, options = {}) {
|
|
672
873
|
const { toolId, name, source, stored, requestStored } = candidate;
|
|
874
|
+
// The point after which a change makes what this admission holds stale:
|
|
875
|
+
// where its row was read, or, for a caller that did not say, now.
|
|
876
|
+
const readAt = options.readAt ?? this.registry.generation(toolId);
|
|
673
877
|
let legacyActive = candidate.legacyActive;
|
|
674
878
|
// A row whose last state write did not finish its flag write: finish it
|
|
675
879
|
// first, so the flag reads what the state row says before anything is
|
|
@@ -756,8 +960,16 @@ export class EmergentCapabilityEngine {
|
|
|
756
960
|
allowlist: source.format === 'code-with-list' ? granted.filter((api) => listed.allowlist.includes(api)) : granted,
|
|
757
961
|
};
|
|
758
962
|
}
|
|
963
|
+
// 3a. An agent-tier row from an earlier release is owned by the forging
|
|
964
|
+
// GMI instance's id, which no persona can match, so no caller could
|
|
965
|
+
// ever pass the owner check: it loads suspended rather than running
|
|
966
|
+
// for everyone. Forging the tool again under the persona is the way
|
|
967
|
+
// back; `reactivateTool` goes through this same path.
|
|
968
|
+
if (!refusal && candidate.tier === 'agent' && candidate.createdBy.startsWith(GMI_INSTANCE_ID_PREFIX)) {
|
|
969
|
+
refusal = 'legacy_owner';
|
|
970
|
+
}
|
|
759
971
|
if (implementation && !refusal) {
|
|
760
|
-
refusal = this.refusalFor(implementation);
|
|
972
|
+
refusal = this.refusalFor(implementation, name);
|
|
761
973
|
}
|
|
762
974
|
if (refusal || !implementation) {
|
|
763
975
|
const reason = refusal ?? 'source_unreadable';
|
|
@@ -852,10 +1064,22 @@ export class EmergentCapabilityEngine {
|
|
|
852
1064
|
setBy: 'library',
|
|
853
1065
|
at: Date.now(),
|
|
854
1066
|
request,
|
|
855
|
-
});
|
|
1067
|
+
}, readAt);
|
|
856
1068
|
if (!adopted) {
|
|
857
|
-
//
|
|
858
|
-
|
|
1069
|
+
// The tool changed in this process after its row was read (removed,
|
|
1070
|
+
// replaced under its id, written again), or a removal of it is still
|
|
1071
|
+
// deleting its rows: what this admission holds may be stale. With
|
|
1072
|
+
// storage the row is read again once the tool's queued writes have run;
|
|
1073
|
+
// without, the registry's own record is the answer.
|
|
1074
|
+
if (this.registry.hasStorage()) {
|
|
1075
|
+
return this.readmit(candidate, options);
|
|
1076
|
+
}
|
|
1077
|
+
const current = this.registry.get(toolId);
|
|
1078
|
+
if (!current) {
|
|
1079
|
+
return { toolId, name, state: 'demoted', reason: 'removed' };
|
|
1080
|
+
}
|
|
1081
|
+
const held = this.registry.getState(toolId);
|
|
1082
|
+
return { toolId, name: current.name, state: held?.state ?? 'active', reason: held?.reason ?? null };
|
|
859
1083
|
}
|
|
860
1084
|
this.indexTool(tool.id, tool.createdBy, this.extractSessionId(tool.source) ?? `persisted:${tool.id}`);
|
|
861
1085
|
if (this.onToolForged) {
|
|
@@ -870,28 +1094,145 @@ export class EmergentCapabilityEngine {
|
|
|
870
1094
|
this.removeIndexedToolEverywhere(toolId);
|
|
871
1095
|
throw error;
|
|
872
1096
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
await this.onToolRemoved(tool);
|
|
877
|
-
}
|
|
878
|
-
this.removeIndexedToolEverywhere(toolId);
|
|
879
|
-
return { toolId, name, state: 'demoted', reason: 'removed' };
|
|
1097
|
+
const settled = await this.settleRegistration(tool);
|
|
1098
|
+
if (settled) {
|
|
1099
|
+
return settled;
|
|
880
1100
|
}
|
|
881
1101
|
}
|
|
882
1102
|
return { toolId, name, state: 'active', reason: null };
|
|
883
1103
|
}
|
|
1104
|
+
/**
|
|
1105
|
+
* After the host registered a tool's executable, the registry must still
|
|
1106
|
+
* hold that very object. When it does not (the tool was removed, or
|
|
1107
|
+
* replaced under its id, while the registration ran), the executor is
|
|
1108
|
+
* brought back in line with the registry: the current tool's executable is
|
|
1109
|
+
* registered (the old name's taken out first when the name changed), or
|
|
1110
|
+
* the stale executable is taken out when the registry holds none. The
|
|
1111
|
+
* registry is read again after every registration, so a change during the
|
|
1112
|
+
* settlement is settled too, up to three rounds. A registration that fails,
|
|
1113
|
+
* or a tool still changing after three rounds, leaves no executable under
|
|
1114
|
+
* the name and throws: a tool that cannot be called is reported, never a
|
|
1115
|
+
* stale executable left running. Returns the outcome to report, or
|
|
1116
|
+
* undefined when the first registration stands.
|
|
1117
|
+
*/
|
|
1118
|
+
async settleRegistration(tool) {
|
|
1119
|
+
let registered = tool;
|
|
1120
|
+
for (let round = 0; round < 3; round += 1) {
|
|
1121
|
+
const current = this.registry.get(tool.id);
|
|
1122
|
+
if (current === registered) {
|
|
1123
|
+
if (round === 0) {
|
|
1124
|
+
return undefined;
|
|
1125
|
+
}
|
|
1126
|
+
if (!current) {
|
|
1127
|
+
this.removeIndexedToolEverywhere(tool.id);
|
|
1128
|
+
return { toolId: tool.id, name: tool.name, state: 'demoted', reason: 'removed' };
|
|
1129
|
+
}
|
|
1130
|
+
const held = this.registry.getState(tool.id);
|
|
1131
|
+
return { toolId: tool.id, name: current.name, state: held?.state ?? 'active', reason: held?.reason ?? null };
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
if (registered && (!current || current.name !== registered.name) && this.onToolRemoved) {
|
|
1135
|
+
await this.onToolRemoved(registered);
|
|
1136
|
+
}
|
|
1137
|
+
if (current && this.onToolForged) {
|
|
1138
|
+
await this.onToolForged(current, this.createExecutableTool(current));
|
|
1139
|
+
}
|
|
1140
|
+
registered = current;
|
|
1141
|
+
}
|
|
1142
|
+
catch (error) {
|
|
1143
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1144
|
+
await this.dropExecutable(registered ?? current ?? tool);
|
|
1145
|
+
if (!this.registry.get(tool.id)) {
|
|
1146
|
+
this.removeIndexedToolEverywhere(tool.id);
|
|
1147
|
+
}
|
|
1148
|
+
throw new Error(`the executable of "${tool.name}" (${tool.id}) could not be brought in line with the registry: ${message}`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
await this.dropExecutable(registered ?? tool);
|
|
1152
|
+
throw new Error(`the executable of "${tool.name}" (${tool.id}) could not be settled: the tool kept changing while it was registered`);
|
|
1153
|
+
}
|
|
1154
|
+
/** Take a tool's executable out of the host, best-effort. */
|
|
1155
|
+
async dropExecutable(tool) {
|
|
1156
|
+
if (!this.onToolRemoved) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
try {
|
|
1160
|
+
await this.onToolRemoved(tool);
|
|
1161
|
+
}
|
|
1162
|
+
catch (error) {
|
|
1163
|
+
console.warn(`[agentos:emergent] could not take out the executable of "${tool.name}" (${tool.id}):`, error instanceof Error ? error.message : error);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
884
1166
|
/**
|
|
885
1167
|
* The library's reason for not running an implementation under the
|
|
886
1168
|
* configuration in force, or `null`. Later steps add their checks here, so
|
|
887
1169
|
* forging, loading and promotion all ask the same question.
|
|
888
1170
|
*/
|
|
889
|
-
refusalFor(implementation) {
|
|
890
|
-
if (implementation.mode === 'sandbox'
|
|
891
|
-
|
|
1171
|
+
refusalFor(implementation, name) {
|
|
1172
|
+
if (implementation.mode === 'sandbox') {
|
|
1173
|
+
if (!this.config.allowSandboxTools) {
|
|
1174
|
+
return 'sandbox_tools_off';
|
|
1175
|
+
}
|
|
1176
|
+
if (implementation.code.trim() === '') {
|
|
1177
|
+
return 'source_not_persisted';
|
|
1178
|
+
}
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
for (const step of implementation.steps) {
|
|
1182
|
+
const verdict = this.composableBuilder.check(step.tool);
|
|
1183
|
+
if (!verdict.ok) {
|
|
1184
|
+
return verdict.code;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if (name !== undefined && this.compositionCycle(name, implementation.steps)) {
|
|
1188
|
+
return 'step_cycle';
|
|
892
1189
|
}
|
|
893
1190
|
return null;
|
|
894
1191
|
}
|
|
1192
|
+
/**
|
|
1193
|
+
* The path of step tool names through which a composition named `name`
|
|
1194
|
+
* reaches itself, or a nested composition reaches one already on the path;
|
|
1195
|
+
* null when there is none. Nested compositions are followed through the
|
|
1196
|
+
* tools the gate resolves now.
|
|
1197
|
+
*/
|
|
1198
|
+
compositionCycle(name, steps, path = []) {
|
|
1199
|
+
for (const step of steps) {
|
|
1200
|
+
if (step.tool === name || path.includes(step.tool)) {
|
|
1201
|
+
return [...path, step.tool];
|
|
1202
|
+
}
|
|
1203
|
+
const nested = this.nestedSteps(step.tool);
|
|
1204
|
+
if (nested) {
|
|
1205
|
+
const found = this.compositionCycle(name, nested, [...path, step.tool]);
|
|
1206
|
+
if (found) {
|
|
1207
|
+
return found;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
/** The steps of the emergent composition registered as `toolName`, or undefined. */
|
|
1214
|
+
nestedSteps(toolName) {
|
|
1215
|
+
const resolved = this.composableBuilder.resolve(toolName);
|
|
1216
|
+
if (resolved?.emergentMode !== 'compose' || !resolved.id.startsWith('emergent-tool:')) {
|
|
1217
|
+
return undefined;
|
|
1218
|
+
}
|
|
1219
|
+
const implementation = this.registry.get(resolved.id.slice('emergent-tool:'.length))?.implementation;
|
|
1220
|
+
return implementation?.mode === 'compose' ? implementation.steps : undefined;
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* A suspension the library sets from a run or a promotion check: written as
|
|
1224
|
+
* the library's (so a later load, or a registration of the missing step
|
|
1225
|
+
* tool, re-checks it), held here, and the executable taken out.
|
|
1226
|
+
*/
|
|
1227
|
+
async suspendAsLibrary(toolId, reason) {
|
|
1228
|
+
try {
|
|
1229
|
+
await this.registry.setState(toolId, 'suspended', reason, { setBy: 'library' });
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
console.warn(`[agentos:emergent] could not store the suspension of "${toolId}" (${reason}):`, error instanceof Error ? error.message : error);
|
|
1233
|
+
}
|
|
1234
|
+
await this.unregisterIfLive(toolId);
|
|
1235
|
+
}
|
|
895
1236
|
async unregisterIfLive(toolId) {
|
|
896
1237
|
const live = this.registry.get(toolId);
|
|
897
1238
|
if (live && this.onToolRemoved) {
|
|
@@ -994,6 +1335,7 @@ export class EmergentCapabilityEngine {
|
|
|
994
1335
|
},
|
|
995
1336
|
executeTool: deps.executeTool,
|
|
996
1337
|
listTools: deps.listTools,
|
|
1338
|
+
checkStep: (name) => this.composableBuilder.check(name),
|
|
997
1339
|
}));
|
|
998
1340
|
}
|
|
999
1341
|
catch {
|
|
@@ -1043,7 +1385,12 @@ export class EmergentCapabilityEngine {
|
|
|
1043
1385
|
inputSchema: tool.inputSchema,
|
|
1044
1386
|
outputSchema: tool.outputSchema,
|
|
1045
1387
|
category: 'emergent',
|
|
1046
|
-
|
|
1388
|
+
emergentMode: tool.implementation.mode,
|
|
1389
|
+
// A composition has side effects when one of its steps does (or is a
|
|
1390
|
+
// composition itself); each such step is then asked at the step (see
|
|
1391
|
+
// ToolOrchestrator), not at the composed call.
|
|
1392
|
+
hasSideEffects: tool.implementation.mode === 'sandbox' ||
|
|
1393
|
+
this.composableBuilder.hasSideEffectingStep(tool.implementation),
|
|
1047
1394
|
execute: async (args, context) => {
|
|
1048
1395
|
// The registry no longer holds the tool (removed, or its session
|
|
1049
1396
|
// cleaned up) while the executable is still registered: refuse, do
|
|
@@ -1064,11 +1411,12 @@ export class EmergentCapabilityEngine {
|
|
|
1064
1411
|
// The owner is the persona that forged the tool, compared as it was
|
|
1065
1412
|
// stored ('unknown' for a caller without one). A row from an earlier
|
|
1066
1413
|
// release holds the forging GMI instance's id instead, which no
|
|
1067
|
-
// persona can match
|
|
1068
|
-
//
|
|
1414
|
+
// persona can match; the loader suspends such a row (`legacy_owner`),
|
|
1415
|
+
// and a call to one held in memory is refused here like any other
|
|
1416
|
+
// owner mismatch.
|
|
1069
1417
|
const owner = current.createdBy;
|
|
1070
1418
|
const caller = context.personaId ?? 'unknown';
|
|
1071
|
-
if (current.tier === 'agent' &&
|
|
1419
|
+
if (current.tier === 'agent' && caller !== owner) {
|
|
1072
1420
|
return {
|
|
1073
1421
|
success: false,
|
|
1074
1422
|
error: `Emergent tool "${tool.name}" belongs to agent ${owner}; it is not callable as ${caller}.`,
|
|
@@ -1077,6 +1425,20 @@ export class EmergentCapabilityEngine {
|
|
|
1077
1425
|
const startTime = performance.now();
|
|
1078
1426
|
const result = await baseTool.execute(args, context);
|
|
1079
1427
|
const executionTimeMs = Math.round(performance.now() - startTime);
|
|
1428
|
+
// A step that can no longer be chained (its tool was removed or
|
|
1429
|
+
// replaced, or the chain reaches itself) suspends the composition
|
|
1430
|
+
// until a fitting tool is registered.
|
|
1431
|
+
const refused = result.details?.code;
|
|
1432
|
+
const suspendFor = !result.success && refused ? RUN_REFUSAL_REASONS[refused] : undefined;
|
|
1433
|
+
if (suspendFor) {
|
|
1434
|
+
await this.suspendAsLibrary(tool.id, suspendFor);
|
|
1435
|
+
return {
|
|
1436
|
+
success: false,
|
|
1437
|
+
output: result.output,
|
|
1438
|
+
error: result.error ?? 'A step of this composition can no longer be chained.',
|
|
1439
|
+
details: result.details,
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1080
1442
|
let success = result.success;
|
|
1081
1443
|
let error = result.error;
|
|
1082
1444
|
// Track whether the output passed schema validation separately from
|
|
@@ -1104,6 +1466,7 @@ export class EmergentCapabilityEngine {
|
|
|
1104
1466
|
success: false,
|
|
1105
1467
|
output: result.output,
|
|
1106
1468
|
error: error ?? 'Emergent tool execution failed.',
|
|
1469
|
+
...(result.details ? { details: result.details } : {}),
|
|
1107
1470
|
};
|
|
1108
1471
|
},
|
|
1109
1472
|
};
|