@animalabs/agent-framework 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/agent.d.ts +1 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +58 -0
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +437 -28
- package/dist/src/framework.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +21 -5
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +5 -3
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +9 -3
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +9 -6
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/queue.d.ts.map +1 -1
- package/dist/src/queue.js +9 -2
- package/dist/src/queue.js.map +1 -1
- package/dist/src/types/agent.d.ts +3 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +7 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/busy-agent-scheduler.test.js +29 -0
- package/dist/test/busy-agent-scheduler.test.js.map +1 -1
- package/dist/test/discord-history-delete-window.test.d.ts +2 -0
- package/dist/test/discord-history-delete-window.test.d.ts.map +1 -0
- package/dist/test/discord-history-delete-window.test.js +81 -0
- package/dist/test/discord-history-delete-window.test.js.map +1 -0
- package/dist/test/discord-history-sync.test.d.ts +2 -0
- package/dist/test/discord-history-sync.test.d.ts.map +1 -0
- package/dist/test/discord-history-sync.test.js +56 -0
- package/dist/test/discord-history-sync.test.js.map +1 -0
- package/dist/test/explicit-prose-routing.test.js +42 -0
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/framework.test.js +33 -0
- package/dist/test/framework.test.js.map +1 -1
- package/dist/test/provider-acceleration-cooldown.test.d.ts +2 -0
- package/dist/test/provider-acceleration-cooldown.test.d.ts.map +1 -0
- package/dist/test/provider-acceleration-cooldown.test.js +286 -0
- package/dist/test/provider-acceleration-cooldown.test.js.map +1 -0
- package/dist/test/puppet-tool-call.test.d.ts +2 -0
- package/dist/test/puppet-tool-call.test.d.ts.map +1 -0
- package/dist/test/puppet-tool-call.test.js +106 -0
- package/dist/test/puppet-tool-call.test.js.map +1 -0
- package/dist/test/release-changelog.test.d.ts +2 -0
- package/dist/test/release-changelog.test.d.ts.map +1 -0
- package/dist/test/release-changelog.test.js +159 -0
- package/dist/test/release-changelog.test.js.map +1 -0
- package/dist/test/workspace-path-containment.test.d.ts +19 -0
- package/dist/test/workspace-path-containment.test.d.ts.map +1 -0
- package/dist/test/workspace-path-containment.test.js +91 -0
- package/dist/test/workspace-path-containment.test.js.map +1 -0
- package/dist/test/workspace-watcher.test.js +15 -3
- package/dist/test/workspace-watcher.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/agent.ts +1 -1
- package/src/framework.ts +467 -27
- package/src/modules/discord/index.ts +24 -6
- package/src/modules/workspace/index.ts +5 -3
- package/src/modules/workspace/sync.ts +9 -3
- package/src/modules/workspace/watcher.ts +9 -6
- package/src/queue.ts +8 -2
- package/src/types/agent.ts +3 -1
- package/src/types/trace.ts +13 -0
- package/dist/test/fixtures/awareness-barrier-mcpl-server.mjs +0 -276
- package/dist/test/fixtures/flaky-mcpl-server.mjs +0 -41
package/src/framework.ts
CHANGED
|
@@ -228,7 +228,7 @@ const isAddressedMessage = (
|
|
|
228
228
|
// envelopes can never disagree on what is a prefix.
|
|
229
229
|
|
|
230
230
|
/** One-time primer appended when an agent's proseRouting mode changes. */
|
|
231
|
-
function proseModePrimer(mode: 'explicit' | 'hybrid' | 'locus'): string {
|
|
231
|
+
function proseModePrimer(mode: 'explicit' | 'hybrid' | 'locus' | 'disabled'): string {
|
|
232
232
|
if (mode === 'locus') {
|
|
233
233
|
return '[prose-routing] Mode change: your plain text auto-routes to the ' +
|
|
234
234
|
'conversational locus again. `>>` destination prefixes are no longer needed.';
|
|
@@ -475,6 +475,53 @@ const DEFAULT_MAINTENANCE_INTERVAL_MS = 5000;
|
|
|
475
475
|
/** Bound one pass so a large backlog yields to inference and other agents. */
|
|
476
476
|
const MAINTENANCE_TICKS_PER_PASS = 8;
|
|
477
477
|
|
|
478
|
+
/** Local per-residence provider admission (AF #114 bounded first slice). */
|
|
479
|
+
interface LocalProviderGate {
|
|
480
|
+
primaryDepth: number;
|
|
481
|
+
primaryPending: boolean;
|
|
482
|
+
auxiliaryInFlight: number;
|
|
483
|
+
auxiliaryWaiters: Array<() => void>;
|
|
484
|
+
idleWaiters: Array<() => void>;
|
|
485
|
+
deferredAuxiliary: number;
|
|
486
|
+
}
|
|
487
|
+
interface ProviderAccelerationCooldown {
|
|
488
|
+
startedAt: number;
|
|
489
|
+
until: number;
|
|
490
|
+
timer: ReturnType<typeof setTimeout>;
|
|
491
|
+
heldRequests: InferenceRequest[];
|
|
492
|
+
reason: string;
|
|
493
|
+
failures: number;
|
|
494
|
+
}
|
|
495
|
+
interface ProviderAccelerationRecovery {
|
|
496
|
+
startedAt: number;
|
|
497
|
+
releasedAt?: number;
|
|
498
|
+
failures: number;
|
|
499
|
+
heldRequests: number;
|
|
500
|
+
reason: string;
|
|
501
|
+
}
|
|
502
|
+
interface ProviderAccelerationReceipt {
|
|
503
|
+
startedAt: number;
|
|
504
|
+
releasedAt: number;
|
|
505
|
+
completedAt: number;
|
|
506
|
+
cooldownMs: number;
|
|
507
|
+
waitedMs: number;
|
|
508
|
+
failures: number;
|
|
509
|
+
heldRequests: number;
|
|
510
|
+
messageCount: number;
|
|
511
|
+
toolCount: number;
|
|
512
|
+
stopReason: string;
|
|
513
|
+
}
|
|
514
|
+
const PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS = 65_000;
|
|
515
|
+
const PROVIDER_ACCELERATION_MAX_COOLDOWN_MS = 10 * 60_000;
|
|
516
|
+
const PROVIDER_ACCELERATION_JITTER_MS = 5_000;
|
|
517
|
+
function isOrganizationAccelerationRateLimit(error: Error): error is MembraneError {
|
|
518
|
+
if (!(error instanceof MembraneError) || error.type !== 'rate_limit') return false;
|
|
519
|
+
return (
|
|
520
|
+
/organization(?:'s)?[^\n]{0,120}maximum(?:\s+usage)?\s+increase\s+rate/i.test(error.message) ||
|
|
521
|
+
/organization(?:'s)?[^\n]{0,120}acceleration(?:\s+limit)?/i.test(error.message)
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
478
525
|
/**
|
|
479
526
|
* Extract fields the EventGate cares about from a ProcessEvent. The set of
|
|
480
527
|
* event variants that carry `content`/`mount`/`paths`/`metadata` is open-ended
|
|
@@ -623,6 +670,13 @@ export class AgentFramework {
|
|
|
623
670
|
private maintenanceRunId = 0;
|
|
624
671
|
private currentMaintenanceRun: ContextMaintenanceRun | null = null;
|
|
625
672
|
private maintenanceHistory: ContextMaintenanceRun[] = [];
|
|
673
|
+
private providerGates: Map<string, LocalProviderGate> = new Map();
|
|
674
|
+
private providerAccelerationCooldowns: Map<string, ProviderAccelerationCooldown> = new Map();
|
|
675
|
+
private providerAccelerationRecoveries: Map<string, ProviderAccelerationRecovery> = new Map();
|
|
676
|
+
private providerAccelerationLastRecovery: Map<string, ProviderAccelerationReceipt> = new Map();
|
|
677
|
+
private providerAccelerationDefaultCooldownMs = PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS;
|
|
678
|
+
private providerAccelerationJitterMs = PROVIDER_ACCELERATION_JITTER_MS;
|
|
679
|
+
private providerAdmissionClosed = false;
|
|
626
680
|
/** Last time we reported stale (busy-requeued) inference requests, per agent. */
|
|
627
681
|
private staleWarnAt = new Map<string, number>();
|
|
628
682
|
/** Per-agent last inference activity (epoch ms), for /healthz + doctor tooling. */
|
|
@@ -1197,6 +1251,7 @@ export class AgentFramework {
|
|
|
1197
1251
|
}
|
|
1198
1252
|
|
|
1199
1253
|
this.running = true;
|
|
1254
|
+
this.providerAdmissionClosed = false;
|
|
1200
1255
|
this.loopPromise = this.runLoop();
|
|
1201
1256
|
|
|
1202
1257
|
// Start periodic sync timer (if enabled)
|
|
@@ -1226,6 +1281,7 @@ export class AgentFramework {
|
|
|
1226
1281
|
*/
|
|
1227
1282
|
async stop(): Promise<void> {
|
|
1228
1283
|
this.running = false;
|
|
1284
|
+
this.providerAdmissionClosed = true;
|
|
1229
1285
|
this.queue.close();
|
|
1230
1286
|
|
|
1231
1287
|
// Kill running code_execution scripts before cancelling streams: a
|
|
@@ -1247,6 +1303,9 @@ export class AgentFramework {
|
|
|
1247
1303
|
}
|
|
1248
1304
|
this.backgroundScripts.clear();
|
|
1249
1305
|
|
|
1306
|
+
// A stopped host must never hang behind its own cooldown.
|
|
1307
|
+
this.cancelProviderAdmission();
|
|
1308
|
+
|
|
1250
1309
|
// Cancel all active streams
|
|
1251
1310
|
for (const agent of this.agents.values()) {
|
|
1252
1311
|
if (agent.state.status === 'streaming' ||
|
|
@@ -1312,6 +1371,138 @@ export class AgentFramework {
|
|
|
1312
1371
|
* schemas are absent. Passes never overlap; each agent gets a bounded drain
|
|
1313
1372
|
* so maintenance cannot monopolize the framework event loop.
|
|
1314
1373
|
*/
|
|
1374
|
+
private providerGate(agentName: string): LocalProviderGate {
|
|
1375
|
+
let gate = this.providerGates.get(agentName);
|
|
1376
|
+
if (!gate) {
|
|
1377
|
+
gate = { primaryDepth: 0, primaryPending: false, auxiliaryInFlight: 0,
|
|
1378
|
+
auxiliaryWaiters: [], idleWaiters: [], deferredAuxiliary: 0 };
|
|
1379
|
+
this.providerGates.set(agentName, gate);
|
|
1380
|
+
}
|
|
1381
|
+
return gate;
|
|
1382
|
+
}
|
|
1383
|
+
private providerGateBlocked(agentName: string): boolean {
|
|
1384
|
+
const gate = this.providerGates.get(agentName);
|
|
1385
|
+
return (gate?.primaryDepth ?? 0) > 0 || (gate?.primaryPending ?? false) ||
|
|
1386
|
+
this.providerAccelerationCooldowns.has(agentName);
|
|
1387
|
+
}
|
|
1388
|
+
private acquirePrimaryProviderGate(agentName: string): void {
|
|
1389
|
+
const gate = this.providerGate(agentName); gate.primaryPending = false; gate.primaryDepth++;
|
|
1390
|
+
}
|
|
1391
|
+
private releasePrimaryProviderGate(agentName: string): void {
|
|
1392
|
+
const gate = this.providerGate(agentName); gate.primaryDepth = Math.max(0, gate.primaryDepth - 1);
|
|
1393
|
+
this.flushAuxiliaryAdmission(agentName);
|
|
1394
|
+
}
|
|
1395
|
+
private flushAuxiliaryAdmission(agentName: string): void {
|
|
1396
|
+
const gate = this.providerGate(agentName);
|
|
1397
|
+
if (gate.primaryDepth > 0 || gate.primaryPending || this.providerAccelerationCooldowns.has(agentName)) return;
|
|
1398
|
+
for (const resolve of gate.auxiliaryWaiters.splice(0)) resolve();
|
|
1399
|
+
}
|
|
1400
|
+
private async waitForAuxiliaryIdle(agentName: string): Promise<void> {
|
|
1401
|
+
const gate = this.providerGate(agentName);
|
|
1402
|
+
if (gate.auxiliaryInFlight === 0) return;
|
|
1403
|
+
await new Promise<void>((resolve) => gate.idleWaiters.push(resolve));
|
|
1404
|
+
}
|
|
1405
|
+
private async withAuxiliaryAdmission<T>(agentName: string, run: () => Promise<T>): Promise<T> {
|
|
1406
|
+
const gate = this.providerGate(agentName);
|
|
1407
|
+
if (this.providerGateBlocked(agentName)) {
|
|
1408
|
+
gate.deferredAuxiliary++;
|
|
1409
|
+
try {
|
|
1410
|
+
await new Promise<void>((resolve) => gate.auxiliaryWaiters.push(resolve));
|
|
1411
|
+
} finally {
|
|
1412
|
+
gate.deferredAuxiliary = Math.max(0, gate.deferredAuxiliary - 1);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (this.providerAdmissionClosed) {
|
|
1416
|
+
throw new Error(`Provider admission closed while stopping (${agentName})`);
|
|
1417
|
+
}
|
|
1418
|
+
gate.auxiliaryInFlight++;
|
|
1419
|
+
try { return await run(); }
|
|
1420
|
+
finally {
|
|
1421
|
+
gate.auxiliaryInFlight = Math.max(0, gate.auxiliaryInFlight - 1);
|
|
1422
|
+
if (gate.auxiliaryInFlight === 0) for (const resolve of gate.idleWaiters.splice(0)) resolve();
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
private auxiliaryMembraneFor(agentName: string): Membrane {
|
|
1426
|
+
const target = this.membrane as unknown as Record<PropertyKey, unknown>;
|
|
1427
|
+
return new Proxy(target, { get: (obj, prop, receiver) => {
|
|
1428
|
+
const value = Reflect.get(obj, prop, receiver);
|
|
1429
|
+
if (prop === 'complete' && typeof value === 'function') {
|
|
1430
|
+
return (...args: unknown[]) => this.withAuxiliaryAdmission(
|
|
1431
|
+
agentName, () => Reflect.apply(value, this.membrane, args) as Promise<unknown>);
|
|
1432
|
+
}
|
|
1433
|
+
return typeof value === 'function' ? value.bind(this.membrane) : value;
|
|
1434
|
+
}}) as unknown as Membrane;
|
|
1435
|
+
}
|
|
1436
|
+
private accelerationCooldownMs(agentName: string, error: MembraneError): number {
|
|
1437
|
+
const base = error.retryAfterMs ?? this.providerAccelerationDefaultCooldownMs;
|
|
1438
|
+
let hash = 0; for (const ch of agentName) hash = ((hash * 31) + ch.charCodeAt(0)) >>> 0;
|
|
1439
|
+
const jitter = this.providerAccelerationJitterMs > 0 ? hash % (this.providerAccelerationJitterMs + 1) : 0;
|
|
1440
|
+
return Math.min(PROVIDER_ACCELERATION_MAX_COOLDOWN_MS, Math.max(1_000, base) + jitter);
|
|
1441
|
+
}
|
|
1442
|
+
private sameInferenceRequest(a: InferenceRequest, b: InferenceRequest): boolean {
|
|
1443
|
+
return a.agentName === b.agentName && a.reason === b.reason && a.source === b.source &&
|
|
1444
|
+
a.timestamp === b.timestamp && a.channelId === b.channelId;
|
|
1445
|
+
}
|
|
1446
|
+
private holdProviderAcceleration(agent: Agent, error: Error, trigger?: InferenceRequest): boolean {
|
|
1447
|
+
if (this.ephemeralRuns.has(agent.name) || this.conversationAgentHomes.has(agent.name) || !isOrganizationAccelerationRateLimit(error)) return false;
|
|
1448
|
+
const now = Date.now(); const delayMs = this.accelerationCooldownMs(agent.name, error);
|
|
1449
|
+
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
1450
|
+
const held = existing?.heldRequests ?? [];
|
|
1451
|
+
if (trigger && !held.some((r) => this.sameInferenceRequest(r, trigger))) held.push(trigger);
|
|
1452
|
+
if (existing) clearTimeout(existing.timer);
|
|
1453
|
+
const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), delayMs); timer.unref?.();
|
|
1454
|
+
this.providerAccelerationCooldowns.set(agent.name, { startedAt: existing?.startedAt ?? now,
|
|
1455
|
+
until: now + delayMs, timer, heldRequests: held, reason: error.message,
|
|
1456
|
+
failures: (existing?.failures ?? 0) + 1 });
|
|
1457
|
+
const gate = this.providerGate(agent.name); gate.primaryPending = true;
|
|
1458
|
+
this.providerAccelerationRecoveries.set(agent.name, { startedAt: existing?.startedAt ?? now,
|
|
1459
|
+
failures: (existing?.failures ?? 0) + 1, heldRequests: held.length, reason: error.message });
|
|
1460
|
+
console.error(`[provider-cooldown] agent=${agent.name} organization acceleration 429 — ` +
|
|
1461
|
+
`holding primary/auxiliary for ${delayMs}ms; ${held.length} request(s) retained`);
|
|
1462
|
+
return true;
|
|
1463
|
+
}
|
|
1464
|
+
private releaseProviderAccelerationCooldown(agentName: string): void {
|
|
1465
|
+
const cooldown = this.providerAccelerationCooldowns.get(agentName); if (!cooldown) return;
|
|
1466
|
+
clearTimeout(cooldown.timer); this.providerAccelerationCooldowns.delete(agentName);
|
|
1467
|
+
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
1468
|
+
if (recovery) { recovery.releasedAt = Date.now(); recovery.heldRequests = cooldown.heldRequests.length; }
|
|
1469
|
+
const requests = cooldown.heldRequests.length > 0 ? cooldown.heldRequests : [{ agentName,
|
|
1470
|
+
reason: 'provider-acceleration-retry', source: 'framework', timestamp: Date.now() } satisfies InferenceRequest];
|
|
1471
|
+
this.pendingRequests.push(...requests);
|
|
1472
|
+
console.error(`[provider-cooldown] agent=${agentName} released after ${Date.now() - cooldown.startedAt}ms — ` +
|
|
1473
|
+
`fresh compile queued with ${requests.length} retained request(s)`);
|
|
1474
|
+
}
|
|
1475
|
+
private recordProviderAccelerationRecovery(agent: Agent, request: NormalizedRequest | undefined, stopReason: string): void {
|
|
1476
|
+
const recovery = this.providerAccelerationRecoveries.get(agent.name); if (!recovery?.releasedAt) return;
|
|
1477
|
+
this.providerAccelerationRecoveries.delete(agent.name);
|
|
1478
|
+
const completedAt = Date.now();
|
|
1479
|
+
const receipt: ProviderAccelerationReceipt = {
|
|
1480
|
+
startedAt: recovery.startedAt,
|
|
1481
|
+
releasedAt: recovery.releasedAt,
|
|
1482
|
+
completedAt,
|
|
1483
|
+
cooldownMs: recovery.releasedAt - recovery.startedAt,
|
|
1484
|
+
waitedMs: completedAt - recovery.startedAt,
|
|
1485
|
+
failures: recovery.failures,
|
|
1486
|
+
heldRequests: recovery.heldRequests,
|
|
1487
|
+
messageCount: request?.messages.length ?? 0,
|
|
1488
|
+
toolCount: request?.tools?.length ?? 0,
|
|
1489
|
+
stopReason,
|
|
1490
|
+
};
|
|
1491
|
+
this.providerAccelerationLastRecovery.set(agent.name, receipt);
|
|
1492
|
+
// Operational receipt only: provider admission must never author resident memory.
|
|
1493
|
+
console.error(`[provider-cooldown] recovered ${JSON.stringify({ agent: agent.name, ...receipt })}`);
|
|
1494
|
+
}
|
|
1495
|
+
private cancelProviderAdmission(): void {
|
|
1496
|
+
for (const cooldown of this.providerAccelerationCooldowns.values()) clearTimeout(cooldown.timer);
|
|
1497
|
+
this.providerAccelerationCooldowns.clear(); this.providerAccelerationRecoveries.clear();
|
|
1498
|
+
for (const gate of this.providerGates.values()) {
|
|
1499
|
+
gate.primaryDepth = 0; gate.primaryPending = false;
|
|
1500
|
+
for (const resolve of gate.auxiliaryWaiters.splice(0)) resolve();
|
|
1501
|
+
for (const resolve of gate.idleWaiters.splice(0)) resolve();
|
|
1502
|
+
}
|
|
1503
|
+
this.providerGates.clear();
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1315
1506
|
private startQueuedMaintenance(): void {
|
|
1316
1507
|
if (this.maintenancePass || !this.running) return;
|
|
1317
1508
|
const pass = this.runQueuedMaintenance().catch((error) => {
|
|
@@ -1338,6 +1529,7 @@ export class AgentFramework {
|
|
|
1338
1529
|
const cm = agent.getContextManager();
|
|
1339
1530
|
const tools = this.getToolsForAgent(agent.name).filter((tool) => agent.canUseTool(tool.name));
|
|
1340
1531
|
cm.setToolDefinitions(tools);
|
|
1532
|
+
if (this.providerGateBlocked(agent.name)) return [];
|
|
1341
1533
|
if (cm.isReady()) return [];
|
|
1342
1534
|
const pending = cm.getPendingWork()?.description;
|
|
1343
1535
|
const progress = this.contextProgress(cm);
|
|
@@ -3738,7 +3930,9 @@ export class AgentFramework {
|
|
|
3738
3930
|
store: this.store,
|
|
3739
3931
|
namespace: `agents/${config.name}`,
|
|
3740
3932
|
strategy: config.strategy ?? new PassthroughStrategy(),
|
|
3741
|
-
|
|
3933
|
+
// Context Manager complete() calls are auxiliary work; the primary
|
|
3934
|
+
// streaming agent keeps the original Membrane.
|
|
3935
|
+
membrane: this.auxiliaryMembraneFor(config.name),
|
|
3742
3936
|
debugLogContext: !!process.env.DEBUG_CONTEXT,
|
|
3743
3937
|
});
|
|
3744
3938
|
|
|
@@ -3993,7 +4187,7 @@ export class AgentFramework {
|
|
|
3993
4187
|
// updates lastAnnouncedLocus so the next turn's announce-on-change
|
|
3994
4188
|
// diffs against what the agent was actually last told.
|
|
3995
4189
|
if (
|
|
3996
|
-
agent.proseRouting
|
|
4190
|
+
(agent.proseRouting === 'locus' || agent.proseRouting === 'hybrid') &&
|
|
3997
4191
|
!shouldEndTurn && !overBudget && currentState.stream
|
|
3998
4192
|
) {
|
|
3999
4193
|
// Two signals qualify an injection to move the pin (n=6 + n=7):
|
|
@@ -4462,6 +4656,9 @@ export class AgentFramework {
|
|
|
4462
4656
|
isolate: true,
|
|
4463
4657
|
// Strategy instances are stateful — never share the template's.
|
|
4464
4658
|
strategy: router.strategyFactory?.() ?? new PassthroughStrategy(),
|
|
4659
|
+
// Dynamic conversation forks retain the established provider policy in
|
|
4660
|
+
// this bounded first slice. Provider cooldown ownership belongs to the
|
|
4661
|
+
// persistent resident only; generation-unique forks must not leak gates.
|
|
4465
4662
|
membrane: this.membrane,
|
|
4466
4663
|
debugLogContext: !!process.env.DEBUG_CONTEXT,
|
|
4467
4664
|
});
|
|
@@ -4845,7 +5042,8 @@ export class AgentFramework {
|
|
|
4845
5042
|
this.pendingRequests = [];
|
|
4846
5043
|
|
|
4847
5044
|
// Check each agent
|
|
4848
|
-
for (const [agentName,
|
|
5045
|
+
for (const [agentName, groupedRequests] of requestsByAgent) {
|
|
5046
|
+
let requests = groupedRequests;
|
|
4849
5047
|
const agent = this.agents.get(agentName);
|
|
4850
5048
|
if (!agent) {
|
|
4851
5049
|
// Agent not found — request is orphaned. Emit warning and drop.
|
|
@@ -4861,6 +5059,28 @@ export class AgentFramework {
|
|
|
4861
5059
|
continue;
|
|
4862
5060
|
}
|
|
4863
5061
|
|
|
5062
|
+
const providerCooldown = this.providerAccelerationCooldowns?.get(agentName);
|
|
5063
|
+
if (providerCooldown) {
|
|
5064
|
+
if (now < providerCooldown.until) {
|
|
5065
|
+
for (const req of requests) {
|
|
5066
|
+
if (!providerCooldown.heldRequests.some((r) => this.sameInferenceRequest(r, req))) {
|
|
5067
|
+
providerCooldown.heldRequests.push(req);
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
5071
|
+
if (recovery) recovery.heldRequests = providerCooldown.heldRequests.length;
|
|
5072
|
+
// The timer owns release. Do not hot-poll these requests.
|
|
5073
|
+
continue;
|
|
5074
|
+
}
|
|
5075
|
+
// Timers can deliver late under load. Release synchronously and merge
|
|
5076
|
+
// the held causes into this one current compile.
|
|
5077
|
+
clearTimeout(providerCooldown.timer);
|
|
5078
|
+
this.providerAccelerationCooldowns.delete(agentName);
|
|
5079
|
+
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
5080
|
+
if (recovery) recovery.releasedAt = now;
|
|
5081
|
+
requests = [...providerCooldown.heldRequests, ...requests];
|
|
5082
|
+
}
|
|
5083
|
+
|
|
4864
5084
|
// Skip if agent is busy (inferring, streaming, or waiting for tools) —
|
|
4865
5085
|
// or if a turn is alive at all (activeTurnTokens): the state machine
|
|
4866
5086
|
// goes idle at endTurn's reset while driveStream teardown is still
|
|
@@ -4876,9 +5096,14 @@ export class AgentFramework {
|
|
|
4876
5096
|
// token; the predecessor frame's late finally no-ops via token-match.
|
|
4877
5097
|
// This mirrors how the restart has always overwritten activeStreams
|
|
4878
5098
|
// rather than waiting for the old stream's teardown.
|
|
4879
|
-
const
|
|
4880
|
-
const turnAlive = !
|
|
4881
|
-
|
|
5099
|
+
const budgetRestart = requests.find((r) => r.reason === 'context_budget_restart');
|
|
5100
|
+
const turnAlive = !budgetRestart && this.activeTurnTokens.has(agentName);
|
|
5101
|
+
const providerGate = this.providerGates?.get(agentName);
|
|
5102
|
+
// A primary can own provider admission while yielding to an already in-flight
|
|
5103
|
+
// auxiliary call. Keep this agent's later wakes queued, but do not block the
|
|
5104
|
+
// framework event loop or other residents while that auxiliary call settles.
|
|
5105
|
+
const providerPrimaryWaiting = (providerGate?.primaryDepth ?? 0) > 0 && !this.activeTurnTokens.has(agentName);
|
|
5106
|
+
if (providerPrimaryWaiting || turnAlive || agent.state.status === 'inferring' || agent.state.status === 'streaming' || agent.state.status === 'waiting_for_tools') {
|
|
4882
5107
|
// Re-queue requests, but warn if they've been pending too long
|
|
4883
5108
|
const oldest = Math.min(...requests.map(r => r.timestamp));
|
|
4884
5109
|
if (
|
|
@@ -4922,11 +5147,22 @@ export class AgentFramework {
|
|
|
4922
5147
|
`[inference-dropped] agent=${agentName} reason=policy-skip ` +
|
|
4923
5148
|
`requests=${requests.length} triggers=${requests.map((r) => r.reason).join(',')}`,
|
|
4924
5149
|
);
|
|
5150
|
+
const gate = this.providerGates.get(agentName);
|
|
5151
|
+
if (gate?.primaryPending && this.providerAccelerationRecoveries.has(agentName)) {
|
|
5152
|
+
gate.primaryPending = false;
|
|
5153
|
+
this.providerAccelerationRecoveries.delete(agentName);
|
|
5154
|
+
this.flushAuxiliaryAdmission(agentName);
|
|
5155
|
+
}
|
|
4925
5156
|
continue;
|
|
4926
5157
|
}
|
|
4927
5158
|
|
|
4928
5159
|
// Start streaming inference (non-blocking — driveStream runs in background)
|
|
4929
|
-
|
|
5160
|
+
// The request that grants the turn-alive exception must also define the
|
|
5161
|
+
// downstream turn semantics. An ordinary wake can already be pending
|
|
5162
|
+
// when the tool boundary appends a context-budget restart; selecting
|
|
5163
|
+
// requests[0] in that mixed batch bypassed the turn lock because a
|
|
5164
|
+
// restart existed, then treated the continuation as a fresh turn.
|
|
5165
|
+
const trigger = budgetRestart ?? requests[0];
|
|
4930
5166
|
// Route this turn's auto-published speech to the channel that triggered
|
|
4931
5167
|
// it (item-3 redux). A batched wake may carry several triggering channels
|
|
4932
5168
|
// (messages arrived in >1 channel while the agent was busy/idle):
|
|
@@ -5008,7 +5244,9 @@ export class AgentFramework {
|
|
|
5008
5244
|
}
|
|
5009
5245
|
const suppressedNote =
|
|
5010
5246
|
suppressed > 0
|
|
5011
|
-
?
|
|
5247
|
+
? agent.proseRouting === 'disabled'
|
|
5248
|
+
? `${suppressed} plain-speech segment(s) suppressed (proseRouting=disabled — publish only with an explicit send tool)`
|
|
5249
|
+
: `${suppressed} plain-speech segment(s) suppressed (explicit send in the same round — resend with a send tool if it was meant to be heard)`
|
|
5012
5250
|
: '';
|
|
5013
5251
|
const text =
|
|
5014
5252
|
shown.length > 0
|
|
@@ -5378,6 +5616,13 @@ export class AgentFramework {
|
|
|
5378
5616
|
console.error('maybePrimeProseMode: state read/write failed:', err);
|
|
5379
5617
|
return;
|
|
5380
5618
|
}
|
|
5619
|
+
// Disabled mode is intentionally silent: no model-visible routing prose.
|
|
5620
|
+
// The resident publishes only through explicit tools. This contains
|
|
5621
|
+
// continuity-summary locus leaks (Host #96).
|
|
5622
|
+
if (mode === 'disabled') {
|
|
5623
|
+
console.error(`[prose] ${agent.name}: disabled mode recorded (no primer injected)`);
|
|
5624
|
+
return;
|
|
5625
|
+
}
|
|
5381
5626
|
try {
|
|
5382
5627
|
const id = agent.getContextManager().addMessage(
|
|
5383
5628
|
'user',
|
|
@@ -5391,7 +5636,38 @@ export class AgentFramework {
|
|
|
5391
5636
|
}
|
|
5392
5637
|
}
|
|
5393
5638
|
|
|
5394
|
-
private async startAgentStream(
|
|
5639
|
+
private async startAgentStream(
|
|
5640
|
+
agent: Agent,
|
|
5641
|
+
trigger?: InferenceRequest,
|
|
5642
|
+
attempt = 0,
|
|
5643
|
+
providerGateAlreadyHeld = false,
|
|
5644
|
+
): Promise<void> {
|
|
5645
|
+
const ownsProviderGate =
|
|
5646
|
+
!this.ephemeralRuns.has(agent.name) && !this.conversationAgentHomes.has(agent.name);
|
|
5647
|
+
if (ownsProviderGate && !providerGateAlreadyHeld) {
|
|
5648
|
+
this.acquirePrimaryProviderGate(agent.name);
|
|
5649
|
+
const gate = this.providerGate(agent.name);
|
|
5650
|
+
if (gate.auxiliaryInFlight > 0) {
|
|
5651
|
+
// Do not await inside processInferenceRequests: one slow auxiliary call for
|
|
5652
|
+
// this resident must not block events or other residents. Admission stays
|
|
5653
|
+
// owned and the continuation receives it after the in-flight call settles.
|
|
5654
|
+
void this.waitForAuxiliaryIdle(agent.name).then(async () => {
|
|
5655
|
+
if (this.providerAdmissionClosed) {
|
|
5656
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
5657
|
+
return;
|
|
5658
|
+
}
|
|
5659
|
+
await this.startAgentStream(agent, trigger, attempt, true);
|
|
5660
|
+
}).catch((error) => {
|
|
5661
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
5662
|
+
console.error(`[provider-cooldown] failed to resume primary for ${agent.name}:`, error);
|
|
5663
|
+
});
|
|
5664
|
+
return;
|
|
5665
|
+
}
|
|
5666
|
+
if (this.providerAdmissionClosed) {
|
|
5667
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
5668
|
+
return;
|
|
5669
|
+
}
|
|
5670
|
+
}
|
|
5395
5671
|
// Mark the turn alive before ANYTHING awaits (hooks, compile, stream
|
|
5396
5672
|
// setup): from here until this turn's teardown, cross-turn writers defer
|
|
5397
5673
|
// (addMessage guard) instead of appending — nothing may enter the window
|
|
@@ -5412,11 +5688,12 @@ export class AgentFramework {
|
|
|
5412
5688
|
this.activeTurnTokens.set(agent.name, turnToken);
|
|
5413
5689
|
let tokenHandedOff = false;
|
|
5414
5690
|
try {
|
|
5415
|
-
tokenHandedOff = await this.beginAgentTurn(agent, trigger, attempt, turnToken);
|
|
5691
|
+
tokenHandedOff = await this.beginAgentTurn(agent, trigger, attempt, turnToken, ownsProviderGate);
|
|
5416
5692
|
} finally {
|
|
5417
5693
|
if (!tokenHandedOff && this.activeTurnTokens.get(agent.name) === turnToken) {
|
|
5418
5694
|
this.activeTurnTokens.delete(agent.name);
|
|
5419
5695
|
}
|
|
5696
|
+
if (!tokenHandedOff && ownsProviderGate) this.releasePrimaryProviderGate(agent.name);
|
|
5420
5697
|
}
|
|
5421
5698
|
}
|
|
5422
5699
|
|
|
@@ -5430,6 +5707,7 @@ export class AgentFramework {
|
|
|
5430
5707
|
trigger: InferenceRequest | undefined,
|
|
5431
5708
|
attempt: number,
|
|
5432
5709
|
turnToken: number,
|
|
5710
|
+
ownsProviderGate: boolean,
|
|
5433
5711
|
): Promise<boolean> {
|
|
5434
5712
|
// Flush messages deferred during the PREVIOUS turn — before the
|
|
5435
5713
|
// checkpoint, the locus announcement, and the compile — so a turn started
|
|
@@ -5520,10 +5798,10 @@ export class AgentFramework {
|
|
|
5520
5798
|
this.turnProseSuppressed.delete(agent.name);
|
|
5521
5799
|
this.proseHybridSuppressed.delete(agent.name);
|
|
5522
5800
|
if (agent.proseRouting === 'hybrid') this.proseTargetPins.delete(agent.name);
|
|
5523
|
-
if (agent.proseRouting === 'explicit') {
|
|
5524
|
-
// Explicit prose routing
|
|
5525
|
-
//
|
|
5526
|
-
//
|
|
5801
|
+
if (agent.proseRouting === 'explicit' || agent.proseRouting === 'disabled') {
|
|
5802
|
+
// Explicit and disabled prose routing have no inferred locus.
|
|
5803
|
+
// Explicit mode uses a turn-scoped `>>` target; disabled mode has no
|
|
5804
|
+
// prose target at all. Neither mode freezes or announces a locus.
|
|
5527
5805
|
this.proseTargetPins.delete(agent.name);
|
|
5528
5806
|
this.proseContinuations.delete(agent.name);
|
|
5529
5807
|
this.midTurnInputSignals.delete(agent.name);
|
|
@@ -5617,6 +5895,7 @@ export class AgentFramework {
|
|
|
5617
5895
|
trigger,
|
|
5618
5896
|
attempt,
|
|
5619
5897
|
compiledRequest,
|
|
5898
|
+
ownsProviderGate,
|
|
5620
5899
|
);
|
|
5621
5900
|
this.activeStreams.set(agent.name, handle);
|
|
5622
5901
|
// Handoff: driveStream captured the token in its synchronous prefix;
|
|
@@ -5636,6 +5915,13 @@ export class AgentFramework {
|
|
|
5636
5915
|
});
|
|
5637
5916
|
agent.reset();
|
|
5638
5917
|
|
|
5918
|
+
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
5919
|
+
// Capacity scheduling is not poisoned history. The cooldown timer owns
|
|
5920
|
+
// one later fresh compile; no immediate retry or exhaustion marker.
|
|
5921
|
+
this.eventGate?.onInferenceEnded(agent.name);
|
|
5922
|
+
return false;
|
|
5923
|
+
}
|
|
5924
|
+
|
|
5639
5925
|
const action = this.errorPolicy.onInferenceError(err, agent.name, attempt);
|
|
5640
5926
|
if (action.retry) {
|
|
5641
5927
|
await new Promise((resolve) => setTimeout(resolve, action.delayMs));
|
|
@@ -5689,7 +5975,8 @@ export class AgentFramework {
|
|
|
5689
5975
|
requestSnapshot: InferenceToolSnapshot,
|
|
5690
5976
|
trigger?: InferenceRequest,
|
|
5691
5977
|
attempt = 0,
|
|
5692
|
-
compiledRequest?: NormalizedRequest
|
|
5978
|
+
compiledRequest?: NormalizedRequest,
|
|
5979
|
+
ownsProviderGate = false,
|
|
5693
5980
|
): Promise<void> {
|
|
5694
5981
|
const startTime = Date.now();
|
|
5695
5982
|
const requestId = `${agent.name}-${startTime}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -5754,9 +6041,11 @@ export class AgentFramework {
|
|
|
5754
6041
|
// sent here", which is true regardless of where the reply goes.
|
|
5755
6042
|
// Heartbeat/no-trigger explicit turns show no indicator.
|
|
5756
6043
|
const typingChannel =
|
|
5757
|
-
agent.proseRouting === '
|
|
5758
|
-
?
|
|
5759
|
-
:
|
|
6044
|
+
agent.proseRouting === 'disabled'
|
|
6045
|
+
? null
|
|
6046
|
+
: agent.proseRouting === 'explicit'
|
|
6047
|
+
? trigger?.channelId ?? null
|
|
6048
|
+
: resolveTurnLocus();
|
|
5760
6049
|
if (typingChannel) this.channelRegistry!.startTyping(typingChannel);
|
|
5761
6050
|
|
|
5762
6051
|
// MCPL Spec 14.3 outgoing streaming: route text deltas to their
|
|
@@ -5786,7 +6075,7 @@ export class AgentFramework {
|
|
|
5786
6075
|
turnIndex: 0,
|
|
5787
6076
|
phase: 'started',
|
|
5788
6077
|
});
|
|
5789
|
-
const proseStream = this.channelRegistry
|
|
6078
|
+
const proseStream = this.channelRegistry && agent.proseRouting !== 'disabled'
|
|
5790
6079
|
? new ProseStreamRouter({
|
|
5791
6080
|
mode: agent.proseRouting === 'explicit' ? 'explicit' : agent.proseRouting === 'hybrid' ? 'hybrid' : 'locus',
|
|
5792
6081
|
initialTarget: typingChannel,
|
|
@@ -5978,7 +6267,12 @@ export class AgentFramework {
|
|
|
5978
6267
|
liveProseRouting = true;
|
|
5979
6268
|
const roundSegments = splitProseSegments(assistantBlocks);
|
|
5980
6269
|
if (roundSegments.length > 0) {
|
|
5981
|
-
if (agent.proseRouting === '
|
|
6270
|
+
if (agent.proseRouting === 'disabled') {
|
|
6271
|
+
console.error(
|
|
6272
|
+
`[routing] ${agent.name}: mid-turn prose NOT routed (proseRouting=disabled)`,
|
|
6273
|
+
);
|
|
6274
|
+
this.recordProseSuppression(agent.name, roundSegments.length);
|
|
6275
|
+
} else if (agent.proseRouting === 'hybrid') {
|
|
5982
6276
|
if (turnSilenced) {
|
|
5983
6277
|
this.recordProseSuppression(agent.name, roundSegments.length);
|
|
5984
6278
|
} else if (!hasSameRoundPrivateThink) {
|
|
@@ -6094,6 +6388,10 @@ export class AgentFramework {
|
|
|
6094
6388
|
agent.addAssistantResponse(terminalContent);
|
|
6095
6389
|
}
|
|
6096
6390
|
|
|
6391
|
+
// Bind the cooldown receipt to this fresh, successful request —
|
|
6392
|
+
// never to the frozen request that received the 429.
|
|
6393
|
+
this.recordProviderAccelerationRecovery(agent, compiledRequest, response.stopReason);
|
|
6394
|
+
|
|
6097
6395
|
// §10.5: context/afterInference is removed in 0.5.0 — no
|
|
6098
6396
|
// content leaves the host here. Turn boundaries are announced
|
|
6099
6397
|
// by the metadata-only inference/lifecycle notifications
|
|
@@ -6319,7 +6617,10 @@ export class AgentFramework {
|
|
|
6319
6617
|
.join('\n')
|
|
6320
6618
|
.trim();
|
|
6321
6619
|
if (speechText) {
|
|
6322
|
-
if (agent.proseRouting === '
|
|
6620
|
+
if (agent.proseRouting === 'disabled') {
|
|
6621
|
+
console.error(`[routing] ${agent.name}: text-only prose NOT routed (proseRouting=disabled)`);
|
|
6622
|
+
this.recordProseSuppression(agent.name, 1);
|
|
6623
|
+
} else if (agent.proseRouting === 'hybrid') {
|
|
6323
6624
|
const locus = resolveTurnLocus();
|
|
6324
6625
|
console.error(`[prose] ${agent.name}: text-only turn -> hybrid prose gateway`);
|
|
6325
6626
|
try {
|
|
@@ -6386,7 +6687,14 @@ export class AgentFramework {
|
|
|
6386
6687
|
// the chain may still be flushing earlier rounds' posts.
|
|
6387
6688
|
await turnSpeechChain;
|
|
6388
6689
|
|
|
6389
|
-
if (agent.proseRouting === '
|
|
6690
|
+
if (agent.proseRouting === 'disabled') {
|
|
6691
|
+
if (segments.length > 0) {
|
|
6692
|
+
console.error(
|
|
6693
|
+
`[routing] ${agent.name}: tool-call trailing prose NOT routed (proseRouting=disabled)`,
|
|
6694
|
+
);
|
|
6695
|
+
this.recordProseSuppression(agent.name, segments.length);
|
|
6696
|
+
}
|
|
6697
|
+
} else if (agent.proseRouting === 'hybrid') {
|
|
6390
6698
|
if (silenced && segments.length > 0) {
|
|
6391
6699
|
this.recordProseSuppression(agent.name, segments.length);
|
|
6392
6700
|
} else if (segments.length > 0) {
|
|
@@ -6500,6 +6808,12 @@ export class AgentFramework {
|
|
|
6500
6808
|
this.abortAgentScript(agent.name, 'stream error');
|
|
6501
6809
|
agent.reset();
|
|
6502
6810
|
|
|
6811
|
+
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
6812
|
+
lifecyclePhase = 'failed';
|
|
6813
|
+
this.eventGate?.onInferenceEnded(agent.name);
|
|
6814
|
+
break;
|
|
6815
|
+
}
|
|
6816
|
+
|
|
6503
6817
|
const action = this.errorPolicy.onInferenceError(err, agent.name, attempt);
|
|
6504
6818
|
if (action.retry) {
|
|
6505
6819
|
await new Promise((resolve) => setTimeout(resolve, action.delayMs));
|
|
@@ -6639,10 +6953,19 @@ export class AgentFramework {
|
|
|
6639
6953
|
}
|
|
6640
6954
|
}
|
|
6641
6955
|
} catch (error) {
|
|
6642
|
-
// Stream itself threw
|
|
6643
|
-
//
|
|
6956
|
+
// Stream itself threw. Organization acceleration is deferred from a
|
|
6957
|
+
// fresh compile; every other throw keeps the ordinary exhausted path.
|
|
6644
6958
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
6645
6959
|
const durationMs = Date.now() - startTime;
|
|
6960
|
+
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
6961
|
+
this.emitTrace({ type: 'inference:failed', agentName: agent.name, error: err.message, stack: err.stack });
|
|
6962
|
+
this.logInference({ timestamp: startTime, agentName: agent.name, requestId, success: false,
|
|
6963
|
+
error: `Provider acceleration cooldown: ${err.message}`,
|
|
6964
|
+
request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
|
|
6965
|
+
this.abortAgentScript(agent.name, 'provider acceleration cooldown');
|
|
6966
|
+
lifecyclePhase = 'failed'; agent.reset(); this.eventGate?.onInferenceEnded(agent.name);
|
|
6967
|
+
return;
|
|
6968
|
+
}
|
|
6646
6969
|
this.emitTrace({
|
|
6647
6970
|
type: 'inference:failed',
|
|
6648
6971
|
agentName: agent.name,
|
|
@@ -6697,6 +7020,7 @@ export class AgentFramework {
|
|
|
6697
7020
|
// wedge). onInferenceEnded is idempotent, so a redundant call is safe.
|
|
6698
7021
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
6699
7022
|
this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
|
|
7023
|
+
if (ownsProviderGate) this.releasePrimaryProviderGate(agent.name);
|
|
6700
7024
|
|
|
6701
7025
|
// Stop the typing indicator on every exit path (complete, error,
|
|
6702
7026
|
// exhausted, abort) so it never sticks after the turn ends.
|
|
@@ -6762,6 +7086,107 @@ export class AgentFramework {
|
|
|
6762
7086
|
});
|
|
6763
7087
|
}
|
|
6764
7088
|
|
|
7089
|
+
/**
|
|
7090
|
+
* Admin puppet: execute ONE tool call AS an agent and persist the
|
|
7091
|
+
* tool_use + tool_result pair in that agent's window exactly as a
|
|
7092
|
+
* model-initiated call is stored.
|
|
7093
|
+
*
|
|
7094
|
+
* Born from the princess exemplar surgery (2026-08-23). Her folded
|
|
7095
|
+
* narrative said "I have no tools"; every past tool_use/tool_result pair
|
|
7096
|
+
* had folded out of her context, so the act of calling had no in-context
|
|
7097
|
+
* exemplar and she could not emit her first call despite 30 eidoverse
|
|
7098
|
+
* tools riding every request. Placing one first-person pair restored the
|
|
7099
|
+
* capacity. That repair required stop → store surgery → restart; this
|
|
7100
|
+
* method is the live path — and the general lever for demonstrating an
|
|
7101
|
+
* affordance to a model that cannot find it (older models especially).
|
|
7102
|
+
*
|
|
7103
|
+
* Semantics:
|
|
7104
|
+
* - Refused unless the agent is idle: puppeting mid-turn would corrupt
|
|
7105
|
+
* the turn state machine and the live stream's wire ordering.
|
|
7106
|
+
* - Refused for tools outside the agent's own surface (canUseTool): the
|
|
7107
|
+
* stored pair must be an act the agent could genuinely have taken.
|
|
7108
|
+
* - The call executes FOR REAL through the shared dispatch (MCPL,
|
|
7109
|
+
* channel tools, utils, modules) with the agent's provenance — a
|
|
7110
|
+
* puppeted walk_to actually moves the avatar.
|
|
7111
|
+
* - Storage byte-follows the ordinary path: a bare tool_use assistant
|
|
7112
|
+
* message (no fabricated thinking or text — a stored assistant turn
|
|
7113
|
+
* with unverifiable thinking is the labclaude 400 class) plus a
|
|
7114
|
+
* tool_result under the same bounded spill policy (issue #89).
|
|
7115
|
+
* - Does NOT request inference; the agent sees the pair on its next
|
|
7116
|
+
* wake. Speak to the agent afterward if a wake is wanted.
|
|
7117
|
+
* - Provenance is loud in traces (puppet:tool-call) and the host log,
|
|
7118
|
+
* deliberately NOT in the stored messages — metadata there would break
|
|
7119
|
+
* byte-parity with real turns. Whether to disclose to the resident is
|
|
7120
|
+
* the operator's call; the princess precedent was disclosed first.
|
|
7121
|
+
*/
|
|
7122
|
+
async puppetToolCall(
|
|
7123
|
+
agentName: string,
|
|
7124
|
+
toolName: string,
|
|
7125
|
+
input: Record<string, unknown>,
|
|
7126
|
+
): Promise<{ toolUseId: string; result: ToolResult }> {
|
|
7127
|
+
const agent = this.agents.get(agentName);
|
|
7128
|
+
if (!agent) throw new Error(`Unknown agent: ${agentName}`);
|
|
7129
|
+
if (agent.state.status !== 'idle') {
|
|
7130
|
+
throw new Error(
|
|
7131
|
+
`puppet refused: agent ${agentName} is ${agent.state.status} (requires idle — ` +
|
|
7132
|
+
`injecting a turn under an active stream corrupts wire ordering)`,
|
|
7133
|
+
);
|
|
7134
|
+
}
|
|
7135
|
+
const onSurface = this.getToolsForAgent(agentName)
|
|
7136
|
+
.some((t) => t.name === toolName && agent.canUseTool(t.name));
|
|
7137
|
+
if (!onSurface) {
|
|
7138
|
+
throw new Error(
|
|
7139
|
+
`puppet refused: tool ${toolName} is not on ${agentName}'s surface — ` +
|
|
7140
|
+
`the stored pair must be an act the agent could genuinely have taken`,
|
|
7141
|
+
);
|
|
7142
|
+
}
|
|
7143
|
+
|
|
7144
|
+
// Anthropic-shaped id so the stored pair is indistinguishable from a
|
|
7145
|
+
// provider-issued call.
|
|
7146
|
+
const alphabet = 'ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz0123456789';
|
|
7147
|
+
let suffix = '';
|
|
7148
|
+
for (let i = 0; i < 22; i++) suffix += alphabet[Math.floor(Math.random() * alphabet.length)];
|
|
7149
|
+
const toolUseId = `toolu_01${suffix}`;
|
|
7150
|
+
|
|
7151
|
+
const started = Date.now();
|
|
7152
|
+
const result = await this.executeToolCall({
|
|
7153
|
+
id: toolUseId,
|
|
7154
|
+
name: toolName,
|
|
7155
|
+
input,
|
|
7156
|
+
callerAgentName: agentName,
|
|
7157
|
+
});
|
|
7158
|
+
const durationMs = Date.now() - started;
|
|
7159
|
+
|
|
7160
|
+
// Store the pair through the same shapes the ordinary path uses. Build
|
|
7161
|
+
// the result blocks BEFORE storing the tool_use: the spill path awaits,
|
|
7162
|
+
// and a message arriving during that await must land before the pair,
|
|
7163
|
+
// never between tool_use and its tool_result. The two addMessage calls
|
|
7164
|
+
// below are synchronous and adjacent — nothing can interleave.
|
|
7165
|
+
const { blocks } = await this.buildStoredToolResultContent(
|
|
7166
|
+
[{ id: toolUseId, name: toolName, input, result, durationMs }],
|
|
7167
|
+
this.resolveToolResultInlineCap(agent).cap,
|
|
7168
|
+
);
|
|
7169
|
+
const cm = agent.getContextManager();
|
|
7170
|
+
cm.addMessage(agentName, [
|
|
7171
|
+
{ type: 'tool_use', id: toolUseId, name: toolName, input } as ContentBlock,
|
|
7172
|
+
]);
|
|
7173
|
+
cm.addMessage('user', blocks);
|
|
7174
|
+
|
|
7175
|
+
this.emitTrace({
|
|
7176
|
+
type: 'puppet:tool-call',
|
|
7177
|
+
agentName,
|
|
7178
|
+
toolName,
|
|
7179
|
+
toolUseId,
|
|
7180
|
+
isError: !!result.isError,
|
|
7181
|
+
durationMs,
|
|
7182
|
+
});
|
|
7183
|
+
console.log(
|
|
7184
|
+
`[puppet] ${agentName}: ${toolName} → ${result.isError ? 'ERROR' : 'ok'} ` +
|
|
7185
|
+
`in ${durationMs}ms (${toolUseId})`,
|
|
7186
|
+
);
|
|
7187
|
+
return { toolUseId, result };
|
|
7188
|
+
}
|
|
7189
|
+
|
|
6765
7190
|
private async executeToolCallFrom(call: ToolCall, origin: ChannelToolOrigin): Promise<ToolResult> {
|
|
6766
7191
|
// Client-side programmatic tool calling for promise-based callers
|
|
6767
7192
|
// (SubagentModule ephemerals). Keyed by callerAgentName so each ephemeral
|
|
@@ -8025,6 +8450,19 @@ export class AgentFramework {
|
|
|
8025
8450
|
name,
|
|
8026
8451
|
status: agent.state.status,
|
|
8027
8452
|
consecutiveInferenceFailures: this.consecutiveInferenceFailures.get(name) ?? 0,
|
|
8453
|
+
providerAdmission: (() => {
|
|
8454
|
+
const gate = this.providerGates?.get(name);
|
|
8455
|
+
const cooldown = this.providerAccelerationCooldowns?.get(name);
|
|
8456
|
+
return {
|
|
8457
|
+
primaryActive: (gate?.primaryDepth ?? 0) > 0,
|
|
8458
|
+
primaryPending: gate?.primaryPending ?? false,
|
|
8459
|
+
auxiliaryInFlight: gate?.auxiliaryInFlight ?? 0,
|
|
8460
|
+
auxiliaryDeferred: gate?.deferredAuxiliary ?? 0,
|
|
8461
|
+
cooldownUntil: cooldown?.until ?? null,
|
|
8462
|
+
heldRequests: cooldown?.heldRequests.length ?? 0,
|
|
8463
|
+
lastRecovery: this.providerAccelerationLastRecovery?.get(name) ?? null,
|
|
8464
|
+
};
|
|
8465
|
+
})(),
|
|
8028
8466
|
lastInference: this.lastInferenceAt.get(name) ?? null,
|
|
8029
8467
|
refusalStats: this.refusalStats.get(name) ?? null,
|
|
8030
8468
|
})),
|
|
@@ -10075,7 +10513,7 @@ export class AgentFramework {
|
|
|
10075
10513
|
if (opened) {
|
|
10076
10514
|
this.activeTriggerChannels.set(agentName, opened);
|
|
10077
10515
|
const openerAgent = this.agents.get(agentName);
|
|
10078
|
-
if (openerAgent && openerAgent.proseRouting
|
|
10516
|
+
if (openerAgent && (openerAgent.proseRouting === 'locus' || openerAgent.proseRouting === 'hybrid')) {
|
|
10079
10517
|
this.turnLocusPins.set(agentName, opened);
|
|
10080
10518
|
this.lastAnnouncedLocus.set(agentName, opened);
|
|
10081
10519
|
result = {
|
|
@@ -10293,7 +10731,9 @@ export class AgentFramework {
|
|
|
10293
10731
|
if (announce && this.channelRegistry) {
|
|
10294
10732
|
const text = input.message ?? `💤 Going quiet for ${human}. I'll still see messages, but won't respond until I wake.`;
|
|
10295
10733
|
const agent = this.agents.get(agentName);
|
|
10296
|
-
if (agent?.proseRouting === '
|
|
10734
|
+
if (agent?.proseRouting === 'disabled') {
|
|
10735
|
+
console.error(`[sleep] ${agentName}: proseRouting=disabled — sleep announcement not posted`);
|
|
10736
|
+
} else if (agent?.proseRouting === 'explicit') {
|
|
10297
10737
|
// Explicit mode: announce to the turn's sticky prose target if the
|
|
10298
10738
|
// model has set one; otherwise stay quiet — never guess a channel.
|
|
10299
10739
|
const target = this.proseTargetPins.get(agentName);
|