@animalabs/agent-framework 0.6.4 → 0.6.6

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 (85) hide show
  1. package/README.md +5 -0
  2. package/dist/src/agent.d.ts +18 -1
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +221 -2
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/framework.d.ts +59 -2
  7. package/dist/src/framework.d.ts.map +1 -1
  8. package/dist/src/framework.js +687 -229
  9. package/dist/src/framework.js.map +1 -1
  10. package/dist/src/gate/event-gate.d.ts.map +1 -1
  11. package/dist/src/gate/event-gate.js +27 -4
  12. package/dist/src/gate/event-gate.js.map +1 -1
  13. package/dist/src/mcpl/channel-registry.d.ts +34 -15
  14. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  15. package/dist/src/mcpl/channel-registry.js +299 -67
  16. package/dist/src/mcpl/channel-registry.js.map +1 -1
  17. package/dist/src/mcpl/push-handler.d.ts +1 -0
  18. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  19. package/dist/src/mcpl/push-handler.js +1 -0
  20. package/dist/src/mcpl/push-handler.js.map +1 -1
  21. package/dist/src/mcpl/server-connection.d.ts +3 -1
  22. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  23. package/dist/src/mcpl/server-connection.js +4 -0
  24. package/dist/src/mcpl/server-connection.js.map +1 -1
  25. package/dist/src/mcpl/types.d.ts +43 -12
  26. package/dist/src/mcpl/types.d.ts.map +1 -1
  27. package/dist/src/mcpl/types.js +1 -0
  28. package/dist/src/mcpl/types.js.map +1 -1
  29. package/dist/src/types/agent.d.ts +15 -0
  30. package/dist/src/types/agent.d.ts.map +1 -1
  31. package/dist/src/types/events.d.ts +1 -0
  32. package/dist/src/types/events.d.ts.map +1 -1
  33. package/dist/src/types/framework.d.ts +9 -0
  34. package/dist/src/types/framework.d.ts.map +1 -1
  35. package/dist/src/types/trace.d.ts +2 -0
  36. package/dist/src/types/trace.d.ts.map +1 -1
  37. package/dist/test/activation-request-sanitize.test.d.ts +2 -0
  38. package/dist/test/activation-request-sanitize.test.d.ts.map +1 -0
  39. package/dist/test/activation-request-sanitize.test.js +89 -0
  40. package/dist/test/activation-request-sanitize.test.js.map +1 -0
  41. package/dist/test/agent-runtime-settings.test.d.ts +2 -0
  42. package/dist/test/agent-runtime-settings.test.d.ts.map +1 -0
  43. package/dist/test/agent-runtime-settings.test.js +120 -0
  44. package/dist/test/agent-runtime-settings.test.js.map +1 -0
  45. package/dist/test/channel-registry-routing.test.js +5 -4
  46. package/dist/test/channel-registry-routing.test.js.map +1 -1
  47. package/dist/test/conversation-fork-injection-scoping.test.js +12 -1
  48. package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
  49. package/dist/test/ephemeral-settle.test.d.ts +2 -0
  50. package/dist/test/ephemeral-settle.test.d.ts.map +1 -0
  51. package/dist/test/ephemeral-settle.test.js +351 -0
  52. package/dist/test/ephemeral-settle.test.js.map +1 -0
  53. package/dist/test/event-gate.test.js +21 -1
  54. package/dist/test/event-gate.test.js.map +1 -1
  55. package/dist/test/helpers/mock-membrane.d.ts +13 -1
  56. package/dist/test/helpers/mock-membrane.d.ts.map +1 -1
  57. package/dist/test/helpers/mock-membrane.js +8 -1
  58. package/dist/test/helpers/mock-membrane.js.map +1 -1
  59. package/dist/test/maintenance-timer.test.js +52 -1
  60. package/dist/test/maintenance-timer.test.js.map +1 -1
  61. package/dist/test/mcpl-gate-roundtrip.test.js +138 -6
  62. package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
  63. package/dist/test/overbudget-drain-breaker.test.d.ts +2 -0
  64. package/dist/test/overbudget-drain-breaker.test.d.ts.map +1 -0
  65. package/dist/test/overbudget-drain-breaker.test.js +157 -0
  66. package/dist/test/overbudget-drain-breaker.test.js.map +1 -0
  67. package/dist/test/present-while-acting.test.d.ts +18 -0
  68. package/dist/test/present-while-acting.test.d.ts.map +1 -0
  69. package/dist/test/present-while-acting.test.js +267 -0
  70. package/dist/test/present-while-acting.test.js.map +1 -0
  71. package/dist/test/trunk-channel-routing.test.js +38 -0
  72. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  73. package/dist/tsconfig.tsbuildinfo +1 -1
  74. package/package.json +3 -3
  75. package/src/agent.ts +244 -3
  76. package/src/framework.ts +763 -235
  77. package/src/gate/event-gate.ts +38 -4
  78. package/src/mcpl/channel-registry.ts +372 -76
  79. package/src/mcpl/push-handler.ts +2 -0
  80. package/src/mcpl/server-connection.ts +10 -0
  81. package/src/mcpl/types.ts +49 -12
  82. package/src/types/agent.ts +18 -0
  83. package/src/types/events.ts +1 -1
  84. package/src/types/framework.ts +10 -0
  85. package/src/types/trace.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animalabs/agent-framework",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Multi-agent framework with pluggable modules, persistent state, and concurrent tool execution",
