@genesislcap/ai-assistant 15.23.0 → 15.23.1-FUI-2603.9

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.
Files changed (33) hide show
  1. package/dist/ai-assistant.api.json +269 -5
  2. package/dist/ai-assistant.d.ts +256 -31
  3. package/dist/chat-driver.cjs +125 -58
  4. package/dist/chat-driver.cjs.map +2 -2
  5. package/dist/chat-driver.mjs +125 -58
  6. package/dist/chat-driver.mjs.map +2 -2
  7. package/dist/custom-elements.json +125 -6
  8. package/dist/dts/components/ai-driver/ai-driver.d.ts +63 -0
  9. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  10. package/dist/dts/components/chat-driver/chat-driver.d.ts +7 -0
  11. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  12. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +9 -1
  13. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  14. package/dist/dts/config/config.d.ts +53 -17
  15. package/dist/dts/config/config.d.ts.map +1 -1
  16. package/dist/dts/config/define-stateful-agent.d.ts +16 -1
  17. package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
  18. package/dist/dts/main/main.d.ts +102 -14
  19. package/dist/dts/main/main.d.ts.map +1 -1
  20. package/dist/dts/utils/context-tokens.d.ts +100 -43
  21. package/dist/dts/utils/context-tokens.d.ts.map +1 -1
  22. package/dist/dts/utils/history-transform.d.ts +7 -1
  23. package/dist/dts/utils/history-transform.d.ts.map +1 -1
  24. package/dist/dts/utils/resolve-context-budget.d.ts +8 -5
  25. package/dist/dts/utils/resolve-context-budget.d.ts.map +1 -1
  26. package/dist/esm/components/chat-driver/chat-driver.js +22 -2
  27. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +92 -0
  28. package/dist/esm/config/define-stateful-agent.js +1 -0
  29. package/dist/esm/main/main.js +170 -27
  30. package/dist/esm/utils/context-tokens.js +331 -106
  31. package/dist/esm/utils/history-transform.js +5 -5
  32. package/dist/esm/utils/resolve-context-budget.js +37 -7
  33. package/package.json +16 -16
@@ -645,6 +645,39 @@ export declare interface AiDriver extends EventTarget {
645
645
  * retain the active config implement it.
646
646
  */
647
647
  getActiveAgent?(): AgentConfig | undefined;
648
+ /**
649
+ * Headroom the next turn could need, and which agent set that requirement
650
+ * (GENC-1582). `undefined` when no agent the turn could land on declares a
651
+ * `contextReserve`, which leaves the ambient default in force.
652
+ *
653
+ * The host cannot work this out for itself: it depends on the user pin, the
654
+ * flow-owner lock and the classifier's candidate set, none of which it can see.
655
+ * Reading the ACTIVE agent instead — which is what the gate did first — asks the
656
+ * wrong question, because the agent that last ran is not the agent about to run.
657
+ * Under automatic routing they are routinely different, and the turn that hands
658
+ * over is exactly the one that needs the room.
659
+ *
660
+ * Optional — only drivers that route implement it.
661
+ */
662
+ getContextReserve?(): ContextReserveResolution | undefined;
663
+ /**
664
+ * Token cost of the last request's preamble — system prompt, tool definitions
665
+ * and framed tail (GENC-1582). `undefined` before the first call of a turn.
666
+ *
667
+ * A live getter rather than a field stamped on each message, deliberately: every
668
+ * consumer wants the CURRENT preamble, never its history, and a message field
669
+ * would mean a type change in `@genesislcap/foundation-ai` plus churn in every
670
+ * persisted session for a value nobody reads twice.
671
+ *
672
+ * It exists because the preamble cannot be derived reliably. Inferring it as
673
+ * "first measured prompt minus the messages it covered" is accurate once and then
674
+ * goes stale, since each agent brings its own prompt and tool surface — and every
675
+ * attempt to compensate downstream ended up comparing stored characters against
676
+ * wire tokens. The driver is the only place that knows what it just sent.
677
+ *
678
+ * Optional — only drivers that build requests implement it.
679
+ */
680
+ getPreambleTokens?(): number | undefined;
648
681
  /**
649
682
  * Set the mid-loop context guard (GENC-1567). `undefined` disables it.
650
683
  *
@@ -1175,13 +1208,15 @@ declare interface BaseAgentConfig {
1175
1208
  * room it needs so that it does not have to be. An agent whose flow runs a long
1176
1209
  * tool loop, or returns large payloads, needs more than the global default.
1177
1210
  *
1178
- * **When it takes effect.** The reserve is read from the agent that is already
1179
- * ACTIVE, so it governs the gate from that agent's next send onwards — one turn
1180
- * later than a true pre-flight. It is deliberately not enforced at flow entry:
1181
- * `flow-owner-changed` fires while the driver is already routing the turn, so
1182
- * gating there would mean aborting a turn in flight. An agent that activates
1183
- * mid-turn therefore gets its first turn under the global reserve; the mid-loop
1184
- * context guard is what stops that turn running away.
1211
+ * **When it takes effect.** Before the send, as a real pre-flight (GENC-1582).
1212
+ * The driver resolves it against how routing is currently constrained — the
1213
+ * pinned or flow-owning agent when there is one, otherwise the largest reserve
1214
+ * among the agents the classifier could route to — so a turn is refused before
1215
+ * it can start rather than after. This used to read the agent that was already
1216
+ * ACTIVE, which is a different agent from the one about to run whenever the
1217
+ * orchestrator is doing its job, and the handover turn is exactly the one that
1218
+ * needs the room. The mid-loop context guard remains the backstop for a turn
1219
+ * that outgrows what it reserved.
1185
1220
  *
1186
1221
  * **Set this from measurement, not from a guess.** Per-turn `inputTokens` is
1187
1222
  * recorded for every call, so a single real run of the flow tells you its peak
@@ -1278,6 +1313,11 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
1278
1313
  */
1279
1314
  private interactionContexts;
1280
1315
  private systemPrompt?;
1316
+ /**
1317
+ * Preamble cost of the most recent request, set at the request-build site.
1318
+ * Read through {@link ChatDriver.getPreambleTokens}.
1319
+ */
1320
+ private lastPreambleTokens?;
1281
1321
  /**
1282
1322
  * Resolved tool definitions visible to the LLM. Folds mutate this in place
1283
1323
  * (push/pop on open/close). When `toolDefinitionsFactory` is set, this is
@@ -1950,6 +1990,8 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
1950
1990
  * nothing worth compacting or the default provider cannot summarize.
1951
1991
  */
1952
1992
  compact(options?: CompactionPlanOptions): Promise<ChatMessage | null>;
1993
+ /** {@inheritDoc AiDriver.getPreambleTokens} */
1994
+ getPreambleTokens(): number | undefined;
1953
1995
  isBusy(): boolean;
1954
1996
  /**
1955
1997
  * Wire a parent driver as the host for this driver's interactions. When set,
@@ -2240,6 +2282,12 @@ declare interface CompactionPlanOptions {
2240
2282
  tailTokenBudget?: number;
2241
2283
  /** Minimum reclaim for the plan to be considered worth running. */
2242
2284
  minReclaimTokens?: number;
2285
+ /**
2286
+ * Measured preamble cost, from `AiDriver.getPreambleTokens()` (GENC-1582).
2287
+ * Without it the projection sizes the post-compaction context as if the system
2288
+ * prompt and tool definitions were free, so it lands above the line it aimed at.
2289
+ */
2290
+ preambleTokens?: number;
2243
2291
  }
