@framers/agentos 0.11.6 → 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 +61 -7
- package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts.map +1 -1
- package/dist/cognition/emergent/EmergentCapabilityEngine.js +389 -100
- 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 +40 -10
- package/dist/cognition/emergent/EmergentToolRegistry.d.ts.map +1 -1
- package/dist/cognition/emergent/EmergentToolRegistry.js +84 -16
- 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 +28 -0
- 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/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/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;
|
|
152
203
|
}
|
|
204
|
+
const details = result.details;
|
|
205
|
+
if (details?.code === 'dry_run_needs_output') {
|
|
206
|
+
return { success: false, error: result.error };
|
|
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 {
|
|
@@ -289,7 +361,16 @@ export class EmergentCapabilityEngine {
|
|
|
289
361
|
error: error instanceof Error ? error.message : 'Failed to activate forged tool.',
|
|
290
362
|
};
|
|
291
363
|
}
|
|
292
|
-
|
|
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
|
+
}
|
|
293
374
|
if (settled && settled.reason === 'removed') {
|
|
294
375
|
// Removed while the host was registering it: nothing is registered.
|
|
295
376
|
return { success: false, error: 'the tool was removed while it was being forged' };
|
|
@@ -334,10 +415,21 @@ export class EmergentCapabilityEngine {
|
|
|
334
415
|
if (!tool) {
|
|
335
416
|
return null;
|
|
336
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
|
+
}
|
|
337
424
|
// Only session-tier tools are eligible for auto-promotion.
|
|
338
425
|
if (tool.tier !== 'session') {
|
|
339
426
|
return null;
|
|
340
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
|
+
}
|
|
341
433
|
// Check thresholds.
|
|
342
434
|
const { uses, confidence } = this.config.promotionThreshold;
|
|
343
435
|
if (tool.usageStats.totalUses < uses || tool.usageStats.confidenceScore < confidence) {
|
|
@@ -440,30 +532,34 @@ export class EmergentCapabilityEngine {
|
|
|
440
532
|
* @returns what happened, so a host can tell a registered tool from a refused one.
|
|
441
533
|
*/
|
|
442
534
|
async syncPersistedTool(tool) {
|
|
443
|
-
|
|
444
|
-
if (!
|
|
535
|
+
const existing = await this.registry.loadRow(tool.id);
|
|
536
|
+
if (!existing && this.registry.hasStorage()) {
|
|
445
537
|
// No stored row yet: the host hydrates from its own store. The row is
|
|
446
538
|
// written as it was before loading went through the stored row, so the
|
|
447
539
|
// tool's uses are recorded and the next load finds it.
|
|
448
540
|
await this.registry.writeToolRow(tool);
|
|
449
|
-
row = await this.registry.loadRow(tool.id);
|
|
450
|
-
}
|
|
451
|
-
if (row) {
|
|
452
|
-
// With source persistence off the row holds a redacted record; the
|
|
453
|
-
// implementation the host supplied is what runs then.
|
|
454
|
-
return this.admitRow(row, { sourceFallback: sourceFromImplementation(tool.implementation) });
|
|
455
541
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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 });
|
|
467
563
|
});
|
|
468
564
|
}
|
|
469
565
|
/**
|
|
@@ -516,17 +612,47 @@ export class EmergentCapabilityEngine {
|
|
|
516
612
|
throw new Error("selector_required: loadPersistedTools({ tiers: ['session'] }) needs the sessionId whose tools to load; " +
|
|
517
613
|
"a session's tools are private to it.");
|
|
518
614
|
}
|
|
519
|
-
const rows = await this.registry.loadRows(tiers, { agentId, sessionId });
|
|
520
615
|
const outcomes = [];
|
|
521
616
|
const failed = [];
|
|
522
|
-
|
|
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
|
+
}
|
|
523
641
|
try {
|
|
524
|
-
|
|
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
|
+
}
|
|
525
649
|
}
|
|
526
650
|
catch (error) {
|
|
527
651
|
const message = error instanceof Error ? error.message : String(error);
|
|
528
|
-
|
|
529
|
-
|
|
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}`);
|
|
530
656
|
}
|
|
531
657
|
}
|
|
532
658
|
for (const outcome of outcomes) {
|
|
@@ -602,26 +728,69 @@ export class EmergentCapabilityEngine {
|
|
|
602
728
|
* @returns the outcome, or `undefined` when the tool is unknown.
|
|
603
729
|
*/
|
|
604
730
|
async reactivateTool(toolId) {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
+
}
|
|
608
755
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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();
|
|
612
793
|
}
|
|
613
|
-
const held = this.registry.getState(toolId);
|
|
614
|
-
return this.admit({
|
|
615
|
-
toolId,
|
|
616
|
-
name: tool.name,
|
|
617
|
-
source: sourceFromImplementation(tool.implementation),
|
|
618
|
-
stored: held,
|
|
619
|
-
requestStored: held?.request != null,
|
|
620
|
-
legacyActive: true,
|
|
621
|
-
tier: tool.tier,
|
|
622
|
-
createdBy: tool.createdBy,
|
|
623
|
-
buildTool: () => tool,
|
|
624
|
-
}, { force: true });
|
|
625
794
|
}
|
|
626
795
|
admitRow(row, options = {}) {
|
|
627
796
|
const stored = row.state
|
|
@@ -676,12 +845,17 @@ export class EmergentCapabilityEngine {
|
|
|
676
845
|
const { toolId, name } = candidate;
|
|
677
846
|
const depth = options.readmitted ?? 0;
|
|
678
847
|
if (depth < 2) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
+
});
|
|
685
859
|
}
|
|
686
860
|
// Nothing is written; the tool is held off here until the next load.
|
|
687
861
|
this.holdStored(toolId, {
|
|
@@ -697,10 +871,9 @@ export class EmergentCapabilityEngine {
|
|
|
697
871
|
}
|
|
698
872
|
async admit(candidate, options = {}) {
|
|
699
873
|
const { toolId, name, source, stored, requestStored } = candidate;
|
|
700
|
-
// The
|
|
701
|
-
//
|
|
702
|
-
|
|
703
|
-
const generation = this.registry.generation(toolId);
|
|
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);
|
|
704
877
|
let legacyActive = candidate.legacyActive;
|
|
705
878
|
// A row whose last state write did not finish its flag write: finish it
|
|
706
879
|
// first, so the flag reads what the state row says before anything is
|
|
@@ -792,11 +965,11 @@ export class EmergentCapabilityEngine {
|
|
|
792
965
|
// ever pass the owner check: it loads suspended rather than running
|
|
793
966
|
// for everyone. Forging the tool again under the persona is the way
|
|
794
967
|
// back; `reactivateTool` goes through this same path.
|
|
795
|
-
if (!refusal && candidate.tier === 'agent' && candidate.createdBy.startsWith(
|
|
968
|
+
if (!refusal && candidate.tier === 'agent' && candidate.createdBy.startsWith(GMI_INSTANCE_ID_PREFIX)) {
|
|
796
969
|
refusal = 'legacy_owner';
|
|
797
970
|
}
|
|
798
971
|
if (implementation && !refusal) {
|
|
799
|
-
refusal = this.refusalFor(implementation);
|
|
972
|
+
refusal = this.refusalFor(implementation, name);
|
|
800
973
|
}
|
|
801
974
|
if (refusal || !implementation) {
|
|
802
975
|
const reason = refusal ?? 'source_unreadable';
|
|
@@ -891,11 +1064,22 @@ export class EmergentCapabilityEngine {
|
|
|
891
1064
|
setBy: 'library',
|
|
892
1065
|
at: Date.now(),
|
|
893
1066
|
request,
|
|
894
|
-
},
|
|
1067
|
+
}, readAt);
|
|
895
1068
|
if (!adopted) {
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
|
|
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 };
|
|
899
1083
|
}
|
|
900
1084
|
this.indexTool(tool.id, tool.createdBy, this.extractSessionId(tool.source) ?? `persisted:${tool.id}`);
|
|
901
1085
|
if (this.onToolForged) {
|
|
@@ -922,49 +1106,133 @@ export class EmergentCapabilityEngine {
|
|
|
922
1106
|
* hold that very object. When it does not (the tool was removed, or
|
|
923
1107
|
* replaced under its id, while the registration ran), the executor is
|
|
924
1108
|
* brought back in line with the registry: the current tool's executable is
|
|
925
|
-
* registered
|
|
926
|
-
*
|
|
927
|
-
* is
|
|
928
|
-
*
|
|
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.
|
|
929
1117
|
*/
|
|
930
1118
|
async settleRegistration(tool) {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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 };
|
|
938
1132
|
}
|
|
939
|
-
|
|
940
|
-
|
|
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}`);
|
|
941
1149
|
}
|
|
942
1150
|
}
|
|
943
|
-
|
|
944
|
-
|
|
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;
|
|
945
1158
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
this.removeIndexedToolEverywhere(tool.id);
|
|
949
|
-
}
|
|
1159
|
+
try {
|
|
1160
|
+
await this.onToolRemoved(tool);
|
|
950
1161
|
}
|
|
951
|
-
|
|
952
|
-
|
|
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);
|
|
953
1164
|
}
|
|
954
|
-
const held = this.registry.getState(tool.id);
|
|
955
|
-
return { toolId: tool.id, name: current.name, state: held?.state ?? 'active', reason: held?.reason ?? null };
|
|
956
1165
|
}
|
|
957
1166
|
/**
|
|
958
1167
|
* The library's reason for not running an implementation under the
|
|
959
1168
|
* configuration in force, or `null`. Later steps add their checks here, so
|
|
960
1169
|
* forging, loading and promotion all ask the same question.
|
|
961
1170
|
*/
|
|
962
|
-
refusalFor(implementation) {
|
|
963
|
-
if (implementation.mode === 'sandbox'
|
|
964
|
-
|
|
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';
|
|
1189
|
+
}
|
|
1190
|
+
return null;
|
|
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
|
+
}
|
|
965
1210
|
}
|
|
966
1211
|
return null;
|
|
967
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
|
+
}
|
|
968
1236
|
async unregisterIfLive(toolId) {
|
|
969
1237
|
const live = this.registry.get(toolId);
|
|
970
1238
|
if (live && this.onToolRemoved) {
|
|
@@ -1067,6 +1335,7 @@ export class EmergentCapabilityEngine {
|
|
|
1067
1335
|
},
|
|
1068
1336
|
executeTool: deps.executeTool,
|
|
1069
1337
|
listTools: deps.listTools,
|
|
1338
|
+
checkStep: (name) => this.composableBuilder.check(name),
|
|
1070
1339
|
}));
|
|
1071
1340
|
}
|
|
1072
1341
|
catch {
|
|
@@ -1116,7 +1385,12 @@ export class EmergentCapabilityEngine {
|
|
|
1116
1385
|
inputSchema: tool.inputSchema,
|
|
1117
1386
|
outputSchema: tool.outputSchema,
|
|
1118
1387
|
category: 'emergent',
|
|
1119
|
-
|
|
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),
|
|
1120
1394
|
execute: async (args, context) => {
|
|
1121
1395
|
// The registry no longer holds the tool (removed, or its session
|
|
1122
1396
|
// cleaned up) while the executable is still registered: refuse, do
|
|
@@ -1151,6 +1425,20 @@ export class EmergentCapabilityEngine {
|
|
|
1151
1425
|
const startTime = performance.now();
|
|
1152
1426
|
const result = await baseTool.execute(args, context);
|
|
1153
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
|
+
}
|
|
1154
1442
|
let success = result.success;
|
|
1155
1443
|
let error = result.error;
|
|
1156
1444
|
// Track whether the output passed schema validation separately from
|
|
@@ -1178,6 +1466,7 @@ export class EmergentCapabilityEngine {
|
|
|
1178
1466
|
success: false,
|
|
1179
1467
|
output: result.output,
|
|
1180
1468
|
error: error ?? 'Emergent tool execution failed.',
|
|
1469
|
+
...(result.details ? { details: result.details } : {}),
|
|
1181
1470
|
};
|
|
1182
1471
|
},
|
|
1183
1472
|
};
|