@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/dist/src/framework.js
CHANGED
|
@@ -331,6 +331,15 @@ const DEFAULT_SYNC_INTERVAL_MS = 1000;
|
|
|
331
331
|
const DEFAULT_MAINTENANCE_INTERVAL_MS = 5000;
|
|
332
332
|
/** Bound one pass so a large backlog yields to inference and other agents. */
|
|
333
333
|
const MAINTENANCE_TICKS_PER_PASS = 8;
|
|
334
|
+
const PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS = 65_000;
|
|
335
|
+
const PROVIDER_ACCELERATION_MAX_COOLDOWN_MS = 10 * 60_000;
|
|
336
|
+
const PROVIDER_ACCELERATION_JITTER_MS = 5_000;
|
|
337
|
+
function isOrganizationAccelerationRateLimit(error) {
|
|
338
|
+
if (!(error instanceof MembraneError) || error.type !== 'rate_limit')
|
|
339
|
+
return false;
|
|
340
|
+
return (/organization(?:'s)?[^\n]{0,120}maximum(?:\s+usage)?\s+increase\s+rate/i.test(error.message) ||
|
|
341
|
+
/organization(?:'s)?[^\n]{0,120}acceleration(?:\s+limit)?/i.test(error.message));
|
|
342
|
+
}
|
|
334
343
|
/**
|
|
335
344
|
* Extract fields the EventGate cares about from a ProcessEvent. The set of
|
|
336
345
|
* event variants that carry `content`/`mount`/`paths`/`metadata` is open-ended
|
|
@@ -399,6 +408,13 @@ export class AgentFramework {
|
|
|
399
408
|
maintenanceRunId = 0;
|
|
400
409
|
currentMaintenanceRun = null;
|
|
401
410
|
maintenanceHistory = [];
|
|
411
|
+
providerGates = new Map();
|
|
412
|
+
providerAccelerationCooldowns = new Map();
|
|
413
|
+
providerAccelerationRecoveries = new Map();
|
|
414
|
+
providerAccelerationLastRecovery = new Map();
|
|
415
|
+
providerAccelerationDefaultCooldownMs = PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS;
|
|
416
|
+
providerAccelerationJitterMs = PROVIDER_ACCELERATION_JITTER_MS;
|
|
417
|
+
providerAdmissionClosed = false;
|
|
402
418
|
/** Last time we reported stale (busy-requeued) inference requests, per agent. */
|
|
403
419
|
staleWarnAt = new Map();
|
|
404
420
|
/** Per-agent last inference activity (epoch ms), for /healthz + doctor tooling. */
|
|
@@ -897,6 +913,7 @@ export class AgentFramework {
|
|
|
897
913
|
return;
|
|
898
914
|
}
|
|
899
915
|
this.running = true;
|
|
916
|
+
this.providerAdmissionClosed = false;
|
|
900
917
|
this.loopPromise = this.runLoop();
|
|
901
918
|
// Start periodic sync timer (if enabled)
|
|
902
919
|
if (this.syncIntervalMs > 0) {
|
|
@@ -924,6 +941,7 @@ export class AgentFramework {
|
|
|
924
941
|
*/
|
|
925
942
|
async stop() {
|
|
926
943
|
this.running = false;
|
|
944
|
+
this.providerAdmissionClosed = true;
|
|
927
945
|
this.queue.close();
|
|
928
946
|
// Kill running code_execution scripts before cancelling streams: a
|
|
929
947
|
// zombie script must not keep firing side-effectful tool calls into a
|
|
@@ -943,6 +961,8 @@ export class AgentFramework {
|
|
|
943
961
|
record.runner.dispose();
|
|
944
962
|
}
|
|
945
963
|
this.backgroundScripts.clear();
|
|
964
|
+
// A stopped host must never hang behind its own cooldown.
|
|
965
|
+
this.cancelProviderAdmission();
|
|
946
966
|
// Cancel all active streams
|
|
947
967
|
for (const agent of this.agents.values()) {
|
|
948
968
|
if (agent.state.status === 'streaming' ||
|
|
@@ -999,6 +1019,168 @@ export class AgentFramework {
|
|
|
999
1019
|
* schemas are absent. Passes never overlap; each agent gets a bounded drain
|
|
1000
1020
|
* so maintenance cannot monopolize the framework event loop.
|
|
1001
1021
|
*/
|
|
1022
|
+
providerGate(agentName) {
|
|
1023
|
+
let gate = this.providerGates.get(agentName);
|
|
1024
|
+
if (!gate) {
|
|
1025
|
+
gate = { primaryDepth: 0, primaryPending: false, auxiliaryInFlight: 0,
|
|
1026
|
+
auxiliaryWaiters: [], idleWaiters: [], deferredAuxiliary: 0 };
|
|
1027
|
+
this.providerGates.set(agentName, gate);
|
|
1028
|
+
}
|
|
1029
|
+
return gate;
|
|
1030
|
+
}
|
|
1031
|
+
providerGateBlocked(agentName) {
|
|
1032
|
+
const gate = this.providerGates.get(agentName);
|
|
1033
|
+
return (gate?.primaryDepth ?? 0) > 0 || (gate?.primaryPending ?? false) ||
|
|
1034
|
+
this.providerAccelerationCooldowns.has(agentName);
|
|
1035
|
+
}
|
|
1036
|
+
acquirePrimaryProviderGate(agentName) {
|
|
1037
|
+
const gate = this.providerGate(agentName);
|
|
1038
|
+
gate.primaryPending = false;
|
|
1039
|
+
gate.primaryDepth++;
|
|
1040
|
+
}
|
|
1041
|
+
releasePrimaryProviderGate(agentName) {
|
|
1042
|
+
const gate = this.providerGate(agentName);
|
|
1043
|
+
gate.primaryDepth = Math.max(0, gate.primaryDepth - 1);
|
|
1044
|
+
this.flushAuxiliaryAdmission(agentName);
|
|
1045
|
+
}
|
|
1046
|
+
flushAuxiliaryAdmission(agentName) {
|
|
1047
|
+
const gate = this.providerGate(agentName);
|
|
1048
|
+
if (gate.primaryDepth > 0 || gate.primaryPending || this.providerAccelerationCooldowns.has(agentName))
|
|
1049
|
+
return;
|
|
1050
|
+
for (const resolve of gate.auxiliaryWaiters.splice(0))
|
|
1051
|
+
resolve();
|
|
1052
|
+
}
|
|
1053
|
+
async waitForAuxiliaryIdle(agentName) {
|
|
1054
|
+
const gate = this.providerGate(agentName);
|
|
1055
|
+
if (gate.auxiliaryInFlight === 0)
|
|
1056
|
+
return;
|
|
1057
|
+
await new Promise((resolve) => gate.idleWaiters.push(resolve));
|
|
1058
|
+
}
|
|
1059
|
+
async withAuxiliaryAdmission(agentName, run) {
|
|
1060
|
+
const gate = this.providerGate(agentName);
|
|
1061
|
+
if (this.providerGateBlocked(agentName)) {
|
|
1062
|
+
gate.deferredAuxiliary++;
|
|
1063
|
+
try {
|
|
1064
|
+
await new Promise((resolve) => gate.auxiliaryWaiters.push(resolve));
|
|
1065
|
+
}
|
|
1066
|
+
finally {
|
|
1067
|
+
gate.deferredAuxiliary = Math.max(0, gate.deferredAuxiliary - 1);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (this.providerAdmissionClosed) {
|
|
1071
|
+
throw new Error(`Provider admission closed while stopping (${agentName})`);
|
|
1072
|
+
}
|
|
1073
|
+
gate.auxiliaryInFlight++;
|
|
1074
|
+
try {
|
|
1075
|
+
return await run();
|
|
1076
|
+
}
|
|
1077
|
+
finally {
|
|
1078
|
+
gate.auxiliaryInFlight = Math.max(0, gate.auxiliaryInFlight - 1);
|
|
1079
|
+
if (gate.auxiliaryInFlight === 0)
|
|
1080
|
+
for (const resolve of gate.idleWaiters.splice(0))
|
|
1081
|
+
resolve();
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
auxiliaryMembraneFor(agentName) {
|
|
1085
|
+
const target = this.membrane;
|
|
1086
|
+
return new Proxy(target, { get: (obj, prop, receiver) => {
|
|
1087
|
+
const value = Reflect.get(obj, prop, receiver);
|
|
1088
|
+
if (prop === 'complete' && typeof value === 'function') {
|
|
1089
|
+
return (...args) => this.withAuxiliaryAdmission(agentName, () => Reflect.apply(value, this.membrane, args));
|
|
1090
|
+
}
|
|
1091
|
+
return typeof value === 'function' ? value.bind(this.membrane) : value;
|
|
1092
|
+
} });
|
|
1093
|
+
}
|
|
1094
|
+
accelerationCooldownMs(agentName, error) {
|
|
1095
|
+
const base = error.retryAfterMs ?? this.providerAccelerationDefaultCooldownMs;
|
|
1096
|
+
let hash = 0;
|
|
1097
|
+
for (const ch of agentName)
|
|
1098
|
+
hash = ((hash * 31) + ch.charCodeAt(0)) >>> 0;
|
|
1099
|
+
const jitter = this.providerAccelerationJitterMs > 0 ? hash % (this.providerAccelerationJitterMs + 1) : 0;
|
|
1100
|
+
return Math.min(PROVIDER_ACCELERATION_MAX_COOLDOWN_MS, Math.max(1_000, base) + jitter);
|
|
1101
|
+
}
|
|
1102
|
+
sameInferenceRequest(a, b) {
|
|
1103
|
+
return a.agentName === b.agentName && a.reason === b.reason && a.source === b.source &&
|
|
1104
|
+
a.timestamp === b.timestamp && a.channelId === b.channelId;
|
|
1105
|
+
}
|
|
1106
|
+
holdProviderAcceleration(agent, error, trigger) {
|
|
1107
|
+
if (this.ephemeralRuns.has(agent.name) || this.conversationAgentHomes.has(agent.name) || !isOrganizationAccelerationRateLimit(error))
|
|
1108
|
+
return false;
|
|
1109
|
+
const now = Date.now();
|
|
1110
|
+
const delayMs = this.accelerationCooldownMs(agent.name, error);
|
|
1111
|
+
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
1112
|
+
const held = existing?.heldRequests ?? [];
|
|
1113
|
+
if (trigger && !held.some((r) => this.sameInferenceRequest(r, trigger)))
|
|
1114
|
+
held.push(trigger);
|
|
1115
|
+
if (existing)
|
|
1116
|
+
clearTimeout(existing.timer);
|
|
1117
|
+
const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), delayMs);
|
|
1118
|
+
timer.unref?.();
|
|
1119
|
+
this.providerAccelerationCooldowns.set(agent.name, { startedAt: existing?.startedAt ?? now,
|
|
1120
|
+
until: now + delayMs, timer, heldRequests: held, reason: error.message,
|
|
1121
|
+
failures: (existing?.failures ?? 0) + 1 });
|
|
1122
|
+
const gate = this.providerGate(agent.name);
|
|
1123
|
+
gate.primaryPending = true;
|
|
1124
|
+
this.providerAccelerationRecoveries.set(agent.name, { startedAt: existing?.startedAt ?? now,
|
|
1125
|
+
failures: (existing?.failures ?? 0) + 1, heldRequests: held.length, reason: error.message });
|
|
1126
|
+
console.error(`[provider-cooldown] agent=${agent.name} organization acceleration 429 — ` +
|
|
1127
|
+
`holding primary/auxiliary for ${delayMs}ms; ${held.length} request(s) retained`);
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1130
|
+
releaseProviderAccelerationCooldown(agentName) {
|
|
1131
|
+
const cooldown = this.providerAccelerationCooldowns.get(agentName);
|
|
1132
|
+
if (!cooldown)
|
|
1133
|
+
return;
|
|
1134
|
+
clearTimeout(cooldown.timer);
|
|
1135
|
+
this.providerAccelerationCooldowns.delete(agentName);
|
|
1136
|
+
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
1137
|
+
if (recovery) {
|
|
1138
|
+
recovery.releasedAt = Date.now();
|
|
1139
|
+
recovery.heldRequests = cooldown.heldRequests.length;
|
|
1140
|
+
}
|
|
1141
|
+
const requests = cooldown.heldRequests.length > 0 ? cooldown.heldRequests : [{ agentName,
|
|
1142
|
+
reason: 'provider-acceleration-retry', source: 'framework', timestamp: Date.now() }];
|
|
1143
|
+
this.pendingRequests.push(...requests);
|
|
1144
|
+
console.error(`[provider-cooldown] agent=${agentName} released after ${Date.now() - cooldown.startedAt}ms — ` +
|
|
1145
|
+
`fresh compile queued with ${requests.length} retained request(s)`);
|
|
1146
|
+
}
|
|
1147
|
+
recordProviderAccelerationRecovery(agent, request, stopReason) {
|
|
1148
|
+
const recovery = this.providerAccelerationRecoveries.get(agent.name);
|
|
1149
|
+
if (!recovery?.releasedAt)
|
|
1150
|
+
return;
|
|
1151
|
+
this.providerAccelerationRecoveries.delete(agent.name);
|
|
1152
|
+
const completedAt = Date.now();
|
|
1153
|
+
const receipt = {
|
|
1154
|
+
startedAt: recovery.startedAt,
|
|
1155
|
+
releasedAt: recovery.releasedAt,
|
|
1156
|
+
completedAt,
|
|
1157
|
+
cooldownMs: recovery.releasedAt - recovery.startedAt,
|
|
1158
|
+
waitedMs: completedAt - recovery.startedAt,
|
|
1159
|
+
failures: recovery.failures,
|
|
1160
|
+
heldRequests: recovery.heldRequests,
|
|
1161
|
+
messageCount: request?.messages.length ?? 0,
|
|
1162
|
+
toolCount: request?.tools?.length ?? 0,
|
|
1163
|
+
stopReason,
|
|
1164
|
+
};
|
|
1165
|
+
this.providerAccelerationLastRecovery.set(agent.name, receipt);
|
|
1166
|
+
// Operational receipt only: provider admission must never author resident memory.
|
|
1167
|
+
console.error(`[provider-cooldown] recovered ${JSON.stringify({ agent: agent.name, ...receipt })}`);
|
|
1168
|
+
}
|
|
1169
|
+
cancelProviderAdmission() {
|
|
1170
|
+
for (const cooldown of this.providerAccelerationCooldowns.values())
|
|
1171
|
+
clearTimeout(cooldown.timer);
|
|
1172
|
+
this.providerAccelerationCooldowns.clear();
|
|
1173
|
+
this.providerAccelerationRecoveries.clear();
|
|
1174
|
+
for (const gate of this.providerGates.values()) {
|
|
1175
|
+
gate.primaryDepth = 0;
|
|
1176
|
+
gate.primaryPending = false;
|
|
1177
|
+
for (const resolve of gate.auxiliaryWaiters.splice(0))
|
|
1178
|
+
resolve();
|
|
1179
|
+
for (const resolve of gate.idleWaiters.splice(0))
|
|
1180
|
+
resolve();
|
|
1181
|
+
}
|
|
1182
|
+
this.providerGates.clear();
|
|
1183
|
+
}
|
|
1002
1184
|
startQueuedMaintenance() {
|
|
1003
1185
|
if (this.maintenancePass || !this.running)
|
|
1004
1186
|
return;
|
|
@@ -1018,6 +1200,8 @@ export class AgentFramework {
|
|
|
1018
1200
|
const cm = agent.getContextManager();
|
|
1019
1201
|
const tools = this.getToolsForAgent(agent.name).filter((tool) => agent.canUseTool(tool.name));
|
|
1020
1202
|
cm.setToolDefinitions(tools);
|
|
1203
|
+
if (this.providerGateBlocked(agent.name))
|
|
1204
|
+
return [];
|
|
1021
1205
|
if (cm.isReady())
|
|
1022
1206
|
return [];
|
|
1023
1207
|
const pending = cm.getPendingWork()?.description;
|
|
@@ -3104,7 +3288,9 @@ export class AgentFramework {
|
|
|
3104
3288
|
store: this.store,
|
|
3105
3289
|
namespace: `agents/${config.name}`,
|
|
3106
3290
|
strategy: config.strategy ?? new PassthroughStrategy(),
|
|
3107
|
-
|
|
3291
|
+
// Context Manager complete() calls are auxiliary work; the primary
|
|
3292
|
+
// streaming agent keeps the original Membrane.
|
|
3293
|
+
membrane: this.auxiliaryMembraneFor(config.name),
|
|
3108
3294
|
debugLogContext: !!process.env.DEBUG_CONTEXT,
|
|
3109
3295
|
});
|
|
3110
3296
|
const agent = new Agent(config, contextManager, this.membrane);
|
|
@@ -3331,7 +3517,7 @@ export class AgentFramework {
|
|
|
3331
3517
|
// classifier-safe D-class from the 2026-07-24 ablation). It also
|
|
3332
3518
|
// updates lastAnnouncedLocus so the next turn's announce-on-change
|
|
3333
3519
|
// diffs against what the agent was actually last told.
|
|
3334
|
-
if (agent.proseRouting
|
|
3520
|
+
if ((agent.proseRouting === 'locus' || agent.proseRouting === 'hybrid') &&
|
|
3335
3521
|
!shouldEndTurn && !overBudget && currentState.stream) {
|
|
3336
3522
|
// Two signals qualify an injection to move the pin (n=6 + n=7):
|
|
3337
3523
|
// - chat:addressed — someone explicitly spoke TO the agent
|
|
@@ -3734,6 +3920,9 @@ export class AgentFramework {
|
|
|
3734
3920
|
isolate: true,
|
|
3735
3921
|
// Strategy instances are stateful — never share the template's.
|
|
3736
3922
|
strategy: router.strategyFactory?.() ?? new PassthroughStrategy(),
|
|
3923
|
+
// Dynamic conversation forks retain the established provider policy in
|
|
3924
|
+
// this bounded first slice. Provider cooldown ownership belongs to the
|
|
3925
|
+
// persistent resident only; generation-unique forks must not leak gates.
|
|
3737
3926
|
membrane: this.membrane,
|
|
3738
3927
|
debugLogContext: !!process.env.DEBUG_CONTEXT,
|
|
3739
3928
|
});
|
|
@@ -4067,7 +4256,8 @@ export class AgentFramework {
|
|
|
4067
4256
|
// Clear pending (we'll re-add if inference doesn't run)
|
|
4068
4257
|
this.pendingRequests = [];
|
|
4069
4258
|
// Check each agent
|
|
4070
|
-
for (const [agentName,
|
|
4259
|
+
for (const [agentName, groupedRequests] of requestsByAgent) {
|
|
4260
|
+
let requests = groupedRequests;
|
|
4071
4261
|
const agent = this.agents.get(agentName);
|
|
4072
4262
|
if (!agent) {
|
|
4073
4263
|
// Agent not found — request is orphaned. Emit warning and drop.
|
|
@@ -4082,6 +4272,29 @@ export class AgentFramework {
|
|
|
4082
4272
|
console.error(`[inference-dropped] agent=${agentName} reason=agent_not_found requests=${requests.length}`);
|
|
4083
4273
|
continue;
|
|
4084
4274
|
}
|
|
4275
|
+
const providerCooldown = this.providerAccelerationCooldowns?.get(agentName);
|
|
4276
|
+
if (providerCooldown) {
|
|
4277
|
+
if (now < providerCooldown.until) {
|
|
4278
|
+
for (const req of requests) {
|
|
4279
|
+
if (!providerCooldown.heldRequests.some((r) => this.sameInferenceRequest(r, req))) {
|
|
4280
|
+
providerCooldown.heldRequests.push(req);
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
4284
|
+
if (recovery)
|
|
4285
|
+
recovery.heldRequests = providerCooldown.heldRequests.length;
|
|
4286
|
+
// The timer owns release. Do not hot-poll these requests.
|
|
4287
|
+
continue;
|
|
4288
|
+
}
|
|
4289
|
+
// Timers can deliver late under load. Release synchronously and merge
|
|
4290
|
+
// the held causes into this one current compile.
|
|
4291
|
+
clearTimeout(providerCooldown.timer);
|
|
4292
|
+
this.providerAccelerationCooldowns.delete(agentName);
|
|
4293
|
+
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
4294
|
+
if (recovery)
|
|
4295
|
+
recovery.releasedAt = now;
|
|
4296
|
+
requests = [...providerCooldown.heldRequests, ...requests];
|
|
4297
|
+
}
|
|
4085
4298
|
// Skip if agent is busy (inferring, streaming, or waiting for tools) —
|
|
4086
4299
|
// or if a turn is alive at all (activeTurnTokens): the state machine
|
|
4087
4300
|
// goes idle at endTurn's reset while driveStream teardown is still
|
|
@@ -4097,9 +4310,14 @@ export class AgentFramework {
|
|
|
4097
4310
|
// token; the predecessor frame's late finally no-ops via token-match.
|
|
4098
4311
|
// This mirrors how the restart has always overwritten activeStreams
|
|
4099
4312
|
// rather than waiting for the old stream's teardown.
|
|
4100
|
-
const
|
|
4101
|
-
const turnAlive = !
|
|
4102
|
-
|
|
4313
|
+
const budgetRestart = requests.find((r) => r.reason === 'context_budget_restart');
|
|
4314
|
+
const turnAlive = !budgetRestart && this.activeTurnTokens.has(agentName);
|
|
4315
|
+
const providerGate = this.providerGates?.get(agentName);
|
|
4316
|
+
// A primary can own provider admission while yielding to an already in-flight
|
|
4317
|
+
// auxiliary call. Keep this agent's later wakes queued, but do not block the
|
|
4318
|
+
// framework event loop or other residents while that auxiliary call settles.
|
|
4319
|
+
const providerPrimaryWaiting = (providerGate?.primaryDepth ?? 0) > 0 && !this.activeTurnTokens.has(agentName);
|
|
4320
|
+
if (providerPrimaryWaiting || turnAlive || agent.state.status === 'inferring' || agent.state.status === 'streaming' || agent.state.status === 'waiting_for_tools') {
|
|
4103
4321
|
// Re-queue requests, but warn if they've been pending too long
|
|
4104
4322
|
const oldest = Math.min(...requests.map(r => r.timestamp));
|
|
4105
4323
|
if (now - oldest > STALE_REQUEST_MS &&
|
|
@@ -4136,10 +4354,21 @@ export class AgentFramework {
|
|
|
4136
4354
|
// exactly this class of silent drop. One stderr line per drop.
|
|
4137
4355
|
console.error(`[inference-dropped] agent=${agentName} reason=policy-skip ` +
|
|
4138
4356
|
`requests=${requests.length} triggers=${requests.map((r) => r.reason).join(',')}`);
|
|
4357
|
+
const gate = this.providerGates.get(agentName);
|
|
4358
|
+
if (gate?.primaryPending && this.providerAccelerationRecoveries.has(agentName)) {
|
|
4359
|
+
gate.primaryPending = false;
|
|
4360
|
+
this.providerAccelerationRecoveries.delete(agentName);
|
|
4361
|
+
this.flushAuxiliaryAdmission(agentName);
|
|
4362
|
+
}
|
|
4139
4363
|
continue;
|
|
4140
4364
|
}
|
|
4141
4365
|
// Start streaming inference (non-blocking — driveStream runs in background)
|
|
4142
|
-
|
|
4366
|
+
// The request that grants the turn-alive exception must also define the
|
|
4367
|
+
// downstream turn semantics. An ordinary wake can already be pending
|
|
4368
|
+
// when the tool boundary appends a context-budget restart; selecting
|
|
4369
|
+
// requests[0] in that mixed batch bypassed the turn lock because a
|
|
4370
|
+
// restart existed, then treated the continuation as a fresh turn.
|
|
4371
|
+
const trigger = budgetRestart ?? requests[0];
|
|
4143
4372
|
// Route this turn's auto-published speech to the channel that triggered
|
|
4144
4373
|
// it (item-3 redux). A batched wake may carry several triggering channels
|
|
4145
4374
|
// (messages arrived in >1 channel while the agent was busy/idle):
|
|
@@ -4218,7 +4447,9 @@ export class AgentFramework {
|
|
|
4218
4447
|
: id);
|
|
4219
4448
|
}
|
|
4220
4449
|
const suppressedNote = suppressed > 0
|
|
4221
|
-
?
|
|
4450
|
+
? agent.proseRouting === 'disabled'
|
|
4451
|
+
? `${suppressed} plain-speech segment(s) suppressed (proseRouting=disabled — publish only with an explicit send tool)`
|
|
4452
|
+
: `${suppressed} plain-speech segment(s) suppressed (explicit send in the same round — resend with a send tool if it was meant to be heard)`
|
|
4222
4453
|
: '';
|
|
4223
4454
|
const text = shown.length > 0
|
|
4224
4455
|
? `[delivered] plain speech → ${shown.join(' · ')}${suppressedNote ? ` · ${suppressedNote}` : ''}`
|
|
@@ -4571,6 +4802,13 @@ export class AgentFramework {
|
|
|
4571
4802
|
console.error('maybePrimeProseMode: state read/write failed:', err);
|
|
4572
4803
|
return;
|
|
4573
4804
|
}
|
|
4805
|
+
// Disabled mode is intentionally silent: no model-visible routing prose.
|
|
4806
|
+
// The resident publishes only through explicit tools. This contains
|
|
4807
|
+
// continuity-summary locus leaks (Host #96).
|
|
4808
|
+
if (mode === 'disabled') {
|
|
4809
|
+
console.error(`[prose] ${agent.name}: disabled mode recorded (no primer injected)`);
|
|
4810
|
+
return;
|
|
4811
|
+
}
|
|
4574
4812
|
try {
|
|
4575
4813
|
const id = agent.getContextManager().addMessage('user', [{ type: 'text', text: proseModePrimer(mode) }], { system: true, kind: 'prose-routing-primer' });
|
|
4576
4814
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'prose-routing-primer' });
|
|
@@ -4580,7 +4818,32 @@ export class AgentFramework {
|
|
|
4580
4818
|
console.error('maybePrimeProseMode: failed to append primer:', err);
|
|
4581
4819
|
}
|
|
4582
4820
|
}
|
|
4583
|
-
async startAgentStream(agent, trigger, attempt = 0) {
|
|
4821
|
+
async startAgentStream(agent, trigger, attempt = 0, providerGateAlreadyHeld = false) {
|
|
4822
|
+
const ownsProviderGate = !this.ephemeralRuns.has(agent.name) && !this.conversationAgentHomes.has(agent.name);
|
|
4823
|
+
if (ownsProviderGate && !providerGateAlreadyHeld) {
|
|
4824
|
+
this.acquirePrimaryProviderGate(agent.name);
|
|
4825
|
+
const gate = this.providerGate(agent.name);
|
|
4826
|
+
if (gate.auxiliaryInFlight > 0) {
|
|
4827
|
+
// Do not await inside processInferenceRequests: one slow auxiliary call for
|
|
4828
|
+
// this resident must not block events or other residents. Admission stays
|
|
4829
|
+
// owned and the continuation receives it after the in-flight call settles.
|
|
4830
|
+
void this.waitForAuxiliaryIdle(agent.name).then(async () => {
|
|
4831
|
+
if (this.providerAdmissionClosed) {
|
|
4832
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
4833
|
+
return;
|
|
4834
|
+
}
|
|
4835
|
+
await this.startAgentStream(agent, trigger, attempt, true);
|
|
4836
|
+
}).catch((error) => {
|
|
4837
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
4838
|
+
console.error(`[provider-cooldown] failed to resume primary for ${agent.name}:`, error);
|
|
4839
|
+
});
|
|
4840
|
+
return;
|
|
4841
|
+
}
|
|
4842
|
+
if (this.providerAdmissionClosed) {
|
|
4843
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
4844
|
+
return;
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4584
4847
|
// Mark the turn alive before ANYTHING awaits (hooks, compile, stream
|
|
4585
4848
|
// setup): from here until this turn's teardown, cross-turn writers defer
|
|
4586
4849
|
// (addMessage guard) instead of appending — nothing may enter the window
|
|
@@ -4601,12 +4864,14 @@ export class AgentFramework {
|
|
|
4601
4864
|
this.activeTurnTokens.set(agent.name, turnToken);
|
|
4602
4865
|
let tokenHandedOff = false;
|
|
4603
4866
|
try {
|
|
4604
|
-
tokenHandedOff = await this.beginAgentTurn(agent, trigger, attempt, turnToken);
|
|
4867
|
+
tokenHandedOff = await this.beginAgentTurn(agent, trigger, attempt, turnToken, ownsProviderGate);
|
|
4605
4868
|
}
|
|
4606
4869
|
finally {
|
|
4607
4870
|
if (!tokenHandedOff && this.activeTurnTokens.get(agent.name) === turnToken) {
|
|
4608
4871
|
this.activeTurnTokens.delete(agent.name);
|
|
4609
4872
|
}
|
|
4873
|
+
if (!tokenHandedOff && ownsProviderGate)
|
|
4874
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
4610
4875
|
}
|
|
4611
4876
|
}
|
|
4612
4877
|
/**
|
|
@@ -4614,7 +4879,7 @@ export class AgentFramework {
|
|
|
4614
4879
|
* turn token unconditionally. Returns true iff the token was handed off to
|
|
4615
4880
|
* driveStream (whose finally then owns clearing it).
|
|
4616
4881
|
*/
|
|
4617
|
-
async beginAgentTurn(agent, trigger, attempt, turnToken) {
|
|
4882
|
+
async beginAgentTurn(agent, trigger, attempt, turnToken, ownsProviderGate) {
|
|
4618
4883
|
// Flush messages deferred during the PREVIOUS turn — before the
|
|
4619
4884
|
// checkpoint, the locus announcement, and the compile — so a turn started
|
|
4620
4885
|
// by a queued wake actually CONTAINS the message that woke it. (2026-07-31
|
|
@@ -4700,10 +4965,10 @@ export class AgentFramework {
|
|
|
4700
4965
|
this.proseHybridSuppressed.delete(agent.name);
|
|
4701
4966
|
if (agent.proseRouting === 'hybrid')
|
|
4702
4967
|
this.proseTargetPins.delete(agent.name);
|
|
4703
|
-
if (agent.proseRouting === 'explicit') {
|
|
4704
|
-
// Explicit prose routing
|
|
4705
|
-
//
|
|
4706
|
-
//
|
|
4968
|
+
if (agent.proseRouting === 'explicit' || agent.proseRouting === 'disabled') {
|
|
4969
|
+
// Explicit and disabled prose routing have no inferred locus.
|
|
4970
|
+
// Explicit mode uses a turn-scoped `>>` target; disabled mode has no
|
|
4971
|
+
// prose target at all. Neither mode freezes or announces a locus.
|
|
4707
4972
|
this.proseTargetPins.delete(agent.name);
|
|
4708
4973
|
this.proseContinuations.delete(agent.name);
|
|
4709
4974
|
this.midTurnInputSignals.delete(agent.name);
|
|
@@ -4783,7 +5048,7 @@ export class AgentFramework {
|
|
|
4783
5048
|
}
|
|
4784
5049
|
}
|
|
4785
5050
|
const { stream, request: compiledRequest } = await agent.startStreamWithInjections(tools, injections);
|
|
4786
|
-
const handle = this.driveStream(agent, stream, requestSnapshot, trigger, attempt, compiledRequest);
|
|
5051
|
+
const handle = this.driveStream(agent, stream, requestSnapshot, trigger, attempt, compiledRequest, ownsProviderGate);
|
|
4787
5052
|
this.activeStreams.set(agent.name, handle);
|
|
4788
5053
|
// Handoff: driveStream captured the token in its synchronous prefix;
|
|
4789
5054
|
// its finally now owns clearing it. The caller's finally must not.
|
|
@@ -4802,6 +5067,12 @@ export class AgentFramework {
|
|
|
4802
5067
|
stack: err.stack,
|
|
4803
5068
|
});
|
|
4804
5069
|
agent.reset();
|
|
5070
|
+
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
5071
|
+
// Capacity scheduling is not poisoned history. The cooldown timer owns
|
|
5072
|
+
// one later fresh compile; no immediate retry or exhaustion marker.
|
|
5073
|
+
this.eventGate?.onInferenceEnded(agent.name);
|
|
5074
|
+
return false;
|
|
5075
|
+
}
|
|
4805
5076
|
const action = this.errorPolicy.onInferenceError(err, agent.name, attempt);
|
|
4806
5077
|
if (action.retry) {
|
|
4807
5078
|
await new Promise((resolve) => setTimeout(resolve, action.delayMs));
|
|
@@ -4849,7 +5120,7 @@ export class AgentFramework {
|
|
|
4849
5120
|
return false;
|
|
4850
5121
|
}
|
|
4851
5122
|
}
|
|
4852
|
-
async driveStream(agent, stream, requestSnapshot, trigger, attempt = 0, compiledRequest) {
|
|
5123
|
+
async driveStream(agent, stream, requestSnapshot, trigger, attempt = 0, compiledRequest, ownsProviderGate = false) {
|
|
4853
5124
|
const startTime = Date.now();
|
|
4854
5125
|
const requestId = `${agent.name}-${startTime}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4855
5126
|
const myStreamId = agent.streamId;
|
|
@@ -4906,9 +5177,11 @@ export class AgentFramework {
|
|
|
4906
5177
|
// this doesn't violate never-guess: it says "attending to what you
|
|
4907
5178
|
// sent here", which is true regardless of where the reply goes.
|
|
4908
5179
|
// Heartbeat/no-trigger explicit turns show no indicator.
|
|
4909
|
-
const typingChannel = agent.proseRouting === '
|
|
4910
|
-
?
|
|
4911
|
-
:
|
|
5180
|
+
const typingChannel = agent.proseRouting === 'disabled'
|
|
5181
|
+
? null
|
|
5182
|
+
: agent.proseRouting === 'explicit'
|
|
5183
|
+
? trigger?.channelId ?? null
|
|
5184
|
+
: resolveTurnLocus();
|
|
4912
5185
|
if (typingChannel)
|
|
4913
5186
|
this.channelRegistry.startTyping(typingChannel);
|
|
4914
5187
|
// MCPL Spec 14.3 outgoing streaming: route text deltas to their
|
|
@@ -4936,7 +5209,7 @@ export class AgentFramework {
|
|
|
4936
5209
|
turnIndex: 0,
|
|
4937
5210
|
phase: 'started',
|
|
4938
5211
|
});
|
|
4939
|
-
const proseStream = this.channelRegistry
|
|
5212
|
+
const proseStream = this.channelRegistry && agent.proseRouting !== 'disabled'
|
|
4940
5213
|
? new ProseStreamRouter({
|
|
4941
5214
|
mode: agent.proseRouting === 'explicit' ? 'explicit' : agent.proseRouting === 'hybrid' ? 'hybrid' : 'locus',
|
|
4942
5215
|
initialTarget: typingChannel,
|
|
@@ -5109,7 +5382,11 @@ export class AgentFramework {
|
|
|
5109
5382
|
liveProseRouting = true;
|
|
5110
5383
|
const roundSegments = splitProseSegments(assistantBlocks);
|
|
5111
5384
|
if (roundSegments.length > 0) {
|
|
5112
|
-
if (agent.proseRouting === '
|
|
5385
|
+
if (agent.proseRouting === 'disabled') {
|
|
5386
|
+
console.error(`[routing] ${agent.name}: mid-turn prose NOT routed (proseRouting=disabled)`);
|
|
5387
|
+
this.recordProseSuppression(agent.name, roundSegments.length);
|
|
5388
|
+
}
|
|
5389
|
+
else if (agent.proseRouting === 'hybrid') {
|
|
5113
5390
|
if (turnSilenced) {
|
|
5114
5391
|
this.recordProseSuppression(agent.name, roundSegments.length);
|
|
5115
5392
|
}
|
|
@@ -5210,6 +5487,9 @@ export class AgentFramework {
|
|
|
5210
5487
|
else {
|
|
5211
5488
|
agent.addAssistantResponse(terminalContent);
|
|
5212
5489
|
}
|
|
5490
|
+
// Bind the cooldown receipt to this fresh, successful request —
|
|
5491
|
+
// never to the frozen request that received the 429.
|
|
5492
|
+
this.recordProviderAccelerationRecovery(agent, compiledRequest, response.stopReason);
|
|
5213
5493
|
// §10.5: context/afterInference is removed in 0.5.0 — no
|
|
5214
5494
|
// content leaves the host here. Turn boundaries are announced
|
|
5215
5495
|
// by the metadata-only inference/lifecycle notifications
|
|
@@ -5402,7 +5682,11 @@ export class AgentFramework {
|
|
|
5402
5682
|
.join('\n')
|
|
5403
5683
|
.trim();
|
|
5404
5684
|
if (speechText) {
|
|
5405
|
-
if (agent.proseRouting === '
|
|
5685
|
+
if (agent.proseRouting === 'disabled') {
|
|
5686
|
+
console.error(`[routing] ${agent.name}: text-only prose NOT routed (proseRouting=disabled)`);
|
|
5687
|
+
this.recordProseSuppression(agent.name, 1);
|
|
5688
|
+
}
|
|
5689
|
+
else if (agent.proseRouting === 'hybrid') {
|
|
5406
5690
|
const locus = resolveTurnLocus();
|
|
5407
5691
|
console.error(`[prose] ${agent.name}: text-only turn -> hybrid prose gateway`);
|
|
5408
5692
|
try {
|
|
@@ -5468,7 +5752,13 @@ export class AgentFramework {
|
|
|
5468
5752
|
// land before the trailing prose. Awaited even when silenced —
|
|
5469
5753
|
// the chain may still be flushing earlier rounds' posts.
|
|
5470
5754
|
await turnSpeechChain;
|
|
5471
|
-
if (agent.proseRouting === '
|
|
5755
|
+
if (agent.proseRouting === 'disabled') {
|
|
5756
|
+
if (segments.length > 0) {
|
|
5757
|
+
console.error(`[routing] ${agent.name}: tool-call trailing prose NOT routed (proseRouting=disabled)`);
|
|
5758
|
+
this.recordProseSuppression(agent.name, segments.length);
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5761
|
+
else if (agent.proseRouting === 'hybrid') {
|
|
5472
5762
|
if (silenced && segments.length > 0) {
|
|
5473
5763
|
this.recordProseSuppression(agent.name, segments.length);
|
|
5474
5764
|
}
|
|
@@ -5576,6 +5866,11 @@ export class AgentFramework {
|
|
|
5576
5866
|
break;
|
|
5577
5867
|
this.abortAgentScript(agent.name, 'stream error');
|
|
5578
5868
|
agent.reset();
|
|
5869
|
+
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
5870
|
+
lifecyclePhase = 'failed';
|
|
5871
|
+
this.eventGate?.onInferenceEnded(agent.name);
|
|
5872
|
+
break;
|
|
5873
|
+
}
|
|
5579
5874
|
const action = this.errorPolicy.onInferenceError(err, agent.name, attempt);
|
|
5580
5875
|
if (action.retry) {
|
|
5581
5876
|
await new Promise((resolve) => setTimeout(resolve, action.delayMs));
|
|
@@ -5709,10 +6004,21 @@ export class AgentFramework {
|
|
|
5709
6004
|
}
|
|
5710
6005
|
}
|
|
5711
6006
|
catch (error) {
|
|
5712
|
-
// Stream itself threw
|
|
5713
|
-
//
|
|
6007
|
+
// Stream itself threw. Organization acceleration is deferred from a
|
|
6008
|
+
// fresh compile; every other throw keeps the ordinary exhausted path.
|
|
5714
6009
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
5715
6010
|
const durationMs = Date.now() - startTime;
|
|
6011
|
+
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
6012
|
+
this.emitTrace({ type: 'inference:failed', agentName: agent.name, error: err.message, stack: err.stack });
|
|
6013
|
+
this.logInference({ timestamp: startTime, agentName: agent.name, requestId, success: false,
|
|
6014
|
+
error: `Provider acceleration cooldown: ${err.message}`,
|
|
6015
|
+
request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
|
|
6016
|
+
this.abortAgentScript(agent.name, 'provider acceleration cooldown');
|
|
6017
|
+
lifecyclePhase = 'failed';
|
|
6018
|
+
agent.reset();
|
|
6019
|
+
this.eventGate?.onInferenceEnded(agent.name);
|
|
6020
|
+
return;
|
|
6021
|
+
}
|
|
5716
6022
|
this.emitTrace({
|
|
5717
6023
|
type: 'inference:failed',
|
|
5718
6024
|
agentName: agent.name,
|
|
@@ -5768,6 +6074,8 @@ export class AgentFramework {
|
|
|
5768
6074
|
// wedge). onInferenceEnded is idempotent, so a redundant call is safe.
|
|
5769
6075
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
5770
6076
|
this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
|
|
6077
|
+
if (ownsProviderGate)
|
|
6078
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
5771
6079
|
// Stop the typing indicator on every exit path (complete, error,
|
|
5772
6080
|
// exhausted, abort) so it never sticks after the turn ends.
|
|
5773
6081
|
this.channelRegistry?.stopTyping();
|
|
@@ -5826,6 +6134,91 @@ export class AgentFramework {
|
|
|
5826
6134
|
agentName: call.callerAgentName ?? '__ephemeral__',
|
|
5827
6135
|
});
|
|
5828
6136
|
}
|
|
6137
|
+
/**
|
|
6138
|
+
* Admin puppet: execute ONE tool call AS an agent and persist the
|
|
6139
|
+
* tool_use + tool_result pair in that agent's window exactly as a
|
|
6140
|
+
* model-initiated call is stored.
|
|
6141
|
+
*
|
|
6142
|
+
* Born from the princess exemplar surgery (2026-08-23). Her folded
|
|
6143
|
+
* narrative said "I have no tools"; every past tool_use/tool_result pair
|
|
6144
|
+
* had folded out of her context, so the act of calling had no in-context
|
|
6145
|
+
* exemplar and she could not emit her first call despite 30 eidoverse
|
|
6146
|
+
* tools riding every request. Placing one first-person pair restored the
|
|
6147
|
+
* capacity. That repair required stop → store surgery → restart; this
|
|
6148
|
+
* method is the live path — and the general lever for demonstrating an
|
|
6149
|
+
* affordance to a model that cannot find it (older models especially).
|
|
6150
|
+
*
|
|
6151
|
+
* Semantics:
|
|
6152
|
+
* - Refused unless the agent is idle: puppeting mid-turn would corrupt
|
|
6153
|
+
* the turn state machine and the live stream's wire ordering.
|
|
6154
|
+
* - Refused for tools outside the agent's own surface (canUseTool): the
|
|
6155
|
+
* stored pair must be an act the agent could genuinely have taken.
|
|
6156
|
+
* - The call executes FOR REAL through the shared dispatch (MCPL,
|
|
6157
|
+
* channel tools, utils, modules) with the agent's provenance — a
|
|
6158
|
+
* puppeted walk_to actually moves the avatar.
|
|
6159
|
+
* - Storage byte-follows the ordinary path: a bare tool_use assistant
|
|
6160
|
+
* message (no fabricated thinking or text — a stored assistant turn
|
|
6161
|
+
* with unverifiable thinking is the labclaude 400 class) plus a
|
|
6162
|
+
* tool_result under the same bounded spill policy (issue #89).
|
|
6163
|
+
* - Does NOT request inference; the agent sees the pair on its next
|
|
6164
|
+
* wake. Speak to the agent afterward if a wake is wanted.
|
|
6165
|
+
* - Provenance is loud in traces (puppet:tool-call) and the host log,
|
|
6166
|
+
* deliberately NOT in the stored messages — metadata there would break
|
|
6167
|
+
* byte-parity with real turns. Whether to disclose to the resident is
|
|
6168
|
+
* the operator's call; the princess precedent was disclosed first.
|
|
6169
|
+
*/
|
|
6170
|
+
async puppetToolCall(agentName, toolName, input) {
|
|
6171
|
+
const agent = this.agents.get(agentName);
|
|
6172
|
+
if (!agent)
|
|
6173
|
+
throw new Error(`Unknown agent: ${agentName}`);
|
|
6174
|
+
if (agent.state.status !== 'idle') {
|
|
6175
|
+
throw new Error(`puppet refused: agent ${agentName} is ${agent.state.status} (requires idle — ` +
|
|
6176
|
+
`injecting a turn under an active stream corrupts wire ordering)`);
|
|
6177
|
+
}
|
|
6178
|
+
const onSurface = this.getToolsForAgent(agentName)
|
|
6179
|
+
.some((t) => t.name === toolName && agent.canUseTool(t.name));
|
|
6180
|
+
if (!onSurface) {
|
|
6181
|
+
throw new Error(`puppet refused: tool ${toolName} is not on ${agentName}'s surface — ` +
|
|
6182
|
+
`the stored pair must be an act the agent could genuinely have taken`);
|
|
6183
|
+
}
|
|
6184
|
+
// Anthropic-shaped id so the stored pair is indistinguishable from a
|
|
6185
|
+
// provider-issued call.
|
|
6186
|
+
const alphabet = 'ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz0123456789';
|
|
6187
|
+
let suffix = '';
|
|
6188
|
+
for (let i = 0; i < 22; i++)
|
|
6189
|
+
suffix += alphabet[Math.floor(Math.random() * alphabet.length)];
|
|
6190
|
+
const toolUseId = `toolu_01${suffix}`;
|
|
6191
|
+
const started = Date.now();
|
|
6192
|
+
const result = await this.executeToolCall({
|
|
6193
|
+
id: toolUseId,
|
|
6194
|
+
name: toolName,
|
|
6195
|
+
input,
|
|
6196
|
+
callerAgentName: agentName,
|
|
6197
|
+
});
|
|
6198
|
+
const durationMs = Date.now() - started;
|
|
6199
|
+
// Store the pair through the same shapes the ordinary path uses. Build
|
|
6200
|
+
// the result blocks BEFORE storing the tool_use: the spill path awaits,
|
|
6201
|
+
// and a message arriving during that await must land before the pair,
|
|
6202
|
+
// never between tool_use and its tool_result. The two addMessage calls
|
|
6203
|
+
// below are synchronous and adjacent — nothing can interleave.
|
|
6204
|
+
const { blocks } = await this.buildStoredToolResultContent([{ id: toolUseId, name: toolName, input, result, durationMs }], this.resolveToolResultInlineCap(agent).cap);
|
|
6205
|
+
const cm = agent.getContextManager();
|
|
6206
|
+
cm.addMessage(agentName, [
|
|
6207
|
+
{ type: 'tool_use', id: toolUseId, name: toolName, input },
|
|
6208
|
+
]);
|
|
6209
|
+
cm.addMessage('user', blocks);
|
|
6210
|
+
this.emitTrace({
|
|
6211
|
+
type: 'puppet:tool-call',
|
|
6212
|
+
agentName,
|
|
6213
|
+
toolName,
|
|
6214
|
+
toolUseId,
|
|
6215
|
+
isError: !!result.isError,
|
|
6216
|
+
durationMs,
|
|
6217
|
+
});
|
|
6218
|
+
console.log(`[puppet] ${agentName}: ${toolName} → ${result.isError ? 'ERROR' : 'ok'} ` +
|
|
6219
|
+
`in ${durationMs}ms (${toolUseId})`);
|
|
6220
|
+
return { toolUseId, result };
|
|
6221
|
+
}
|
|
5829
6222
|
async executeToolCallFrom(call, origin) {
|
|
5830
6223
|
// Client-side programmatic tool calling for promise-based callers
|
|
5831
6224
|
// (SubagentModule ephemerals). Keyed by callerAgentName so each ephemeral
|
|
@@ -6948,6 +7341,19 @@ export class AgentFramework {
|
|
|
6948
7341
|
name,
|
|
6949
7342
|
status: agent.state.status,
|
|
6950
7343
|
consecutiveInferenceFailures: this.consecutiveInferenceFailures.get(name) ?? 0,
|
|
7344
|
+
providerAdmission: (() => {
|
|
7345
|
+
const gate = this.providerGates?.get(name);
|
|
7346
|
+
const cooldown = this.providerAccelerationCooldowns?.get(name);
|
|
7347
|
+
return {
|
|
7348
|
+
primaryActive: (gate?.primaryDepth ?? 0) > 0,
|
|
7349
|
+
primaryPending: gate?.primaryPending ?? false,
|
|
7350
|
+
auxiliaryInFlight: gate?.auxiliaryInFlight ?? 0,
|
|
7351
|
+
auxiliaryDeferred: gate?.deferredAuxiliary ?? 0,
|
|
7352
|
+
cooldownUntil: cooldown?.until ?? null,
|
|
7353
|
+
heldRequests: cooldown?.heldRequests.length ?? 0,
|
|
7354
|
+
lastRecovery: this.providerAccelerationLastRecovery?.get(name) ?? null,
|
|
7355
|
+
};
|
|
7356
|
+
})(),
|
|
6951
7357
|
lastInference: this.lastInferenceAt.get(name) ?? null,
|
|
6952
7358
|
refusalStats: this.refusalStats.get(name) ?? null,
|
|
6953
7359
|
})),
|
|
@@ -8615,7 +9021,7 @@ export class AgentFramework {
|
|
|
8615
9021
|
if (opened) {
|
|
8616
9022
|
this.activeTriggerChannels.set(agentName, opened);
|
|
8617
9023
|
const openerAgent = this.agents.get(agentName);
|
|
8618
|
-
if (openerAgent && openerAgent.proseRouting
|
|
9024
|
+
if (openerAgent && (openerAgent.proseRouting === 'locus' || openerAgent.proseRouting === 'hybrid')) {
|
|
8619
9025
|
this.turnLocusPins.set(agentName, opened);
|
|
8620
9026
|
this.lastAnnouncedLocus.set(agentName, opened);
|
|
8621
9027
|
result = {
|
|
@@ -8814,7 +9220,10 @@ export class AgentFramework {
|
|
|
8814
9220
|
if (announce && this.channelRegistry) {
|
|
8815
9221
|
const text = input.message ?? `💤 Going quiet for ${human}. I'll still see messages, but won't respond until I wake.`;
|
|
8816
9222
|
const agent = this.agents.get(agentName);
|
|
8817
|
-
if (agent?.proseRouting === '
|
|
9223
|
+
if (agent?.proseRouting === 'disabled') {
|
|
9224
|
+
console.error(`[sleep] ${agentName}: proseRouting=disabled — sleep announcement not posted`);
|
|
9225
|
+
}
|
|
9226
|
+
else if (agent?.proseRouting === 'explicit') {
|
|
8818
9227
|
// Explicit mode: announce to the turn's sticky prose target if the
|
|
8819
9228
|
// model has set one; otherwise stay quiet — never guess a channel.
|
|
8820
9229
|
const target = this.proseTargetPins.get(agentName);
|