2244
2292
 
2245
2293
  /** What a compaction at a given cut would cost and reclaim. */
@@ -2310,21 +2358,87 @@ export declare interface ContextGuardPolicy {
2310
2358
  }
2311
2359
 
2312
2360
  /**
2313
- * Context headroom an agent reserves for itself, in tokens.
2361
+ * Context headroom an agent reserves for itself (GENC-1582).
2362
+ *
2363
+ * Two figures, because a turn has two costs and only one of them is permanent.
2364
+ *
2365
+ * @beta
2366
+ */
2367
+ export declare interface ContextReserve {
2368
+ /**
2369
+ * Tokens that must be free before one of this agent's turns may START — the
2370
+ * agent's PEAK growth, not its lasting cost.
2371
+ *
2372
+ * The peak is the binding figure even for an agent that sheds most of it again:
2373
+ * `condenseWhen` stubs a spent payload at the end of the turn, so the room has
2374
+ * to be there while the turn is running, before any of that fires.
2375
+ */
2376
+ tokens: number;
2377
+ /**
2378
+ * Extra tokens ABOVE {@link ContextReserve.tokens} at which the warning
2379
+ * appears. Omit to fall back to `chatConfig.context.warnMultiplier`.
2380
+ *
2381
+ * The figure that belongs here is what one of this agent's turns costs
2382
+ * PERMANENTLY — its growth measured after condensation, not at its peak. That
2383
+ * makes the warning land exactly two turns before the block: at the warning
2384
+ * line there is room for a full turn plus the residue it leaves, which is one
2385
+ * turn, and then room for one more.
2386
+ *
2387
+ * It exists because the multiplier cannot express that. `warnMultiplier` scales
2388
+ * the whole reserve, so an agent that peaks at 400k and keeps 30k gets a warning
2389
+ * 800k below the window — a line every one of its turns crosses transiently and
2390
+ * then clears, which is noise rather than a signal.
2391
+ */
2392
+ warnAheadTokens?: number;
2393
+ }
2394
+
2395
+ /**
2396
+ * Context headroom an agent reserves for itself: a token count, or a
2397
+ * {@link ContextReserve} when the agent also wants to say where the warning goes.
2314
2398
  *
2315
- * A plain number, unlike the value-or-resolver shape `provider` / `temperature` /
2316
- * `resumable` use. The resolver form was drafted for it and removed before
2317
- * release: the gate is recomputed from a synchronous getter with no turn context
2318
- * to resolve against, so no path could ever have invoked one. Accepting a
2319
- * type-correct option that is guaranteed to do nothing is worse than not
2320
- * offering it — a warning after the fact still leaves the configuration inert.
2399
+ * NOT the value-or-resolver shape `provider` / `temperature` / `resumable` use.
2400
+ * The resolver form was drafted and removed before release: the gate is recomputed
2401
+ * from a synchronous getter with no turn context to resolve against, so no path
2402
+ * could ever have invoked one. Accepting a type-correct option guaranteed to do
2403
+ * nothing is worse than not offering it.
2321
2404
  *
2322
- * It can widen to a resolver later without breaking a caller, since every
2323
- * existing `number` stays valid.
2405
+ * The object arm was added the way the plain form's docblock anticipated —
2406
+ * additively, so every existing `number` stays valid and means what it did.
2324
2407
  *
2325
2408
  * @beta
2326
2409
  */
