@animalabs/agent-framework 0.16.0 → 0.18.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 +87 -10
- package/dist/src/framework.js.map +1 -1
- package/dist/src/thinking-token-stamp.d.ts +7 -0
- package/dist/src/thinking-token-stamp.d.ts.map +1 -0
- package/dist/src/thinking-token-stamp.js +103 -0
- package/dist/src/thinking-token-stamp.js.map +1 -0
- 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/test/thinking-token-stamp.test.d.ts +12 -0
- package/dist/test/thinking-token-stamp.test.d.ts.map +1 -0
- package/dist/test/thinking-token-stamp.test.js +129 -0
- package/dist/test/thinking-token-stamp.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/framework.ts +89 -10
- package/src/thinking-token-stamp.ts +95 -0
- package/src/types/framework.ts +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animalabs/agent-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Multi-agent framework with pluggable modules, persistent state, and concurrent tool execution",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"license": "MIT",
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@animalabs/chronicle": "^0.4.0",
|
|
50
|
-
"@animalabs/context-manager": "^0.
|
|
50
|
+
"@animalabs/context-manager": "^0.11.0",
|
|
51
51
|
"@animalabs/membrane": "^0.5.78",
|
|
52
52
|
"chokidar": "^4.0.3",
|
|
53
53
|
"discord.js": "^14.25.1",
|
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,
|
|
@@ -97,6 +98,7 @@ import {
|
|
|
97
98
|
} from './code-execution/tool-definition.js';
|
|
98
99
|
import { splitProseSegments } from './prose-segments.js';
|
|
99
100
|
import { cumulativeDelta } from './usage-accounting.js';
|
|
101
|
+
import { stampThinkingTokenEstimates } from './thinking-token-stamp.js';
|
|
100
102
|
|
|
101
103
|
/** Detect a supported image media type from magic bytes (the model API
|
|
102
104
|
* rejects mislabeled media types, so trust bytes over extensions).
|
|
@@ -744,6 +746,12 @@ interface ProviderAccelerationCooldown {
|
|
|
744
746
|
heldRequests: InferenceRequest[];
|
|
745
747
|
reason: string;
|
|
746
748
|
failures: number;
|
|
749
|
+
/** Set on a host-requested hold (config.providerHold): the error to
|
|
750
|
+
* re-present to the hook when this slice expires. */
|
|
751
|
+
hostHoldError?: Error;
|
|
752
|
+
/** Armed by an auxiliary (compression) failure with no primary involved:
|
|
753
|
+
* release reopens auxiliary admission without synthesising an inference. */
|
|
754
|
+
auxiliaryOrigin?: boolean;
|
|
747
755
|
}
|
|
748
756
|
interface ProviderAccelerationRecovery {
|
|
749
757
|
startedAt: number;
|
|
@@ -948,6 +956,7 @@ export class AgentFramework {
|
|
|
948
956
|
private providerAccelerationLastRecovery: Map<string, ProviderAccelerationReceipt> = new Map();
|
|
949
957
|
private providerAccelerationDefaultCooldownMs = PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS;
|
|
950
958
|
private providerAccelerationJitterMs = PROVIDER_ACCELERATION_JITTER_MS;
|
|
959
|
+
private providerHoldHook: ProviderHoldHook | undefined;
|
|
951
960
|
private providerAdmissionClosed = false;
|
|
952
961
|
/** Last time we reported stale (busy-requeued) inference requests, per agent. */
|
|
953
962
|
private staleWarnAt = new Map<string, number>();
|
|
@@ -1452,6 +1461,7 @@ export class AgentFramework {
|
|
|
1452
1461
|
normalizeDiscordAwarenessDeadline(config.discordAwarenessDeadlineMs),
|
|
1453
1462
|
new OperatorLog(operatorLogPath),
|
|
1454
1463
|
);
|
|
1464
|
+
framework.providerHoldHook = config.providerHold;
|
|
1455
1465
|
|
|
1456
1466
|
// If an offline recovery process crashed after switching Chronicle but
|
|
1457
1467
|
// before committing its prepared marker batch, the active branch is the
|
|
@@ -1915,6 +1925,14 @@ export class AgentFramework {
|
|
|
1915
1925
|
}
|
|
1916
1926
|
gate.auxiliaryInFlight++;
|
|
1917
1927
|
try { return await run(); }
|
|
1928
|
+
catch (error) {
|
|
1929
|
+
// Compression can be the first call to find a spent quota (idle agent,
|
|
1930
|
+
// scheduled maintenance). Arm the host hold so later passes wait at the
|
|
1931
|
+
// gate instead of retrying into it; this pass still fails to its caller.
|
|
1932
|
+
const agent = this.agents.get(agentName);
|
|
1933
|
+
if (agent && error instanceof Error) this.holdProviderAcceleration(agent, error, undefined, true);
|
|
1934
|
+
throw error;
|
|
1935
|
+
}
|
|
1918
1936
|
finally {
|
|
1919
1937
|
gate.auxiliaryInFlight = Math.max(0, gate.auxiliaryInFlight - 1);
|
|
1920
1938
|
if (gate.auxiliaryInFlight === 0) for (const resolve of gate.idleWaiters.splice(0)) resolve();
|
|
@@ -1941,27 +1959,75 @@ export class AgentFramework {
|
|
|
1941
1959
|
return a.agentName === b.agentName && a.reason === b.reason && a.source === b.source &&
|
|
1942
1960
|
a.timestamp === b.timestamp && a.channelId === b.channelId;
|
|
1943
1961
|
}
|
|
1944
|
-
private
|
|
1945
|
-
if (this.
|
|
1946
|
-
|
|
1962
|
+
private consultProviderHold(error: Error, agentName: string): { holdMs: number; reason: string } | undefined {
|
|
1963
|
+
if (!this.providerHoldHook) return undefined;
|
|
1964
|
+
try {
|
|
1965
|
+
const hold = this.providerHoldHook(error, agentName, { model: this.agents.get(agentName)?.model });
|
|
1966
|
+
if (!hold || !Number.isFinite(hold.holdMs) || hold.holdMs <= 0) return undefined;
|
|
1967
|
+
return { holdMs: Math.min(PROVIDER_ACCELERATION_MAX_COOLDOWN_MS, Math.max(1_000, hold.holdMs)),
|
|
1968
|
+
reason: typeof hold.reason === 'string' && hold.reason ? hold.reason : error.message };
|
|
1969
|
+
} catch (err) {
|
|
1970
|
+
console.error(`[provider-cooldown] providerHold hook threw for ${agentName}; treating as no hold:`, err);
|
|
1971
|
+
return undefined;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
/** A host hold whose slice expired: ask the host again before spending an
|
|
1975
|
+
* inference on it. True = extended in place (held requests stay held). */
|
|
1976
|
+
private extendHostProviderHold(agentName: string, cooldown: ProviderAccelerationCooldown): boolean {
|
|
1977
|
+
if (!cooldown.hostHoldError) return false;
|
|
1978
|
+
const hold = this.consultProviderHold(cooldown.hostHoldError, agentName);
|
|
1979
|
+
if (!hold) return false;
|
|
1980
|
+
clearTimeout(cooldown.timer);
|
|
1981
|
+
cooldown.until = Date.now() + hold.holdMs;
|
|
1982
|
+
cooldown.timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agentName), hold.holdMs);
|
|
1983
|
+
cooldown.timer.unref?.();
|
|
1984
|
+
if (hold.reason !== cooldown.reason) {
|
|
1985
|
+
cooldown.reason = hold.reason;
|
|
1986
|
+
console.error(`[provider-cooldown] agent=${agentName} host hold extended — ${hold.reason}`);
|
|
1987
|
+
}
|
|
1988
|
+
return true;
|
|
1989
|
+
}
|
|
1990
|
+
private holdProviderAcceleration(agent: Agent, error: Error, trigger?: InferenceRequest, auxiliary = false): boolean {
|
|
1991
|
+
if (this.ephemeralRuns.has(agent.name) || this.conversationAgentHomes.has(agent.name)) return false;
|
|
1992
|
+
// The host knows more than the message wording does: ask it first. An
|
|
1993
|
+
// auxiliary failure arms host holds only — the built-in acceleration
|
|
1994
|
+
// cooldown stays a primary-path mechanism.
|
|
1995
|
+
const hostHold = this.consultProviderHold(error, agent.name);
|
|
1996
|
+
const acceleration = !hostHold && !auxiliary && isOrganizationAccelerationRateLimit(error);
|
|
1997
|
+
if (!acceleration && !hostHold) return false;
|
|
1998
|
+
const now = Date.now(); const delayMs = hostHold ? hostHold.holdMs : this.accelerationCooldownMs(agent.name, error as MembraneError);
|
|
1947
1999
|
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
1948
2000
|
const held = existing?.heldRequests ?? [];
|
|
1949
2001
|
if (trigger && !held.some((r) => this.sameInferenceRequest(r, trigger))) held.push(trigger);
|
|
1950
2002
|
if (existing) clearTimeout(existing.timer);
|
|
1951
2003
|
const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), delayMs); timer.unref?.();
|
|
1952
2004
|
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 })
|
|
2005
|
+
until: now + delayMs, timer, heldRequests: held, reason: hostHold?.reason ?? error.message,
|
|
2006
|
+
failures: (existing?.failures ?? 0) + 1, ...(hostHold ? { hostHoldError: error } : {}),
|
|
2007
|
+
...(auxiliary && (!existing || existing.auxiliaryOrigin) ? { auxiliaryOrigin: true } : {}) });
|
|
2008
|
+
if (auxiliary && !existing) {
|
|
2009
|
+
console.error(`[provider-cooldown] agent=${agent.name} host hold (${hostHold!.reason}) from an auxiliary call — ` +
|
|
2010
|
+
`holding primary/auxiliary for ${delayMs}ms`);
|
|
2011
|
+
return true;
|
|
2012
|
+
}
|
|
1955
2013
|
const gate = this.providerGate(agent.name); gate.primaryPending = true;
|
|
1956
2014
|
this.providerAccelerationRecoveries.set(agent.name, { startedAt: existing?.startedAt ?? now,
|
|
1957
2015
|
failures: (existing?.failures ?? 0) + 1, heldRequests: held.length, reason: error.message });
|
|
1958
|
-
console.error(`[provider-cooldown] agent=${agent.name}
|
|
2016
|
+
console.error(`[provider-cooldown] agent=${agent.name} ` +
|
|
2017
|
+
`${hostHold ? `host hold (${hostHold.reason})` : 'organization acceleration 429'} — ` +
|
|
1959
2018
|
`holding primary/auxiliary for ${delayMs}ms; ${held.length} request(s) retained`);
|
|
1960
2019
|
return true;
|
|
1961
2020
|
}
|
|
1962
2021
|
private releaseProviderAccelerationCooldown(agentName: string): void {
|
|
1963
2022
|
const cooldown = this.providerAccelerationCooldowns.get(agentName); if (!cooldown) return;
|
|
2023
|
+
if (this.extendHostProviderHold(agentName, cooldown)) return;
|
|
1964
2024
|
clearTimeout(cooldown.timer); this.providerAccelerationCooldowns.delete(agentName);
|
|
2025
|
+
if (cooldown.auxiliaryOrigin && cooldown.heldRequests.length === 0) {
|
|
2026
|
+
// Nobody asked for an inference while parked: just reopen the gate.
|
|
2027
|
+
console.error(`[provider-cooldown] agent=${agentName} auxiliary hold released after ${Date.now() - cooldown.startedAt}ms`);
|
|
2028
|
+
this.flushAuxiliaryAdmission(agentName);
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
1965
2031
|
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
1966
2032
|
if (recovery) { recovery.releasedAt = Date.now(); recovery.heldRequests = cooldown.heldRequests.length; }
|
|
1967
2033
|
const requests = cooldown.heldRequests.length > 0 ? cooldown.heldRequests : [{ agentName,
|
|
@@ -7322,7 +7388,7 @@ export class AgentFramework {
|
|
|
7322
7388
|
|
|
7323
7389
|
const providerCooldown = this.providerAccelerationCooldowns?.get(agentName);
|
|
7324
7390
|
if (providerCooldown) {
|
|
7325
|
-
if (now < providerCooldown.until) {
|
|
7391
|
+
if (now < providerCooldown.until || this.extendHostProviderHold(agentName, providerCooldown)) {
|
|
7326
7392
|
for (const req of requests) {
|
|
7327
7393
|
if (!providerCooldown.heldRequests.some((r) => this.sameInferenceRequest(r, req))) {
|
|
7328
7394
|
providerCooldown.heldRequests.push(req);
|
|
@@ -8413,6 +8479,12 @@ export class AgentFramework {
|
|
|
8413
8479
|
cacheCreationTokens: 0,
|
|
8414
8480
|
cacheReadTokens: 0,
|
|
8415
8481
|
};
|
|
8482
|
+
// Output tokens billed since this turn's assistant blocks were last
|
|
8483
|
+
// persisted. The residual over the round's visible blocks is the price of
|
|
8484
|
+
// its hidden thinking, stamped on the signed blocks at persist time
|
|
8485
|
+
// (thinking-token-stamp.ts). Accumulates across max_tokens continuations
|
|
8486
|
+
// within a round; reset at each stamp.
|
|
8487
|
+
let outputTokensSinceStamp = 0;
|
|
8416
8488
|
// This turn's alive-marker, set at startAgentStream entry. Safe to read
|
|
8417
8489
|
// from the map here: the turn-alive busy check in processInferenceRequests
|
|
8418
8490
|
// means no successor turn can have replaced it while we compiled.
|
|
@@ -8676,6 +8748,8 @@ export class AgentFramework {
|
|
|
8676
8748
|
});
|
|
8677
8749
|
}
|
|
8678
8750
|
}
|
|
8751
|
+
assistantBlocks = stampThinkingTokenEstimates(assistantBlocks, outputTokensSinceStamp);
|
|
8752
|
+
outputTokensSinceStamp = 0;
|
|
8679
8753
|
this.pendingAssistantBlocks.set(agent.name, assistantBlocks);
|
|
8680
8754
|
|
|
8681
8755
|
// Note: max_tokens truncation cannot produce tool-calls events here.
|
|
@@ -8824,9 +8898,11 @@ export class AgentFramework {
|
|
|
8824
8898
|
b.type === 'tool_use' || b.type === 'tool_result' ? i : last,
|
|
8825
8899
|
-1
|
|
8826
8900
|
);
|
|
8827
|
-
const terminalContent =
|
|
8828
|
-
? response.content.slice(lastToolIdx + 1)
|
|
8829
|
-
|
|
8901
|
+
const terminalContent = stampThinkingTokenEstimates(
|
|
8902
|
+
lastToolIdx >= 0 ? response.content.slice(lastToolIdx + 1) : response.content,
|
|
8903
|
+
outputTokensSinceStamp,
|
|
8904
|
+
);
|
|
8905
|
+
outputTokensSinceStamp = 0;
|
|
8830
8906
|
// This is a whole-response boundary, not a trailing-prose
|
|
8831
8907
|
// classifier. If the turn executed any genuine structured tool
|
|
8832
8908
|
// call, preserve its later prose exactly as ordinary history.
|
|
@@ -9489,6 +9565,7 @@ export class AgentFramework {
|
|
|
9489
9565
|
),
|
|
9490
9566
|
};
|
|
9491
9567
|
previousUsage = cumulativeUsage;
|
|
9568
|
+
outputTokensSinceStamp += perCallUsage.outputTokens;
|
|
9492
9569
|
const strat = (agent as unknown as {
|
|
9493
9570
|
getContextManager?: () => { getStrategy?: () => unknown };
|
|
9494
9571
|
}).getContextManager?.()?.getStrategy?.() as
|
|
@@ -11349,6 +11426,8 @@ export class AgentFramework {
|
|
|
11349
11426
|
auxiliaryInFlight: gate?.auxiliaryInFlight ?? 0,
|
|
11350
11427
|
auxiliaryDeferred: gate?.deferredAuxiliary ?? 0,
|
|
11351
11428
|
cooldownUntil: cooldown?.until ?? null,
|
|
11429
|
+
cooldownReason: cooldown?.reason ?? null,
|
|
11430
|
+
hostHold: cooldown?.hostHoldError !== undefined,
|
|
11352
11431
|
heldRequests: cooldown?.heldRequests.length ?? 0,
|
|
11353
11432
|
lastRecovery: this.providerAccelerationLastRecovery?.get(name) ?? null,
|
|
11354
11433
|
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ContentBlock } from '@animalabs/membrane';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stamp signed thinking blocks with the tokens they will cost when replayed.
|
|
5
|
+
*
|
|
6
|
+
* On keep-all models (Opus >= 4.5, Sonnet >= 4.6, Fable/Mythos) every prior
|
|
7
|
+
* assistant thinking block is sent back and billed as INPUT at the size of
|
|
8
|
+
* the full hidden chain of thought — whatever its visible text says (empty
|
|
9
|
+
* under `display: "omitted"`, a short summary under `"summarized"`). The
|
|
10
|
+
* signature encodes that chain but its length is not a reliable measure
|
|
11
|
+
* (2.8–8.7 chars/token measured on Opus 4.8), and the stored block carries
|
|
12
|
+
* no other trace of the size. The response DOES: `usage.output_tokens` is
|
|
13
|
+
* the visible blocks plus the hidden thinking. So at creation, price each
|
|
14
|
+
* carrier block from the residual of this call's output tokens over its
|
|
15
|
+
* visible blocks, split across carriers by signature length, and stamp it
|
|
16
|
+
* as `tokenEstimate` — the field context-manager already prefers over any
|
|
17
|
+
* heuristic for thinking, redacted_thinking and image blocks.
|
|
18
|
+
*
|
|
19
|
+
* Carriers: `thinking` with a non-empty `signature`, `redacted_thinking`
|
|
20
|
+
* with `data`. Blocks that already carry a `tokenEstimate` keep it. When the
|
|
21
|
+
* residual is not positive (no thinking, or the visible estimate over-
|
|
22
|
+
* shoots) nothing is stamped and context-manager's own fallback applies.
|
|
23
|
+
*
|
|
24
|
+
* The visible-block rates mirror context-manager's estimator (prose 2.9,
|
|
25
|
+
* JSON 2.3 chars/token); their precision matters little — on agentic turns
|
|
26
|
+
* the hidden thinking is the bulk of the output, and this stamp is what makes
|
|
27
|
+
* the compiled budget honest (see context-manager `SIGNATURE_CHARS_PER_TOKEN`).
|
|
28
|
+
*
|
|
29
|
+
* Wire safety: membrane's Anthropic formatter builds thinking blocks field
|
|
30
|
+
* by field (`type`, `thinking`, `signature`), so the stamp never reaches the
|
|
31
|
+
* provider; the signature itself is copied, never edited.
|
|
32
|
+
*/
|
|
33
|
+
const PROSE_CHARS_PER_TOKEN = 2.9;
|
|
34
|
+
const DENSE_CHARS_PER_TOKEN = 2.3;
|
|
35
|
+
|
|
36
|
+
function carrierWeight(block: ContentBlock): number {
|
|
37
|
+
if (block.type === 'thinking') {
|
|
38
|
+
const sig = (block as { signature?: unknown }).signature;
|
|
39
|
+
return typeof sig === 'string' ? sig.length : 0;
|
|
40
|
+
}
|
|
41
|
+
if (block.type === 'redacted_thinking') {
|
|
42
|
+
const data = (block as { data?: unknown }).data;
|
|
43
|
+
return typeof data === 'string' ? data.length : 0;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function visibleTokens(block: ContentBlock): number {
|
|
49
|
+
switch (block.type) {
|
|
50
|
+
case 'text':
|
|
51
|
+
return Math.ceil((block.text ?? '').length / PROSE_CHARS_PER_TOKEN);
|
|
52
|
+
case 'tool_use': {
|
|
53
|
+
let json = '';
|
|
54
|
+
try { json = JSON.stringify(block.input ?? {}); } catch { json = ''; }
|
|
55
|
+
return Math.ceil(json.length / DENSE_CHARS_PER_TOKEN) + 20;
|
|
56
|
+
}
|
|
57
|
+
case 'thinking':
|
|
58
|
+
// Unsigned thinking (text-only providers, XML mode): the text is the
|
|
59
|
+
// whole cost and is priced by context-manager as text.
|
|
60
|
+
return carrierWeight(block) > 0 ? 0 : Math.ceil((block.thinking ?? '').length / PROSE_CHARS_PER_TOKEN);
|
|
61
|
+
default:
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns a new array; carrier blocks are shallow-copied with `tokenEstimate`
|
|
68
|
+
* set, every other block is the same object.
|
|
69
|
+
*/
|
|
70
|
+
export function stampThinkingTokenEstimates(blocks: ContentBlock[], outputTokens: number): ContentBlock[] {
|
|
71
|
+
if (!Array.isArray(blocks) || blocks.length === 0) return blocks;
|
|
72
|
+
if (!Number.isFinite(outputTokens) || outputTokens <= 0) return blocks;
|
|
73
|
+
|
|
74
|
+
let totalWeight = 0;
|
|
75
|
+
let visible = 0;
|
|
76
|
+
for (const block of blocks) {
|
|
77
|
+
const already = (block as { tokenEstimate?: unknown }).tokenEstimate;
|
|
78
|
+
const w = typeof already === 'number' ? 0 : carrierWeight(block);
|
|
79
|
+
if (w > 0) totalWeight += w;
|
|
80
|
+
else visible += visibleTokens(block);
|
|
81
|
+
if (typeof already === 'number') visible += already;
|
|
82
|
+
}
|
|
83
|
+
if (totalWeight <= 0) return blocks;
|
|
84
|
+
const residual = outputTokens - visible;
|
|
85
|
+
if (residual <= 0) return blocks;
|
|
86
|
+
|
|
87
|
+
return blocks.map((block) => {
|
|
88
|
+
const already = (block as { tokenEstimate?: unknown }).tokenEstimate;
|
|
89
|
+
if (typeof already === 'number') return block;
|
|
90
|
+
const w = carrierWeight(block);
|
|
91
|
+
if (w <= 0) return block;
|
|
92
|
+
const tokenEstimate = Math.max(1, Math.round((residual * w) / totalWeight));
|
|
93
|
+
return { ...block, tokenEstimate } as ContentBlock;
|
|
94
|
+
});
|
|
95
|
+
}
|
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
|
|