@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animalabs/agent-framework",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Multi-agent framework with pluggable modules, persistent state, and concurrent tool execution",
5
5
  "repository": {
6
6
  "type": "git",
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 holdProviderAcceleration(agent: Agent, error: Error, trigger?: InferenceRequest): boolean {
1945
- if (this.ephemeralRuns.has(agent.name) || this.conversationAgentHomes.has(agent.name) || !isOrganizationAccelerationRateLimit(error)) return false;
1946
- const now = Date.now(); const delayMs = this.accelerationCooldownMs(agent.name, error);
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} organization acceleration 429 — ` +
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
  };
@@ -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