2327
- export declare type ContextReserveInput = number;
2410
+ export declare type ContextReserveInput = number | ContextReserve;
2411
+
2412
+ /**
2413
+ * How much context headroom the next turn needs, and where that figure came from
2414
+ * (GENC-1582).
2415
+ *
2416
+ * @internal
2417
+ */
2418
+ declare interface ContextReserveResolution {
2419
+ /** Tokens to keep free for one turn — the winning agent's peak growth. */
2420
+ tokens: number;
2421
+ /**
2422
+ * Extra tokens above {@link ContextReserveResolution.tokens} at which the
2423
+ * warning appears, when the winning agent declared one. Travels WITH the winner
2424
+ * rather than being resolved separately: the two figures describe the same
2425
+ * agent's turn, and pairing a lean agent's warning with a hungry agent's block
2426
+ * would put the warning on the wrong side of it.
2427
+ */
2428
+ warnAheadTokens?: number;
2429
+ /** The agent whose `contextReserve` this is. */
2430
+ agentName: string;
2431
+ /**
2432
+ * Why this agent's reserve governs — which is what lets a blocked composer say
2433
+ * something TRUE about an agent the user has not invoked yet.
2434
+ *
2435
+ * `locked`: a user pin or a flow-owner lock means the next turn can only go to
2436
+ * this agent, so its reserve is the whole requirement. `reachable-max`: routing
2437
+ * is automatic, so the turn could land on any agent the classifier can pick, and
2438
+ * the hungriest of them is the one that has to fit.
2439
+ */
2440
+ basis: 'locked' | 'reachable-max';
2441
+ }
2328
2442
 
2329
2443
  /** A model used during a recorded cost session. */
