@animalabs/agent-framework 0.16.0 → 0.17.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/framework.d.ts +5 -0
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +75 -7
- package/dist/src/framework.js.map +1 -1
- package/dist/src/types/framework.d.ts +32 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/test/liveness-watchdog.test.js +18 -2
- package/dist/test/liveness-watchdog.test.js.map +1 -1
- package/dist/test/provider-acceleration-cooldown.test.js +145 -0
- package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/framework.ts +74 -7
- package/src/types/framework.ts +36 -0
package/package.json
CHANGED
package/src/framework.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
FrameworkConfig,
|
|
22
22
|
InferencePolicy,
|
|
23
23
|
ErrorPolicy,
|
|
24
|
+
ProviderHoldHook,
|
|
24
25
|
ErrorAction,
|
|
25
26
|
FrameworkState,
|
|
26
27
|
TraceEvent,
|
|
@@ -744,6 +745,12 @@ interface ProviderAccelerationCooldown {
|
|
|
744
745
|
heldRequests: InferenceRequest[];
|
|
745
746
|
reason: string;
|
|
746
747
|
failures: number;
|
|
748
|
+
/** Set on a host-requested hold (config.providerHold): the error to
|
|
749
|
+
* re-present to the hook when this slice expires. */
|
|
750
|
+
hostHoldError?: Error;
|
|
751
|
+
/** Armed by an auxiliary (compression) failure with no primary involved:
|
|
752
|
+
* release reopens auxiliary admission without synthesising an inference. */
|
|
753
|
+
auxiliaryOrigin?: boolean;
|
|
747
754
|
}
|
|
748
755
|
interface ProviderAccelerationRecovery {
|
|
749
756
|
startedAt: number;
|
|
@@ -948,6 +955,7 @@ export class AgentFramework {
|
|
|
948
955
|
private providerAccelerationLastRecovery: Map<string, ProviderAccelerationReceipt> = new Map();
|
|
949
956
|
private providerAccelerationDefaultCooldownMs = PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS;
|
|
950
957
|
private providerAccelerationJitterMs = PROVIDER_ACCELERATION_JITTER_MS;
|
|
958
|
+
private providerHoldHook: ProviderHoldHook | undefined;
|
|
951
959
|
private providerAdmissionClosed = false;
|
|
952
960
|
/** Last time we reported stale (busy-requeued) inference requests, per agent. */
|
|
953
961
|
private staleWarnAt = new Map<string, number>();
|
|
@@ -1452,6 +1460,7 @@ export class AgentFramework {
|
|
|
1452
1460
|
normalizeDiscordAwarenessDeadline(config.discordAwarenessDeadlineMs),
|
|
1453
1461
|
new OperatorLog(operatorLogPath),
|
|
1454
1462
|
);
|
|
1463
|
+
framework.providerHoldHook = config.providerHold;
|
|
1455
1464
|
|
|
1456
1465
|
// If an offline recovery process crashed after switching Chronicle but
|
|
1457
1466
|
// before committing its prepared marker batch, the active branch is the
|
|
@@ -1915,6 +1924,14 @@ export class AgentFramework {
|
|
|
1915
1924
|
}
|
|
1916
1925
|
gate.auxiliaryInFlight++;
|
|
1917
1926
|
try { return await run(); }
|
|
1927
|
+
catch (error) {
|
|
1928
|
+
// Compression can be the first call to find a spent quota (idle agent,
|
|
1929
|
+
// scheduled maintenance). Arm the host hold so later passes wait at the
|
|
1930
|
+
// gate instead of retrying into it; this pass still fails to its caller.
|
|
1931
|
+
const agent = this.agents.get(agentName);
|
|
1932
|
+
if (agent && error instanceof Error) this.holdProviderAcceleration(agent, error, undefined, true);
|
|
1933
|
+
throw error;
|
|
1934
|
+
}
|
|
1918
1935
|
finally {
|
|
1919
1936
|
gate.auxiliaryInFlight = Math.max(0, gate.auxiliaryInFlight - 1);
|
|
1920
1937
|
if (gate.auxiliaryInFlight === 0) for (const resolve of gate.idleWaiters.splice(0)) resolve();
|
|
@@ -1941,27 +1958,75 @@ export class AgentFramework {
|
|
|
1941
1958
|
return a.agentName === b.agentName && a.reason === b.reason && a.source === b.source &&
|
|
1942
1959
|
a.timestamp === b.timestamp && a.channelId === b.channelId;
|
|
1943
1960
|
}
|
|
1944
|
-
private
|
|
1945
|
-
if (this.
|
|
1946
|
-
|
|
1961
|
+
private consultProviderHold(error: Error, agentName: string): { holdMs: number; reason: string } | undefined {
|
|
1962
|
+
if (!this.providerHoldHook) return undefined;
|
|
1963
|
+
try {
|
|
1964
|
+
const hold = this.providerHoldHook(error, agentName, { model: this.agents.get(agentName)?.model });
|
|
1965
|
+
if (!hold || !Number.isFinite(hold.holdMs) || hold.holdMs <= 0) return undefined;
|
|
1966
|
+
return { holdMs: Math.min(PROVIDER_ACCELERATION_MAX_COOLDOWN_MS, Math.max(1_000, hold.holdMs)),
|
|
1967
|
+
reason: typeof hold.reason === 'string' && hold.reason ? hold.reason : error.message };
|
|
1968
|
+
} catch (err) {
|
|
1969
|
+
console.error(`[provider-cooldown] providerHold hook threw for ${agentName}; treating as no hold:`, err);
|
|
1970
|
+
return undefined;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
/** A host hold whose slice expired: ask the host again before spending an
|
|
1974
|
+
* inference on it. True = extended in place (held requests stay held). */
|
|
1975
|
+
private extendHostProviderHold(agentName: string, cooldown: ProviderAccelerationCooldown): boolean {
|
|
1976
|
+
if (!cooldown.hostHoldError) return false;
|
|
1977
|
+
const hold = this.consultProviderHold(cooldown.hostHoldError, agentName);
|
|
1978
|
+
if (!hold) return false;
|
|
1979
|
+
clearTimeout(cooldown.timer);
|
|
1980
|
+
cooldown.until = Date.now() + hold.holdMs;
|
|
1981
|
+
cooldown.timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agentName), hold.holdMs);
|
|
1982
|
+
cooldown.timer.unref?.();
|
|
1983
|
+
if (hold.reason !== cooldown.reason) {
|
|
1984
|
+
cooldown.reason = hold.reason;
|
|
1985
|
+
console.error(`[provider-cooldown] agent=${agentName} host hold extended — ${hold.reason}`);
|
|
1986
|
+
}
|
|
1987
|
+
return true;
|
|
1988
|
+
}
|
|
1989
|
+
private holdProviderAcceleration(agent: Agent, error: Error, trigger?: InferenceRequest, auxiliary = false): boolean {
|
|
1990
|
+
if (this.ephemeralRuns.has(agent.name) || this.conversationAgentHomes.has(agent.name)) return false;
|
|
1991
|
+
// The host knows more than the message wording does: ask it first. An
|
|
1992
|
+
// auxiliary failure arms host holds only — the built-in acceleration
|
|
1993
|
+
// cooldown stays a primary-path mechanism.
|
|
1994
|
+
const hostHold = this.consultProviderHold(error, agent.name);
|
|
1995
|
+
const acceleration = !hostHold && !auxiliary && isOrganizationAccelerationRateLimit(error);
|
|
1996
|
+
if (!acceleration && !hostHold) return false;
|
|
1997
|
+
const now = Date.now(); const delayMs = hostHold ? hostHold.holdMs : this.accelerationCooldownMs(agent.name, error as MembraneError);
|
|
1947
1998
|
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
1948
1999
|
const held = existing?.heldRequests ?? [];
|
|
1949
2000
|
if (trigger && !held.some((r) => this.sameInferenceRequest(r, trigger))) held.push(trigger);
|
|
1950
2001
|
if (existing) clearTimeout(existing.timer);
|
|
1951
2002
|
const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), delayMs); timer.unref?.();
|
|
1952
2003
|
this.providerAccelerationCooldowns.set(agent.name, { startedAt: existing?.startedAt ?? now,
|
|
1953
|
-
until: now + delayMs, timer, heldRequests: held, reason: error.message,
|
|
1954
|
-
failures: (existing?.failures ?? 0) + 1 })
|
|
2004
|
+
until: now + delayMs, timer, heldRequests: held, reason: hostHold?.reason ?? error.message,
|
|
2005
|
+
failures: (existing?.failures ?? 0) + 1, ...(hostHold ? { hostHoldError: error } : {}),
|
|
2006
|
+
...(auxiliary && (!existing || existing.auxiliaryOrigin) ? { auxiliaryOrigin: true } : {}) });
|
|
2007
|
+
if (auxiliary && !existing) {
|
|
2008
|
+
console.error(`[provider-cooldown] agent=${agent.name} host hold (${hostHold!.reason}) from an auxiliary call — ` +
|
|
2009
|
+
`holding primary/auxiliary for ${delayMs}ms`);
|
|
2010
|
+
return true;
|
|
2011
|
+
}
|
|
1955
2012
|
const gate = this.providerGate(agent.name); gate.primaryPending = true;
|
|
1956
2013
|
this.providerAccelerationRecoveries.set(agent.name, { startedAt: existing?.startedAt ?? now,
|
|
1957
2014
|
failures: (existing?.failures ?? 0) + 1, heldRequests: held.length, reason: error.message });
|
|
1958
|
-
console.error(`[provider-cooldown] agent=${agent.name}
|
|
2015
|
+
console.error(`[provider-cooldown] agent=${agent.name} ` +
|
|
2016
|
+
`${hostHold ? `host hold (${hostHold.reason})` : 'organization acceleration 429'} — ` +
|
|
1959
2017
|
`holding primary/auxiliary for ${delayMs}ms; ${held.length} request(s) retained`);
|
|
1960
2018
|
return true;
|
|
1961
2019
|
}
|
|
1962
2020
|
private releaseProviderAccelerationCooldown(agentName: string): void {
|
|
1963
2021
|
const cooldown = this.providerAccelerationCooldowns.get(agentName); if (!cooldown) return;
|
|
2022
|
+
if (this.extendHostProviderHold(agentName, cooldown)) return;
|
|
1964
2023
|
clearTimeout(cooldown.timer); this.providerAccelerationCooldowns.delete(agentName);
|
|
2024
|
+
if (cooldown.auxiliaryOrigin && cooldown.heldRequests.length === 0) {
|
|
2025
|
+
// Nobody asked for an inference while parked: just reopen the gate.
|
|
2026
|
+
console.error(`[provider-cooldown] agent=${agentName} auxiliary hold released after ${Date.now() - cooldown.startedAt}ms`);
|
|
2027
|
+
this.flushAuxiliaryAdmission(agentName);
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
1965
2030
|
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
1966
2031
|
if (recovery) { recovery.releasedAt = Date.now(); recovery.heldRequests = cooldown.heldRequests.length; }
|
|
1967
2032
|
const requests = cooldown.heldRequests.length > 0 ? cooldown.heldRequests : [{ agentName,
|
|
@@ -7322,7 +7387,7 @@ export class AgentFramework {
|
|
|
7322
7387
|
|
|
7323
7388
|
const providerCooldown = this.providerAccelerationCooldowns?.get(agentName);
|
|
7324
7389
|
if (providerCooldown) {
|
|
7325
|
-
if (now < providerCooldown.until) {
|
|
7390
|
+
if (now < providerCooldown.until || this.extendHostProviderHold(agentName, providerCooldown)) {
|
|
7326
7391
|
for (const req of requests) {
|
|
7327
7392
|
if (!providerCooldown.heldRequests.some((r) => this.sameInferenceRequest(r, req))) {
|
|
7328
7393
|
providerCooldown.heldRequests.push(req);
|
|
@@ -11349,6 +11414,8 @@ export class AgentFramework {
|
|
|
11349
11414
|
auxiliaryInFlight: gate?.auxiliaryInFlight ?? 0,
|
|
11350
11415
|
auxiliaryDeferred: gate?.deferredAuxiliary ?? 0,
|
|
11351
11416
|
cooldownUntil: cooldown?.until ?? null,
|
|
11417
|
+
cooldownReason: cooldown?.reason ?? null,
|
|
11418
|
+
hostHold: cooldown?.hostHoldError !== undefined,
|
|
11352
11419
|
heldRequests: cooldown?.heldRequests.length ?? 0,
|
|
11353
11420
|
lastRecovery: this.providerAccelerationLastRecovery?.get(name) ?? null,
|
|
11354
11421
|
};
|
package/src/types/framework.ts
CHANGED
|
@@ -70,6 +70,21 @@ export interface CodeExecutionConfig {
|
|
|
70
70
|
maxWakesPerScript?: number;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** See `FrameworkConfig.providerHold`. */
|
|
74
|
+
export interface ProviderHold {
|
|
75
|
+
holdMs: number;
|
|
76
|
+
/** Operator-facing explanation, logged with the hold. */
|
|
77
|
+
reason?: string;
|
|
78
|
+
}
|
|
79
|
+
/** What the framework knows about the failing call. */
|
|
80
|
+
export interface ProviderHoldContext {
|
|
81
|
+
/** Model of the agent whose call failed — a quota window may be model-scoped. */
|
|
82
|
+
model?: string;
|
|
83
|
+
}
|
|
84
|
+
export type ProviderHoldHook = (
|
|
85
|
+
error: Error, agentName: string, context: ProviderHoldContext,
|
|
86
|
+
) => ProviderHold | undefined;
|
|
87
|
+
|
|
73
88
|
export interface FrameworkConfig {
|
|
74
89
|
/**
|
|
75
90
|
* The subconscious resident (issue #77): a persistent side-agent that
|
|
@@ -150,6 +165,27 @@ export interface FrameworkConfig {
|
|
|
150
165
|
/** Custom error policy */
|
|
151
166
|
errorPolicy?: ErrorPolicy;
|
|
152
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Host verdict on a failed inference: "this cannot succeed until later".
|
|
170
|
+
* Consulted before the error policy and before the built-in
|
|
171
|
+
* organization-acceleration classification, for a persistent agent's
|
|
172
|
+
* primary inference and for its auxiliary (compression) calls. Ephemeral
|
|
173
|
+
* runs (subagents) and conversation forks are NOT covered: they have no
|
|
174
|
+
* provider-admission gate and keep the ordinary error policy. A hold parks the agent's provider
|
|
175
|
+
* admission (primary and auxiliary) instead of retrying, and — like the
|
|
176
|
+
* built-in organization-acceleration cooldown — records no failed turn and
|
|
177
|
+
* does not feed the hard-down streak. The motivating case is a subscription
|
|
178
|
+
* credential whose quota window is spent: its 429 looks like a throttle,
|
|
179
|
+
* but retrying cannot help until the window resets.
|
|
180
|
+
*
|
|
181
|
+
* Holds are served in slices of at most 10 minutes. When a slice expires
|
|
182
|
+
* the hook is consulted again with the same error, before any inference is
|
|
183
|
+
* attempted; it extends the hold by returning another, or releases it by
|
|
184
|
+
* returning undefined. Must be synchronous and cheap; a throwing hook is
|
|
185
|
+
* treated as "no hold".
|
|
186
|
+
*/
|
|
187
|
+
providerHold?: ProviderHoldHook;
|
|
188
|
+
|
|
153
189
|
/** Interval for periodic store sync in milliseconds (default: 1000ms, 0 to disable) */
|
|
154
190
|
syncIntervalMs?: number;
|
|
155
191
|
|