5
5
  "repository": {
6
6
  "type": "git",
@@ -44,8 +44,8 @@
44
44
  "license": "MIT",
45
45
  "dependencies": {
46
46
  "@animalabs/chronicle": "^0.2.2",
47
- "@animalabs/context-manager": "^0.5.5",
48
- "@animalabs/membrane": "^0.5.64",
47
+ "@animalabs/context-manager": "^0.5.11",
48
+ "@animalabs/membrane": "^0.5.73",
49
49
  "chokidar": "^4.0.3",
50
50
  "discord.js": "^14.25.1",
51
51
  "ws": "^8.18.0"
package/src/agent.ts CHANGED
@@ -6,7 +6,14 @@ export interface StartStreamResult {
6
6
  stream: YieldingStream;
7
7
  request: NormalizedRequest;
8
8
  }
9
- import type { ContextManager, TokenBudget, ContextInjection, CompileResult } from '@animalabs/context-manager';
9
+ import type {
10
+ ContextManager,
11
+ TokenBudget,
12
+ ContextInjection,
13
+ CompileResult,
14
+ HotContextSettingsStatus,
15
+ HotContextSettingsUpdate,
16
+ } from '@animalabs/context-manager';
10
17
  import type {
11
18
  AgentConfig,
12
19
  AgentState,
@@ -19,8 +26,14 @@ import type {
19
26
  AgentInfo,
20
27
  InferenceResult,
21
28
  InferenceOptions,
29
+ AgentRuntimeSettingsPatch,
30
+ AgentRuntimeSettingsSnapshot,
31
+ AgentRuntimeSettingsOverrides,
22
32
  } from './types/index.js';
23
33
 
34
+ const DEFAULT_CONTEXT_BUDGET_TOKENS = 100_000;
35
+ const DEFAULT_TRANSITION_PACE_TOKENS = 16_000;
36
+
24
37
  /**
25
38
  * An agent wraps a context manager and manages inference state.
26
39
  */
@@ -60,6 +73,11 @@ export class Agent {
60
73
  * ContextManager's built-in default applies. reserveForResponse uses
61
74
  * this agent's maxTokens. */
62
75
  contextBudgetTokens?: number;
76
+ private readonly configuredContextBudgetTokens?: number;
77
+ private readonly configuredTailTokens?: number;
78
+ private readonly configuredTransitionPaceTokens?: number;
79
+ private contextBudgetTargetTokens?: number;
80
+ private runtimeSettingsOverrides: AgentRuntimeSettingsOverrides = {};
63
81
  private contextManager: ContextManager;
64
82
  private membrane: Membrane;
65
83
 
@@ -81,8 +99,12 @@ export class Agent {
81
99
  this.providerParams = config.providerParams;
82
100
  this.maxStreamTokens = config.maxStreamTokens ?? 150_000;
83
101
  this.contextBudgetTokens = config.contextBudgetTokens;
102
+ this.configuredContextBudgetTokens = config.contextBudgetTokens;
84
103
  this.contextManager = contextManager;
85
104
  this.membrane = membrane;
105
+ const hot = this.getHotContextSettings();
106
+ this.configuredTailTokens = hot?.tailTokens;
107
+ this.configuredTransitionPaceTokens = hot?.transitionPaceTokens;
86
108
  }
87
109
 
88
110
  /**
@@ -148,8 +170,203 @@ export class Agent {
148
170
  return { maxTokens: this.contextBudgetTokens, reserveForResponse: this.maxTokens };
149
171
  }
150
172
 
173
+ /** Structural compatibility keeps lightweight test/host ContextManager
174
+ * doubles working while making the new capability optional at runtime. */
175
+ private getHotContextSettings(): HotContextSettingsStatus | null {
176
+ const manager = this.contextManager as ContextManager & {
177
+ getHotContextSettings?: () => HotContextSettingsStatus | null;
178
+ };
179
+ return typeof manager.getHotContextSettings === 'function'
180
+ ? manager.getHotContextSettings()
181
+ : null;
182
+ }
183
+
184
+ private updateHotContextSettings(update: HotContextSettingsUpdate): HotContextSettingsStatus {
185
+ const manager = this.contextManager as ContextManager & {
186
+ updateHotContextSettings?: (value: HotContextSettingsUpdate) => HotContextSettingsStatus;
187
+ };
188
+ if (typeof manager.updateHotContextSettings !== 'function') {
189
+ throw new Error('The active context strategy does not support live context settings');
190
+ }
191
+ return manager.updateHotContextSettings(update);
192
+ }
193
+
194
+ getRuntimeSettings(): AgentRuntimeSettingsSnapshot {
195
+ const hot = this.getHotContextSettings();
196
+ return {
197
+ contextBudgetTokens:
198
+ this.contextBudgetTargetTokens ??
199
+ this.contextBudgetTokens ??
200
+ DEFAULT_CONTEXT_BUDGET_TOKENS,
201
+ ...(hot ? { tailTokens: hot.tailTokens } : {}),
202
+ ...(hot?.transitionPaceTokens !== undefined
203
+ ? { transitionPaceTokens: hot.transitionPaceTokens }
204
+ : {}),
205
+ transition: this.contextBudgetTargetTokens === undefined
206
+ ? 'stable'
207
+ : hot?.blocked
208
+ ? 'blocked'
209
+ : 'converging',
210
+ ...(hot?.blocked === 'transition-pace-floor'
211
+ ? { transitionReason: 'transition_pace_too_small' as const }
212
+ : hot?.blocked === 'prepared-window-floor'
213
+ ? { transitionReason: 'protected_context_exceeds_target' as const }
214
+ : {}),
215
+ };
216
+ }
217
+
218
+ getRuntimeSettingsOverrides(): AgentRuntimeSettingsOverrides {
219
+ return { ...this.runtimeSettingsOverrides };
220
+ }
221
+
222
+ updateRuntimeSettings(patch: AgentRuntimeSettingsPatch): AgentRuntimeSettingsSnapshot {
223
+ this.validateRuntimeSettingsPatch(patch);
224
+ let nextContextBudget = this.contextBudgetTokens;
225
+ let nextContextTarget = this.contextBudgetTargetTokens;
226
+ let appliedDefaultPace = false;
227
+ const hotPatch: {
228
+ tailTokens?: number;
229
+ transitionPaceTokens?: number | null;
230
+ preparedWindowTokens?: number | null;
231
+ } = {};
232
+ if (patch.tailTokens !== undefined) hotPatch.tailTokens = patch.tailTokens;
233
+ if (patch.transitionPaceTokens !== undefined) {
234
+ hotPatch.transitionPaceTokens = patch.transitionPaceTokens;
235
+ }
236
+
237
+ if (patch.contextBudgetTokens !== undefined) {
238
+ const live = this.contextBudgetTokens ?? DEFAULT_CONTEXT_BUDGET_TOKENS;
239
+ if (patch.contextBudgetTokens >= live) {
240
+ nextContextBudget = patch.contextBudgetTokens;
241
+ nextContextTarget = undefined;
242
+ if (this.getHotContextSettings()) hotPatch.preparedWindowTokens = null;
243
+ } else {
244
+ const hot = this.getHotContextSettings();
245
+ if (!hot) {
246
+ throw new Error('The active context strategy cannot prepare a smaller window live');
247
+ }
248
+ if (patch.transitionPaceTokens === undefined && hot.transitionPaceTokens === undefined) {
249
+ hotPatch.transitionPaceTokens = DEFAULT_TRANSITION_PACE_TOKENS;
250
+ appliedDefaultPace = true;
251
+ }
252
+ hotPatch.preparedWindowTokens = patch.contextBudgetTokens - this.maxTokens;
253
+ nextContextTarget = patch.contextBudgetTokens;
254
+ }
255
+ }
256
+
257
+ if (Object.keys(hotPatch).length > 0) {
258
+ this.updateHotContextSettings(hotPatch);
259
+ }
260
+ this.contextBudgetTokens = nextContextBudget;
261
+ this.contextBudgetTargetTokens = nextContextTarget;
262
+ if (appliedDefaultPace) {
263
+ this.runtimeSettingsOverrides.transitionPaceTokens = DEFAULT_TRANSITION_PACE_TOKENS;
264
+ }
265
+ for (const [key, value] of Object.entries(patch)) {
266
+ if (value !== undefined) (this.runtimeSettingsOverrides as Record<string, number>)[key] = value;
267
+ }
268
+ return this.getRuntimeSettings();
269
+ }
270
+
271
+ resetRuntimeSettings(keys?: Array<keyof AgentRuntimeSettingsPatch>): AgentRuntimeSettingsSnapshot {
272
+ const reset = new Set(keys ?? ['contextBudgetTokens', 'tailTokens', 'transitionPaceTokens']);
273
+ let nextContextBudget = this.contextBudgetTokens;
274
+ let nextContextTarget = this.contextBudgetTargetTokens;
275
+ const hotPatch: {
276
+ tailTokens?: number;
277
+ transitionPaceTokens?: number | null;
278
+ preparedWindowTokens?: number | null;
279
+ } = {};
280
+
281
+ if (reset.has('contextBudgetTokens')) {
282
+ const configured = this.configuredContextBudgetTokens ?? DEFAULT_CONTEXT_BUDGET_TOKENS;
283
+ const live = this.contextBudgetTokens ?? DEFAULT_CONTEXT_BUDGET_TOKENS;
284
+ if (configured < live) {
285
+ const hot = this.getHotContextSettings();
286
+ if (!hot) throw new Error('The active context strategy cannot prepare a smaller window live');
287
+ hotPatch.preparedWindowTokens = configured - this.maxTokens;
288
+ if (hot.transitionPaceTokens === undefined) {
289
+ hotPatch.transitionPaceTokens = DEFAULT_TRANSITION_PACE_TOKENS;
290
+ }
291
+ nextContextTarget = configured;
292
+ } else {
293
+ nextContextBudget = this.configuredContextBudgetTokens;
294
+ nextContextTarget = undefined;
295
+ if (this.getHotContextSettings()) hotPatch.preparedWindowTokens = null;
296
+ }
297
+ }
298
+ if (reset.has('tailTokens')) {
299
+ if (this.configuredTailTokens !== undefined) hotPatch.tailTokens = this.configuredTailTokens;
300
+ }
301
+ if (reset.has('transitionPaceTokens') && this.getHotContextSettings()) {
302
+ hotPatch.transitionPaceTokens =
303
+ nextContextTarget !== undefined && this.configuredTransitionPaceTokens === undefined
304
+ ? DEFAULT_TRANSITION_PACE_TOKENS
305
+ : this.configuredTransitionPaceTokens ?? null;
306
+ }
307
+ if (Object.keys(hotPatch).length > 0) this.updateHotContextSettings(hotPatch);
308
+ this.contextBudgetTokens = nextContextBudget;
309
+ this.contextBudgetTargetTokens = nextContextTarget;
310
+ if (reset.has('contextBudgetTokens')) delete this.runtimeSettingsOverrides.contextBudgetTokens;
311
+ if (reset.has('tailTokens')) delete this.runtimeSettingsOverrides.tailTokens;
312
+ if (reset.has('transitionPaceTokens')) {
313
+ delete this.runtimeSettingsOverrides.transitionPaceTokens;
314
+ }
315
+ return this.getRuntimeSettings();
316
+ }
317
+
318
+ cancelRuntimeSettingsTransition(): AgentRuntimeSettingsSnapshot {
319
+ if (this.contextBudgetTargetTokens !== undefined) {
320
+ this.updateHotContextSettings({ preparedWindowTokens: null });
321
+ this.contextBudgetTargetTokens = undefined;
322
+ const live = this.contextBudgetTokens ?? DEFAULT_CONTEXT_BUDGET_TOKENS;
323
+ if (live === (this.configuredContextBudgetTokens ?? DEFAULT_CONTEXT_BUDGET_TOKENS)) {
324
+ delete this.runtimeSettingsOverrides.contextBudgetTokens;
325
+ } else {
326
+ this.runtimeSettingsOverrides.contextBudgetTokens = live;
327
+ }
328
+ }
329
+ return this.getRuntimeSettings();
330
+ }
331
+
332
+ restoreRuntimeSettings(overrides: AgentRuntimeSettingsOverrides): AgentRuntimeSettingsSnapshot {
333
+ return this.updateRuntimeSettings(overrides);
334
+ }
335
+
336
+ private validateRuntimeSettingsPatch(patch: AgentRuntimeSettingsPatch): void {
337
+ if (Object.keys(patch).length === 0) throw new Error('At least one setting is required');
338
+ if (patch.contextBudgetTokens !== undefined) {
339
+ if (!Number.isSafeInteger(patch.contextBudgetTokens) || patch.contextBudgetTokens <= this.maxTokens) {
340
+ throw new Error(`contextBudgetTokens must be a safe integer greater than max response tokens (${this.maxTokens})`);
341
+ }
342
+ }
343
+ if (patch.tailTokens !== undefined &&
344
+ (!Number.isSafeInteger(patch.tailTokens) || patch.tailTokens < 0)) {
345
+ throw new Error('tailTokens must be a non-negative safe integer');
346
+ }
347
+ if (patch.transitionPaceTokens !== undefined &&
348
+ (!Number.isSafeInteger(patch.transitionPaceTokens) || patch.transitionPaceTokens <= 0)) {
349
+ throw new Error('transitionPaceTokens must be a positive safe integer');
350
+ }
351
+ if ((patch.tailTokens !== undefined || patch.transitionPaceTokens !== undefined) &&
352
+ !this.getHotContextSettings()) {
353
+ throw new Error('The active context strategy does not support live tail/transition settings');
354
+ }
355
+ }
356
+
357
+ private settleRuntimeSettingsTransition(): void {
358
+ if (this.contextBudgetTargetTokens === undefined) return;
359
+ const hot = this.getHotContextSettings();
360
+ if (!hot?.prepared) return;
361
+ this.contextBudgetTokens = this.contextBudgetTargetTokens;
362
+ this.contextBudgetTargetTokens = undefined;
363
+ this.updateHotContextSettings({ preparedWindowTokens: null });
364
+ }
365
+
151
366
  async compileContext(budget?: TokenBudget): Promise<CompileResult> {
152
- return this.contextManager.compile(this.resolveBudget(budget));
367
+ const result = await this.contextManager.compile(this.resolveBudget(budget));
368
+ if (!budget) this.settleRuntimeSettingsTransition();
369
+ return result;
153
370
  }
154
371
 
155
372
  /**
@@ -161,7 +378,9 @@ export class Agent {
161
378
  budget?: TokenBudget,
162
379
  injections?: ContextInjection[]
163
380
  ): Promise<CompileResult> {
164
- return this.contextManager.compile(this.resolveBudget(budget), injections);
381
+ const result = await this.contextManager.compile(this.resolveBudget(budget), injections);
382
+ if (!budget) this.settleRuntimeSettingsTransition();
383
+ return result;
165
384
  }
166
385
 
167
386
  // ==========================================================================
@@ -340,6 +559,28 @@ export class Agent {
340
559
 
341
560
  let { messages, systemInjections } = await this.compileWithInjections(budget, injections);
342
561
 
562
+ // Sanitize: strip empty/whitespace text blocks and drop messages left with
563
+ // no content. The Anthropic API rejects empty text blocks with 400
564
+ // "messages: text content blocks must be non-empty"; when such a block
565
+ // reaches a live activation request the agent cannot complete ANY turn
566
+ // ([inference-failed] on every attempt) until the offending message ages
567
+ // out of the window. Empty text blocks occur naturally: tool-only turns,
568
+ // delivery-failure placeholders, silent/skip turns. Non-text blocks pass
569
+ // through unchanged, so tool pairing is unaffected. (Field-observed
570
+ // 2026-07-10 on a resident agent: one empty block muted the agent's live
571
+ // path entirely; twin of context-manager's stripEmptyTextBlocks on the
572
+ // compression path.)
573
+ messages = messages
574
+ .map((m) => ({
575
+ ...m,
576
+ content: m.content.filter(
577
+ // typeof guard is deliberate runtime defense: history loaded from
578
+ // disk can carry a non-string `text` despite what the types claim.
579
+ (b: ContentBlock) => !(b.type === "text" && (typeof b.text !== "string" || b.text.trim() === "")),
580
+ ),
581
+ }))
582
+ .filter((m) => m.content.length > 0);
583
+
343
584
  // Safety: ensure messages don't end with an assistant message.
344
585
  // Some models reject trailing assistant messages ("prefill not supported"),
345
586
  // and after context compression a stale assistant turn can end up last.