2330
2444
  export declare interface CostSessionModelEntry {
@@ -2734,12 +2848,66 @@ export declare class FoundationAiAssistant extends GenesisElement {
2734
2848
  * redux actions re-renders without this; a store *swap* needs it.)
2735
2849
  */
2736
2850
  private _sessionRef?;
2851
+ /**
2852
+ * {@link FoundationAiAssistant.contextTokens} as of the last moment no turn was
2853
+ * running — the figure {@link FoundationAiAssistant.contextGate} is judged
2854
+ * against (GENC-1582).
2855
+ *
2856
+ * **Why the gate reads a held figure and the gauge does not.** They answer
2857
+ * different questions. The gauge is a meter: "how much context is in play right
2858
+ * now", and a build that momentarily fills 900k of a 1M window should say so.
2859
+ * The gate answers "may the next turn START", which is only meaningful when no
2860
+ * turn is running — the composer is disabled by `busy` throughout one anyway, so
2861
+ * a warning raised mid-turn is unactionable by construction.
2862
+ *
2863
+ * Reading the live figure meant the pill went amber part-way through every large
2864
+ * build and cleared itself at the end, because a `condenseWhen` agent's context
2865
+ * peaks mid-turn and is shed before the turn finishes. A signal that fires and
2866
+ * withdraws on its own each time teaches people to ignore it.
2867
+ *
2868
+ * **Held, not latched.** The distinction matters, because `contextGate` is
2869
+ * documented as derived-never-latched and the reason is a real dead end: a figure
2870
+ * pinned to a conversation that has since shrunk blocks the user out of the very
2871
+ * actions that would fix it. This holds only for the duration of a turn and is
2872
+ * refreshed the moment one settles, so no shrink can outlive it — a compaction
2873
+ * runs under `compacting` rather than `busy` and therefore updates it as it goes,
2874
+ * and Clear drops it outright. What it costs is one turn of lag: a turn that
2875
+ * balloons the resting size does not warn until it ends, which is the same trade
2876
+ * the flow-owner soften already makes deliberately.
2877
+ *
2878
+ * Unset falls back to the live figure, which is correct wherever there is no held
2879
+ * value yet — first paint, and a remount, both of which happen at rest.
2880
+ */
2881
+ private _gateContextTokens?;
2737
2882
  get messages(): ChatMessage[];
2738
2883
  set messages(value: ChatMessage[]);
2739
2884
  get state(): AiAssistantState;
2740
2885
  set state(value: AiAssistantState);
2741
2886
  /** Whether a state value counts as "busy" — a turn is active. */
2742
2887
  private isBusyState;
2888
+ /**
2889
+ * Re-arm {@link FoundationAiAssistant.contextGate} against the size the turn
2890
+ * just left behind (GENC-1582).
2891
+ *
2892
+ * The final call of a turn is made AFTER `agentEnd` condensation fires, so by
2893
+ * the time a turn settles `contextTokens` is the resting figure rather than the
2894
+ * mid-turn peak — which is the number the reserve's thresholds are drawn
2895
+ * against.
2896
+ *
2897
+ * **Called from BOTH settle paths, and it has to be.** A turn does not end
2898
+ * through the `state` setter: `send()`'s `finally` writes `setState('idle')`
2899
+ * straight to the captured session ref, because the element may have
2900
+ * disconnected mid-turn, and that block then replicates the parts of the
2901
+ * setter's settle that matter. Arming the gate inline in the setter therefore
2902
+ * armed it on the one path a real turn never takes — the gate silently kept
2903
+ * falling back to the live figure and the hold did nothing at all. Found by
2904
+ * driving a real UI Builder turn and reading `_gateContextTokens`, which was
2905
+ * `undefined` from start to finish; no unit test noticed, because the test
2906
+ * harness sets `state` through the property.
2907
+ *
2908
+ * One method rather than two assignments so the two paths cannot drift again.
2909
+ */
2910
+ private rearmContextGate;
2743
2911
  /**
2744
2912
  * True while a turn is active — `loading` or `cancelling` (a stop was
2745
2913
  * requested but the turn is still winding down). The single "is a turn
@@ -4053,11 +4221,11 @@ export declare class FoundationAiAssistant extends GenesisElement {
4053
4221
  * Context-headroom thresholds for the active model (GENC-1567) — the warning
4054
4222
  * and block lines, plus the tail budget compaction aims at.
4055
4223
  *
4056
- * The agent's own `contextReserve` is deliberately NOT folded in here. This is
4057
- * the AMBIENT budget, which protects any turn from any agent; a per-agent
4058
- * reserve is an additional, larger requirement checked when that agent takes
4059
- * ownership of a flow, where there is a turn context to resolve its function
4060
- * form against.
4224
+ * The per-agent `contextReserve` IS folded in, via the driver (GENC-1582). It
4225
+ * used to be described here as a separate, later check made when an agent took
4226
+ * ownership of a flow — but there is no such check, and by flow entry the send
4227
+ * has already been allowed. Resolving it here, before the send, is what makes it
4228
+ * a gate rather than a label.
4061
4229
  *
4062
4230
  * Overhead is read off `messages` (the Redux mirror) rather than the driver's
4063
4231
  * own history, which the compaction plan uses. That is safe rather than sloppy:
@@ -4067,22 +4235,26 @@ export declare class FoundationAiAssistant extends GenesisElement {
4067
4235
  */
4068
4236
  get contextBudget(): ResolvedContextBudget;
4069
4237
  /**
4070
- * The active agent's declared `contextReserve`, when it is a plain number.
4238
+ * Headroom the next turn needs, resolved against how routing is currently
4239
+ * constrained (GENC-1582), and which agent set it.
4240
+ *
4241
+ * The driver owns the answer because the driver owns routing: whether a pin or a
4242
+ * flow-owner lock is in force, which agents the classifier can reach, and which
4243
+ * one is active. This used to read `getActiveAgent()?.contextReserve` — the agent
4244
+ * that LAST ran — which is a different agent from the one about to run whenever
4245
+ * the orchestrator is doing its job, and the handover turn is precisely the one
4246
+ * that needs the room.
4071
4247
  *
4072
- * Read off the LIVE active agent rather than checked at flow entry, which is
4073
- * where the design first put it. `flow-owner-changed` turned out to fire while
4074
- * the driver is already routing the turn — after the send was allowed — so it
4075
- * is not a pre-flight hook, and gating there would mean aborting a turn in
4076
- * flight. Reading the active agent instead means a hungry agent's reserve
4077
- * governs the gate from its next send onwards: one turn later than a true
4078
- * pre-flight, but synchronous, and it never has to interrupt anything.
4248
+ * Falls back to the active agent for a host-supplied driver that does not
4249
+ * implement the resolution, so an existing single-agent driver keeps the old
4250
+ * behaviour rather than silently losing its reserve.
4079
4251
  *
4080
4252
  * `contextReserve` is a plain number by design — see `ContextReserveInput` for
4081
4253
  * why a resolver form was dropped rather than accepted and ignored.
4082
4254
  *
4083
4255
  * @internal
4084
4256
  */
4085
- private get activeAgentReserveTokens();
4257
+ private get contextReserve();
4086
4258
  /**
4087
4259
  * What compacting right now would reclaim, or `null` when it would not be
4088
4260
  * worth it (GENC-1567). Sized against {@link FoundationAiAssistant.contextBudget},
@@ -4122,6 +4294,36 @@ export declare class FoundationAiAssistant extends GenesisElement {
4122
4294
  * and gating on the way out is the lesser harm; the headroom reserve exists so
4123
4295
  * this is rare rather than routine.
4124
4296
  */
4297
+ /**
4298
+ * WHY the composer is blocked, when it is (GENC-1582). `undefined` unless
4299
+ * {@link FoundationAiAssistant.contextGate} is `'blocked'`.
4300
+ *
4301
+ * A cause rather than a fourth gate level: the severity is identical — sends are
4302
+ * refused either way — and every consumer switches on the three levels. What
4303
+ * differs is what the user can do about it, which is worth splitting because the
4304
+ * two answers have no remedy in common.
4305
+ *
4306
+ * - `'transcript'` — the conversation has grown into the reserve. Compacting or
4307
+ * clearing helps, and the copy says so.
4308
+ * - `'model'` — the active agent's reserve meets or exceeds the whole window, so
4309
+ * `blockAt` is 0 and EVERY message is refused, including the first of an empty
4310
+ * chat. Neither compacting nor clearing can help: the constraint is the model,
4311
+ * not the transcript. Before this existed the user was told to clear the chat,
4312
+ * did, and hit the identical block with no way to understand why.
4313
+ *
4314
+ * Keyed off `blockAt === 0`, deliberately NOT off whether a compaction plan
4315
+ * exists. A null plan has two unrelated causes — nothing left worth summarizing,
4316
+ * versus a window that cannot host the agent at all — and conflating them is what
4317
+ * produced the misleading copy.
4318
+ *
4319
+ * Note the block itself is correct in both cases and must stay: an agent that
4320
+ * measurably cannot complete a turn in this window would otherwise start one and
4321
+ * die mid-loop on a provider context error, with partial writes and no clean
4322
+ * exit. Refusing up front is the better failure.
4323
+ *
4324
+ * @internal
4325
+ */
4326
+ get contextBlockCause(): 'transcript' | 'model' | undefined;
4125
4327
  get contextGate(): 'ok' | 'warn' | 'blocked';
4126
4328
  /**
4127
4329
  * What the composer notice says about the context gate.
@@ -5008,6 +5210,8 @@ export declare class OrchestratingDriver extends EventTarget implements AiDriver
5008
5210
  compact(options?: CompactionPlanOptions): Promise<ChatMessage | null>;
5009
5211
  /** {@inheritDoc AiDriver.canCompact} */
5010
5212
  canCompact(options?: CompactionPlanOptions): boolean;
5213
+ /** {@inheritDoc AiDriver.getPreambleTokens} */
5214
+ getPreambleTokens(): number | undefined;
5011
5215
  /** {@inheritDoc AiDriver.getCompactionPlan} */
5012
5216
  getCompactionPlan(options?: CompactionPlanOptions): CompactionProjection | null;
5013
5217
  /** Delegates to the inner {@link ChatDriver} — turns are captured there. */
@@ -5020,6 +5224,12 @@ export declare class OrchestratingDriver extends EventTarget implements AiDriver
5020
5224
  continueFromHistory(transientPrimer?: ChatMessage[]): Promise<ChatDriverResult>;
5021
5225
  /** {@inheritDoc AiDriver.primeRestoredAgentState} */
5022
5226
  primeRestoredAgentState(snapshots: Record<string, unknown>): void;
5227
+ /**
5228
+ * {@inheritDoc AiDriver.getContextReserve}
5229
+ *
5230
+ * Two cases, because routing has two modes and they need opposite answers.
5231
+ */
5232
+ getContextReserve(): ContextReserveResolution | undefined;
5023
5233
  /** {@inheritDoc AiDriver.getActiveAgent} */
5024
5234
  getActiveAgent(): AgentConfig | undefined;
5025
5235
  private applyAgent;
@@ -5745,6 +5955,21 @@ export declare interface StatefulAgentInit<S> {
5745
5955
  * (fail-safe — never drop a resumable chat on a predicate bug).
5746
5956
  */
5747
5957
  resumable?: (ctx: StatefulAgentPersistenceContext<S>) => boolean | Promise<boolean>;
5958
+ /**
5959
+ * Tokens of context headroom to keep free for one of this agent's turns —
5960
+ * see `AgentConfig.contextReserve`, which this is passed straight through to.
5961
+ *
5962
+ * The third member of the flow-safety group with {@link StatefulAgentInit.resumable}
5963
+ * and {@link StatefulAgentInit.notResumableMessage}, and the one that was missing
5964
+ * (GENC-1582): a stateful flow is the case the reserve was designed for — it is
5965
+ * what runs the long tool loop — and yet the helper for building one could not
5966
+ * express it. Both agents that needed it in the field are built here.
5967
+ *
5968
+ * Set it from measurement. Per-turn `inputTokens` is recorded for every call, so
5969
+ * one real run of the flow gives you its peak growth; leaving it unset inherits
5970
+ * the global default, which is the right starting position.
5971
+ */
5972
+ contextReserve?: ContextReserveInput;
5748
5973
  /**
5749
5974
  * Message shown on return when {@link StatefulAgentInit.resumable} returned
5750
5975
  * `false`. Falls back to a built-in default when omitted.
@@ -3858,6 +3858,21 @@ function createInteractionContext(interactionId) {
3858
3858
  var APPROX_CHARS_PER_TOKEN = 4;
3859
3859
  var IMAGE_TOKEN_ESTIMATE = 1600;
3860
3860
  var PER_MESSAGE_FRAMING_TOKENS = 4;
3861
+ var PREAMBLE_PROSE_CHARS_PER_TOKEN = 4;
3862
+ var PREAMBLE_JSON_CHARS_PER_TOKEN = 3;
3863
+ var PER_TOOL_FRAMING_TOKENS = 8;
3864
+ function estimatePreambleTokens(input) {
3865
+ const prose = (input.systemPrompt?.length ?? 0) + (input.tailContext?.length ?? 0);
3866
+ let json = 0;
3867
+ for (const def of input.toolDefinitions ?? []) {
3868
+ try {
3869
+ json += JSON.stringify(def).length;
3870
+ } catch {
3871
+ json += def.name?.length ?? 0;
3872
+ }
3873
+ }
3874
+ return Math.ceil(prose / PREAMBLE_PROSE_CHARS_PER_TOKEN) + Math.ceil(json / PREAMBLE_JSON_CHARS_PER_TOKEN) + (input.toolDefinitions?.length ?? 0) * PER_TOOL_FRAMING_TOKENS;
3875
+ }
3861
3876
  var COMPACTION_SUMMARY_TOKEN_ESTIMATE = 1200;
3862
3877
  function reachesProvider(m) {
3863
3878
  if (m.role === "system-event" || m.role === "synthetic-user") return false;
@@ -3902,66 +3917,66 @@ function usableAnchor(m, compactedAt) {
3902
3917
  function firstAnchor(history, compactedAt) {
3903
3918
  return history.findIndex((m) => usableAnchor(m, compactedAt));
3904
3919
  }
3905
- function estimateMessageTokens(history) {
3906
- const estimates = history.map(baselineTokens);
3907
- const compactedAt = compactedAtOf(history);
3908
- const anchors = [];
3909
- history.forEach((m, i) => {
3910
- if (usableAnchor(m, compactedAt)) anchors.push(i);
3911
- });
3912
- for (let a = 0; a < anchors.length - 1; a += 1) {
3913
- const from = anchors[a];
3914
- const to = anchors[a + 1];
3915
- const measured = history[to].inputTokens - history[from].inputTokens;
3916
- if (measured <= 0) continue;
3917
- let baseline = 0;
3918
- for (let i = from; i < to; i += 1) baseline += estimates[i];
3919
- if (baseline <= 0) {
3920
- estimates[from] = measured;
3921
- continue;
3922
- }
3923
- const scale = measured / baseline;
3924
- let assigned = 0;
3925
- for (let i = from; i < to - 1; i += 1) {
3926
- estimates[i] = Math.round(estimates[i] * scale);
3927
- assigned += estimates[i];
3928
- }
3929
- estimates[to - 1] = measured - assigned;
3920
+ function lastAnchor(history, compactedAt) {
3921
+ for (let i = history.length - 1; i >= 0; i -= 1) {
3922
+ if (usableAnchor(history[i], compactedAt)) return i;
3930
3923
  }
3931
- return estimates;
3924
+ return -1;
3932
3925
  }
3933
- function estimateSystemOverhead(history) {
3934
- const first = firstAnchor(history, compactedAtOf(history));
3935
- if (first < 0) return 0;
3936
- const estimates = estimateMessageTokens(history);
3937
- let messages = 0;
3938
- for (let i = 0; i < first; i += 1) messages += estimates[i];
3939
- return Math.max(0, history[first].inputTokens - messages);
3940
- }
3941
- function estimateContextTokens(history) {
3942
- const estimates = estimateMessageTokens(history);
3943
- let total = estimateSystemOverhead(history);
3944
- for (const t of estimates) total += t;
3945
- return total;
3946
- }
3947
- function estimateRequestTokens(storedHistory, requestHistory) {
3948
- const overhead = estimateSystemOverhead(storedHistory);
3949
- const calibratedMessages = Math.max(0, estimateContextTokens(storedHistory) - overhead);
3950
- let storedBaseline = 0;
3951
- for (const m of storedHistory) storedBaseline += baselineTokens(m);
3926
+ function calibrate(history, preambleTokens) {
3927
+ const baselines = history.map(baselineTokens);
3928
+ let baselineTotal = 0;
3929
+ for (const b of baselines) baselineTotal += b;
3930
+ const compactedAt = compactedAtOf(history);
3931
+ const first = firstAnchor(history, compactedAt);
3932
+ const last = lastAnchor(history, compactedAt);
3933
+ if (first < 0 || last < 0) {
3934
+ const measured = Math.max(0, preambleTokens ?? 0);
3935
+ return {
3936
+ overhead: measured,
3937
+ total: baselineTotal + measured,
3938
+ estimates: baselines,
3939
+ scale: 1
3940
+ };
3941
+ }
3942
+ let sinceLast = 0;
3943
+ for (let i = last; i < baselines.length; i += 1) sinceLast += baselines[i];
3944
+ const total = history[last].inputTokens + sinceLast;
3945
+ let beforeFirst = 0;
3946
+ for (let i = 0; i < first; i += 1) beforeFirst += baselines[i];
3947
+ const derived = Math.max(0, history[first].inputTokens - beforeFirst);
3948
+ const claimed = preambleTokens ?? derived;
3949
+ const contradicted = baselineTotal > 0 && claimed >= total;
3950
+ const overhead = Math.min(contradicted ? derived : claimed, total);
3951
+ const messages = Math.max(0, total - overhead);
3952
+ if (baselineTotal <= 0) {
3953
+ return { overhead, total, estimates: baselines, scale: 1 };
3954
+ }
3955
+ const scale = messages / baselineTotal;
3956
+ const estimates = baselines.map((b) => Math.round(b * scale));
3957
+ let assigned = 0;
3958
+ let heaviest = -1;
3959
+ for (let i = 0; i < estimates.length; i += 1) {
3960
+ assigned += estimates[i];
3961
+ if (baselines[i] > 0 && (heaviest < 0 || baselines[i] > baselines[heaviest])) heaviest = i;
3962
+ }
3963
+ if (heaviest >= 0) estimates[heaviest] += messages - assigned;
3964
+ return { overhead, total, estimates, scale };
3965
+ }
3966
+ function estimateMessageTokens(history, preambleTokens) {
3967
+ return calibrate(history, preambleTokens).estimates;
3968
+ }
3969
+ function estimateRequestTokens(storedHistory, requestHistory, preambleTokens) {
3970
+ const { overhead } = calibrate(storedHistory, preambleTokens);
3952
3971
  let requestBaseline = 0;
3953
3972
  for (const m of requestHistory) requestBaseline += baselineTokens(m);
3954
- if (storedBaseline <= 0) return overhead + requestBaseline;
3955
- return Math.round(overhead + calibratedMessages * (requestBaseline / storedBaseline));
3973
+ return Math.round(overhead + requestBaseline);
3956
3974
  }
3957
- function projectCompaction(history, cut, summaryTokens = COMPACTION_SUMMARY_TOKEN_ESTIMATE) {
3958
- const estimates = estimateMessageTokens(history);
3959
- const overhead = estimateSystemOverhead(history);
3975
+ function projectCompaction(history, cut, summaryTokens = COMPACTION_SUMMARY_TOKEN_ESTIMATE, preambleTokens) {
3976
+ const { estimates, overhead, total } = calibrate(history, preambleTokens);
3960
3977
  let tail = 0;
3961
3978
  for (let i = cut; i < estimates.length; i += 1) tail += estimates[i];
3962
- let all = 0;
3963
- for (const t of estimates) all += t;
3964
- const tokensBefore = overhead + all;
3979
+ const tokensBefore = total;
3965
3980
  const tokensAfter = overhead + summaryTokens + tail;
3966
3981
  return {
3967
3982
  cut,
@@ -4229,8 +4244,8 @@ function isSafeCompactionCut(history, i) {
4229
4244
  }
4230
4245
  return true;
4231
4246
  }
4232
- function findCompactionCut(history, tailTokenBudget = DEFAULT_TAIL_TOKEN_BUDGET) {
4233
- const estimates = estimateMessageTokens(history);
4247
+ function findCompactionCut(history, tailTokenBudget = DEFAULT_TAIL_TOKEN_BUDGET, preambleTokens) {
4248
+ const estimates = estimateMessageTokens(history, preambleTokens);
4234
4249
  let messagesTotal = 0;
4235
4250
  for (const t of estimates) messagesTotal += t;
4236
4251
  if (messagesTotal <= tailTokenBudget) return null;
@@ -4251,9 +4266,14 @@ function planCompaction(history, options = {}) {
4251
4266
  tailTokenBudget = DEFAULT_TAIL_TOKEN_BUDGET,
4252
4267
  minReclaimTokens = DEFAULT_MIN_RECLAIM_TOKENS
4253
4268
  } = options;
4254
- const cut = findCompactionCut(history, tailTokenBudget);
4269
+ const cut = findCompactionCut(history, tailTokenBudget, options.preambleTokens);
4255
4270
  if (cut == null) return null;
4256
- const projection = projectCompaction(history, cut);
4271
+ const projection = projectCompaction(
4272
+ history,
4273
+ cut,
4274
+ COMPACTION_SUMMARY_TOKEN_ESTIMATE,
4275
+ options.preambleTokens
4276
+ );
4257
4277
  return projection.reclaimed >= minReclaimTokens ? projection : null;
4258
4278
  }
4259
4279
  function renderMessageForSummary(m) {
@@ -5217,7 +5237,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
5217
5237
  attachments: pendingInput.attachments
5218
5238
  }
5219
5239
  ] : requestHistory;
5220
- const estimated = estimateRequestTokens(this.history, request);
5240
+ const estimated = estimateRequestTokens(this.history, request, this.lastPreambleTokens);
5221
5241
  if (estimated < threshold) return void 0;
5222
5242
  logger2.error(
5223
5243
  `ChatDriver: ending the turn \u2014 the request would fill the context window (~${estimated} of ${limit})`
@@ -5405,6 +5425,10 @@ Output format (strict):
5405
5425
  this.loadHistory([summaryMessage, ...tail]);
5406
5426
  return summaryMessage;
5407
5427
  }
5428
+ /** {@inheritDoc AiDriver.getPreambleTokens} */
5429
+ getPreambleTokens() {
5430
+ return this.lastPreambleTokens;
5431
+ }
5408
5432
  isBusy() {
5409
5433
  return this.busy;
5410
5434
  }
@@ -6267,6 +6291,11 @@ ${tailBody}
6267
6291
  const attachmentsForCall = currentAttachments;
6268
6292
  currentInput = "";
6269
6293
  currentAttachments = void 0;
6294
+ this.lastPreambleTokens = estimatePreambleTokens({
6295
+ systemPrompt,
6296
+ toolDefinitions: this.toolDefinitions,
6297
+ tailContext
6298
+ });
6270
6299
  const options = {
6271
6300
  systemPrompt,
6272
6301
  // Strip fold-only properties (foldEvent, foldPath) before sending to provider
@@ -6936,6 +6965,13 @@ function isSpecialist(agent) {
6936
6965
  function isFallback(agent) {
6937
6966
  return agent.fallback === true;
6938
6967
  }
6968
+ function normalizeContextReserve(input) {
6969
+ if (input == null) return void 0;
6970
+ const positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
6971
+ if (typeof input === "number") return positive(input) ? { tokens: input } : void 0;
6972
+ if (!positive(input.tokens)) return void 0;
6973
+ return positive(input.warnAheadTokens) ? { tokens: input.tokens, warnAheadTokens: input.warnAheadTokens } : { tokens: input.tokens };
6974
+ }
6939
6975
  var UNROUTABLE_PLACEHOLDER = "{{unroutable}}";
6940
6976
  var UNROUTABLE_INSTRUCTION = `When the request is substantive but nothing above covers it, say so directly. Do not offer to do it yourself, and do not imply a specialist will pick it up.
6941
6977
 
@@ -7137,6 +7173,10 @@ var OrchestratingDriver = class extends EventTarget {
7137
7173
  canCompact(options) {
7138
7174
  return this.chatDriver.canCompact(options);
7139
7175
  }
7176
+ /** {@inheritDoc AiDriver.getPreambleTokens} */
7177
+ getPreambleTokens() {
7178
+ return this.chatDriver.getPreambleTokens();
7179
+ }
7140
7180
  /** {@inheritDoc AiDriver.getCompactionPlan} */
7141
7181
  getCompactionPlan(options) {
7142
7182
  return this.chatDriver.getCompactionPlan(options);
@@ -7258,6 +7298,32 @@ var OrchestratingDriver = class extends EventTarget {
7258
7298
  primeRestoredAgentState(snapshots) {
7259
7299
  this.restoredAgentStates = { ...snapshots };
7260
7300
  }
7301
+ /**
7302
+ * {@inheritDoc AiDriver.getContextReserve}
7303
+ *
7304
+ * Two cases, because routing has two modes and they need opposite answers.
7305
+ */
7306
+ getContextReserve() {
7307
+ const locked = this.resolvePinnedAgent();
7308
+ if (locked) {
7309
+ const reserve = normalizeContextReserve(locked.contextReserve);
7310
+ return reserve == null ? void 0 : { ...reserve, agentName: locked.name, basis: "locked" };
7311
+ }
7312
+ const candidates = [...this.specialists];
7313
+ if (this.fallback) candidates.push(this.fallback);
7314
+ if (this.activeAgent) candidates.push(this.activeAgent);
7315
+ let winner;
7316
+ let agentName = "";
7317
+ for (const agent of candidates) {
7318
+ const reserve = normalizeContextReserve(agent.contextReserve);
7319
+ if (!reserve) continue;
7320
+ if (!winner || reserve.tokens > winner.tokens) {
7321
+ winner = reserve;
7322
+ agentName = agent.name;
7323
+ }
7324
+ }
7325
+ return winner ? { ...winner, agentName, basis: "reachable-max" } : void 0;
7326
+ }
7261
7327
  /** {@inheritDoc AiDriver.getActiveAgent} */
7262
7328
  getActiveAgent() {
7263
7329
  return this.activeAgent;
@@ -7767,6 +7833,7 @@ function defineStatefulAgent(opts) {
7767
7833
  onUnresolvedTool: wrappedOnUnresolvedTool,
7768
7834
  resumable: wrappedResumable,
7769
7835
  notResumableMessage: opts.notResumableMessage,
7836
+ contextReserve: opts.contextReserve,
7770
7837
  onActivate: async (ctx) => {
7771
7838
  state = await opts.init(ctx);
7772
7839
  if (opts.toolHandlers) {