@animalabs/agent-framework 0.6.5 → 0.6.7

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 (155) hide show
  1. package/README.md +78 -0
  2. package/dist/src/agent.d.ts +56 -2
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +372 -57
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/mcp-server.js +0 -0
  7. package/dist/src/api/server.d.ts.map +1 -1
  8. package/dist/src/api/server.js +7 -2
  9. package/dist/src/api/server.js.map +1 -1
  10. package/dist/src/canonical-json.d.ts +2 -0
  11. package/dist/src/canonical-json.d.ts.map +1 -0
  12. package/dist/src/canonical-json.js +23 -0
  13. package/dist/src/canonical-json.js.map +1 -0
  14. package/dist/src/framework.d.ts +160 -2
  15. package/dist/src/framework.d.ts.map +1 -1
  16. package/dist/src/framework.js +3098 -167
  17. package/dist/src/framework.js.map +1 -1
  18. package/dist/src/gate/event-gate.d.ts.map +1 -1
  19. package/dist/src/gate/event-gate.js +34 -4
  20. package/dist/src/gate/event-gate.js.map +1 -1
  21. package/dist/src/index.d.ts +4 -0
  22. package/dist/src/index.d.ts.map +1 -1
  23. package/dist/src/index.js +3 -0
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/mcpl/channel-registry.d.ts +36 -15
  26. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  27. package/dist/src/mcpl/channel-registry.js +322 -76
  28. package/dist/src/mcpl/channel-registry.js.map +1 -1
  29. package/dist/src/mcpl/push-handler.d.ts +1 -0
  30. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  31. package/dist/src/mcpl/push-handler.js +1 -0
  32. package/dist/src/mcpl/push-handler.js.map +1 -1
  33. package/dist/src/mcpl/server-connection.d.ts +39 -4
  34. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  35. package/dist/src/mcpl/server-connection.js +146 -28
  36. package/dist/src/mcpl/server-connection.js.map +1 -1
  37. package/dist/src/mcpl/types.d.ts +43 -12
  38. package/dist/src/mcpl/types.d.ts.map +1 -1
  39. package/dist/src/mcpl/types.js +1 -0
  40. package/dist/src/mcpl/types.js.map +1 -1
  41. package/dist/src/modules/workspace/index.d.ts +12 -1
  42. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  43. package/dist/src/modules/workspace/index.js +661 -2
  44. package/dist/src/modules/workspace/index.js.map +1 -1
  45. package/dist/src/modules/workspace/types.d.ts +4 -0
  46. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  47. package/dist/src/modules/workspace/types.js.map +1 -1
  48. package/dist/src/recovery/discord-awareness-outbox.d.ts +99 -0
  49. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -0
  50. package/dist/src/recovery/discord-awareness-outbox.js +334 -0
  51. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -0
  52. package/dist/src/recovery/offline-branch.d.ts +45 -0
  53. package/dist/src/recovery/offline-branch.d.ts.map +1 -0
  54. package/dist/src/recovery/offline-branch.js +362 -0
  55. package/dist/src/recovery/offline-branch.js.map +1 -0
  56. package/dist/src/recovery/recover-cli.d.ts +3 -0
  57. package/dist/src/recovery/recover-cli.d.ts.map +1 -0
  58. package/dist/src/recovery/recover-cli.js +129 -0
  59. package/dist/src/recovery/recover-cli.js.map +1 -0
  60. package/dist/src/types/agent.d.ts +37 -0
  61. package/dist/src/types/agent.d.ts.map +1 -1
  62. package/dist/src/types/events.d.ts +1 -0
  63. package/dist/src/types/events.d.ts.map +1 -1
  64. package/dist/src/types/framework.d.ts +14 -0
  65. package/dist/src/types/framework.d.ts.map +1 -1
  66. package/dist/src/types/module.d.ts +31 -0
  67. package/dist/src/types/module.d.ts.map +1 -1
  68. package/dist/src/types/trace.d.ts +4 -0
  69. package/dist/src/types/trace.d.ts.map +1 -1
  70. package/dist/test/agent-runtime-settings.test.d.ts +2 -0
  71. package/dist/test/agent-runtime-settings.test.d.ts.map +1 -0
  72. package/dist/test/agent-runtime-settings.test.js +227 -0
  73. package/dist/test/agent-runtime-settings.test.js.map +1 -0
  74. package/dist/test/busy-agent-scheduler.test.d.ts +2 -0
  75. package/dist/test/busy-agent-scheduler.test.d.ts.map +1 -0
  76. package/dist/test/busy-agent-scheduler.test.js +62 -0
  77. package/dist/test/busy-agent-scheduler.test.js.map +1 -0
  78. package/dist/test/channel-lifecycle-provenance.test.d.ts +2 -0
  79. package/dist/test/channel-lifecycle-provenance.test.d.ts.map +1 -0
  80. package/dist/test/channel-lifecycle-provenance.test.js +47 -0
  81. package/dist/test/channel-lifecycle-provenance.test.js.map +1 -0
  82. package/dist/test/channel-registry-routing.test.js +5 -4
  83. package/dist/test/channel-registry-routing.test.js.map +1 -1
  84. package/dist/test/conversation-fork-injection-scoping.test.js +12 -1
  85. package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
  86. package/dist/test/discord-awareness-outbox.test.d.ts +2 -0
  87. package/dist/test/discord-awareness-outbox.test.d.ts.map +1 -0
  88. package/dist/test/discord-awareness-outbox.test.js +248 -0
  89. package/dist/test/discord-awareness-outbox.test.js.map +1 -0
  90. package/dist/test/event-gate.test.js +21 -1
  91. package/dist/test/event-gate.test.js.map +1 -1
  92. package/dist/test/helpers/mock-membrane.d.ts +13 -1
  93. package/dist/test/helpers/mock-membrane.d.ts.map +1 -1
  94. package/dist/test/helpers/mock-membrane.js +9 -2
  95. package/dist/test/helpers/mock-membrane.js.map +1 -1
  96. package/dist/test/maintenance-timer.test.js +103 -1
  97. package/dist/test/maintenance-timer.test.js.map +1 -1
  98. package/dist/test/mcpl-awareness-barrier.test.d.ts +2 -0
  99. package/dist/test/mcpl-awareness-barrier.test.d.ts.map +1 -0
  100. package/dist/test/mcpl-awareness-barrier.test.js +845 -0
  101. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -0
  102. package/dist/test/mcpl-gate-roundtrip.test.js +138 -6
  103. package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
  104. package/dist/test/mcpl-reconnect-reregistration.test.js +22 -3
  105. package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
  106. package/dist/test/mcpl-request-timeout.test.js +15 -0
  107. package/dist/test/mcpl-request-timeout.test.js.map +1 -1
  108. package/dist/test/offline-recovery-branch.test.d.ts +2 -0
  109. package/dist/test/offline-recovery-branch.test.d.ts.map +1 -0
  110. package/dist/test/offline-recovery-branch.test.js +226 -0
  111. package/dist/test/offline-recovery-branch.test.js.map +1 -0
  112. package/dist/test/present-while-acting.test.d.ts +19 -0
  113. package/dist/test/present-while-acting.test.d.ts.map +1 -0
  114. package/dist/test/present-while-acting.test.js +295 -0
  115. package/dist/test/present-while-acting.test.js.map +1 -0
  116. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts +2 -0
  117. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts.map +1 -0
  118. package/dist/test/primary-summary-refusal-fallback-integration.test.js +1955 -0
  119. package/dist/test/primary-summary-refusal-fallback-integration.test.js.map +1 -0
  120. package/dist/test/primary-summary-refusal-fallback.test.d.ts +2 -0
  121. package/dist/test/primary-summary-refusal-fallback.test.d.ts.map +1 -0
  122. package/dist/test/primary-summary-refusal-fallback.test.js +753 -0
  123. package/dist/test/primary-summary-refusal-fallback.test.js.map +1 -0
  124. package/dist/test/same-round-think-policy.test.d.ts +2 -0
  125. package/dist/test/same-round-think-policy.test.d.ts.map +1 -0
  126. package/dist/test/same-round-think-policy.test.js +853 -0
  127. package/dist/test/same-round-think-policy.test.js.map +1 -0
  128. package/dist/test/trunk-channel-routing.test.js +38 -0
  129. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  130. package/dist/test/workspace-read-image.test.d.ts +2 -0
  131. package/dist/test/workspace-read-image.test.d.ts.map +1 -0
  132. package/dist/test/workspace-read-image.test.js +518 -0
  133. package/dist/test/workspace-read-image.test.js.map +1 -0
  134. package/dist/tsconfig.tsbuildinfo +1 -1
  135. package/package.json +6 -4
  136. package/src/agent.ts +485 -63
  137. package/src/api/server.ts +8 -2
  138. package/src/canonical-json.ts +22 -0
  139. package/src/framework.ts +4096 -304
  140. package/src/gate/event-gate.ts +50 -4
  141. package/src/index.ts +20 -0
  142. package/src/mcpl/channel-registry.ts +396 -85
  143. package/src/mcpl/push-handler.ts +2 -0
  144. package/src/mcpl/server-connection.ts +173 -28
  145. package/src/mcpl/types.ts +49 -12
  146. package/src/modules/workspace/index.ts +759 -2
  147. package/src/modules/workspace/types.ts +5 -0
  148. package/src/recovery/discord-awareness-outbox.ts +459 -0
  149. package/src/recovery/offline-branch.ts +510 -0
  150. package/src/recovery/recover-cli.ts +130 -0
  151. package/src/types/agent.ts +44 -0
  152. package/src/types/events.ts +1 -1
  153. package/src/types/framework.ts +17 -0
  154. package/src/types/module.ts +33 -0
  155. package/src/types/trace.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animalabs/agent-framework",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Multi-agent framework with pluggable modules, persistent state, and concurrent tool execution",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,10 +21,12 @@
21
21
  "src"
22
22
  ],
23
23
  "bin": {
24
- "agent-framework-mcp": "./dist/src/api/mcp-server.js"
24
+ "agent-framework-mcp": "./dist/src/api/mcp-server.js",
25
+ "agent-framework-recover": "./dist/src/recovery/recover-cli.js"
25
26
  },
26
27
  "scripts": {
27
28
  "build": "tsc",
29
+ "postbuild": "node -e \"const fs=require('node:fs'); for (const p of ['dist/src/api/mcp-server.js','dist/src/recovery/recover-cli.js']) fs.chmodSync(p, 0o755)\"",
28
30
  "dev": "tsc --watch",
29
31
  "clean": "rm -rf dist",
30
32
  "typecheck": "tsc --noEmit",
@@ -44,8 +46,8 @@
44
46
  "license": "MIT",
45
47
  "dependencies": {
46
48
  "@animalabs/chronicle": "^0.2.2",
47
- "@animalabs/context-manager": "^0.5.5",
48
- "@animalabs/membrane": "^0.5.64",
49
+ "@animalabs/context-manager": "^0.5.12",
50
+ "@animalabs/membrane": "^0.5.73",
49
51
  "chokidar": "^4.0.3",
50
52
  "discord.js": "^14.25.1",
51
53
  "ws": "^8.18.0"
package/src/agent.ts CHANGED
@@ -1,15 +1,37 @@
1
1
  import type { Membrane, NormalizedMessage, NormalizedRequest, ContentBlock, YieldingStream } from '@animalabs/membrane';
2
2
  import { isAbortedResponse } from '@animalabs/membrane';
3
+ import { createHash } from 'node:crypto';
3
4
  import { toolResultDataToHistoryString } from './tool-result-history.js';
5
+ import { canonicalJsonStringify } from './canonical-json.js';
6
+
7
+ export interface ActivationCompileArtifacts {
8
+ compileResult: CompileResult;
9
+ contract: PrimarySummaryContract & {
10
+ systemHash: string;
11
+ };
12
+ }
4
13
 
5
14
  export interface StartStreamResult {
6
15
  stream: YieldingStream;
7
16
  request: NormalizedRequest;
17
+ artifacts: ActivationCompileArtifacts;
8
18
  }
9
- import type { ContextManager, TokenBudget, ContextInjection, CompileResult } from '@animalabs/context-manager';
19
+ import type {
20
+ ContextManager,
21
+ TokenBudget,
22
+ ContextInjection,
23
+ CompileResult,
24
+ HotContextSettingsStatus,
25
+ HotContextSettingsUpdate,
26
+ PrimarySummaryContract,
27
+ PrimarySummaryIdentity,
28
+ PrimarySummaryProjection,
29
+ } from '@animalabs/context-manager';
10
30
  import type {
11
31
  AgentConfig,
12
32
  AgentState,
33
+ SameRoundThinkTextPolicy,
34
+ SameRoundThinkTextPolicySource,
13
35
  PendingToolCall,
14
36
  CompletedToolCall,
15
37
  ToolCallId,
@@ -19,8 +41,19 @@ import type {
19
41
  AgentInfo,
20
42
  InferenceResult,
21
43
  InferenceOptions,
44
+ AgentRuntimeSettingsPatch,
45
+ AgentRuntimeSettingsSnapshot,
46
+ AgentRuntimeSettingsOverrides,
22
47
  } from './types/index.js';
23
48
 
49
+ type PrimarySummaryContractWithSystemHash = PrimarySummaryContract & {
50
+ systemHash: string;
51
+ };
52
+
53
+ const DEFAULT_CONTEXT_BUDGET_TOKENS = 100_000;
54
+ const DEFAULT_TRANSITION_PACE_TOKENS = 16_000;
55
+ const DEFAULT_SAME_ROUND_THINK_TEXT_POLICY: SameRoundThinkTextPolicy = 'public';
56
+
24
57
  /**
25
58
  * An agent wraps a context manager and manages inference state.
26
59
  */
@@ -50,6 +83,7 @@ export class Agent {
50
83
  readonly cacheTtl: NonNullable<AgentConfig['cacheTtl']>;
51
84
  /** Provider-specific request parameters forwarded unchanged by Membrane. */
52
85
  readonly providerParams?: Record<string, unknown>;
86
+ readonly sameRoundThinkTextPolicy: AgentConfig['sameRoundThinkTextPolicy'];
53
87
 
54
88
  private _state: AgentState = { status: 'idle' };
55
89
  private _inferenceStartedAt = 0;
@@ -60,6 +94,12 @@ export class Agent {
60
94
  * ContextManager's built-in default applies. reserveForResponse uses
61
95
  * this agent's maxTokens. */
62
96
  contextBudgetTokens?: number;
97
+ private readonly configuredContextBudgetTokens?: number;
98
+ private readonly configuredTailTokens?: number;
99
+ private readonly configuredTransitionPaceTokens?: number;
100
+ private readonly configuredSameRoundThinkTextPolicy?: SameRoundThinkTextPolicy;
101
+ private contextBudgetTargetTokens?: number;
102
+ private runtimeSettingsOverrides: AgentRuntimeSettingsOverrides = {};
63
103
  private contextManager: ContextManager;
64
104
  private membrane: Membrane;
65
105
 
@@ -79,10 +119,16 @@ export class Agent {
79
119
  this.refusalHandling = config.refusalHandling;
80
120
  this.cacheTtl = config.cacheTtl ?? '1h';
81
121
  this.providerParams = config.providerParams;
122
+ this.sameRoundThinkTextPolicy = config.sameRoundThinkTextPolicy;
82
123
  this.maxStreamTokens = config.maxStreamTokens ?? 150_000;
83
124
  this.contextBudgetTokens = config.contextBudgetTokens;
125
+ this.configuredContextBudgetTokens = config.contextBudgetTokens;
84
126
  this.contextManager = contextManager;
85
127
  this.membrane = membrane;
128
+ const hot = this.getHotContextSettings();
129
+ this.configuredTailTokens = hot?.tailTokens;
130
+ this.configuredTransitionPaceTokens = hot?.transitionPaceTokens;
131
+ this.configuredSameRoundThinkTextPolicy = config.sameRoundThinkTextPolicy;
86
132
  }
87
133
 
88
134
  /**
@@ -148,8 +194,414 @@ export class Agent {
148
194
  return { maxTokens: this.contextBudgetTokens, reserveForResponse: this.maxTokens };
149
195
  }
150
196
 
197
+ /** Structural compatibility keeps lightweight test/host ContextManager
198
+ * doubles working while making the new capability optional at runtime. */
199
+ private getHotContextSettings(): HotContextSettingsStatus | null {
200
+ const manager = this.contextManager as ContextManager & {
201
+ getHotContextSettings?: () => HotContextSettingsStatus | null;
202
+ };
203
+ return typeof manager.getHotContextSettings === 'function'
204
+ ? manager.getHotContextSettings()
205
+ : null;
206
+ }
207
+
208
+ private updateHotContextSettings(update: HotContextSettingsUpdate): HotContextSettingsStatus {
209
+ const manager = this.contextManager as ContextManager & {
210
+ updateHotContextSettings?: (value: HotContextSettingsUpdate) => HotContextSettingsStatus;
211
+ };
212
+ if (typeof manager.updateHotContextSettings !== 'function') {
213
+ throw new Error('The active context strategy does not support live context settings');
214
+ }
215
+ return manager.updateHotContextSettings(update);
216
+ }
217
+
218
+ private hashJson(value: unknown): string {
219
+ return createHash('sha256').update(canonicalJsonStringify(value), 'utf8').digest('hex');
220
+ }
221
+
222
+ private primarySummaryFallbackEnabled(): boolean {
223
+ return this.refusalHandling?.primarySummaryFallback?.enabled === true;
224
+ }
225
+
226
+ private buildPrimarySummaryContractForRequest(request: NormalizedRequest): PrimarySummaryContractWithSystemHash {
227
+ return {
228
+ systemHash: this.hashJson(request.system ?? ''),
229
+ modelConfigHash: this.hashJson({
230
+ requestConfig: request.config,
231
+ providerParams: request.providerParams,
232
+ promptCaching: request.promptCaching,
233
+ cacheTtl: request.cacheTtl,
234
+ assistantParticipant: request.assistantParticipant,
235
+ }),
236
+ toolContractHash: this.hashJson(request.tools ?? []),
237
+ };
238
+ }
239
+
240
+ private setPrimaryLaneContract(contract: PrimarySummaryContract | undefined): void {
241
+ const manager = this.contextManager as ContextManager & {
242
+ setPrimaryLaneContract?: (value: PrimarySummaryContract | undefined) => void;
243
+ };
244
+ manager.setPrimaryLaneContract?.(contract);
245
+ }
246
+
247
+ private matchingPrimarySummaryQuarantine(
248
+ projection: PrimarySummaryProjection,
249
+ contract: PrimarySummaryContract,
250
+ ): PrimarySummaryIdentity[] {
251
+ const manager = this.contextManager as ContextManager & {
252
+ matchingPrimarySummaryQuarantine?: (
253
+ projection: PrimarySummaryProjection,
254
+ contract?: PrimarySummaryContract,
255
+ ) => PrimarySummaryIdentity[];
256
+ };
257
+ return manager.matchingPrimarySummaryQuarantine?.(projection, contract) ?? [];
258
+ }
259
+
260
+ private expandPrimarySummaryProjectionRaw(
261
+ compiled: CompileResult,
262
+ summaries: ReadonlyArray<PrimarySummaryIdentity>,
263
+ ): CompileResult {
264
+ const manager = this.contextManager as ContextManager & {
265
+ expandPrimarySummaryProjectionRaw?: (
266
+ compiled: CompileResult,
267
+ summaries: ReadonlyArray<PrimarySummaryIdentity>,
268
+ ) => CompileResult;
269
+ };
270
+ if (typeof manager.expandPrimarySummaryProjectionRaw !== 'function') return compiled;
271
+ return manager.expandPrimarySummaryProjectionRaw(compiled, summaries);
272
+ }
273
+
274
+ private primarySummaryIdentityKey(identity: PrimarySummaryIdentity): string {
275
+ return [
276
+ identity.id,
277
+ identity.contentHash,
278
+ identity.carrierHash,
279
+ identity.sourceLeafHash,
280
+ ].join('\u0000');
281
+ }
282
+
283
+ private buildRequestFromCompileResult(
284
+ compileResult: CompileResult,
285
+ availableTools: ToolDefinition[],
286
+ ): NormalizedRequest {
287
+ let { messages, systemInjections } = compileResult;
288
+
289
+ messages = messages
290
+ .map((m) => ({
291
+ ...m,
292
+ content: m.content.filter(
293
+ (b: ContentBlock) => !(b.type === "text" && (typeof b.text !== "string" || b.text.trim() === "")),
294
+ ),
295
+ }))
296
+ .filter((m) => m.content.length > 0);
297
+
298
+ if (messages.length > 0 && messages[messages.length - 1]!.participant === this.name) {
299
+ messages = [...messages, {
300
+ participant: 'user',
301
+ content: [{ type: 'text', text: '[Continue]' }],
302
+ }];
303
+ }
304
+
305
+ return {
306
+ messages,
307
+ system: this.buildSystemPrompt(systemInjections),
308
+ config: {
309
+ model: this.model,
310
+ maxTokens: this.maxTokens,
311
+ ...(this.temperature !== undefined && { temperature: this.temperature }),
312
+ ...(this.thinking !== undefined && { thinking: this.thinking }),
313
+ },
314
+ tools: availableTools.length > 0 ? availableTools : undefined,
315
+ promptCaching: true,
316
+ cacheTtl: this.cacheTtl,
317
+ ...(this.providerParams && { providerParams: this.providerParams }),
318
+ assistantParticipant: this.name,
319
+ };
320
+ }
321
+
322
+ async prepareActivationRequest(
323
+ availableTools: ToolDefinition[],
324
+ injections?: ContextInjection[],
325
+ budget?: TokenBudget,
326
+ ): Promise<{ request: NormalizedRequest; artifacts: ActivationCompileArtifacts }> {
327
+ (this.contextManager as unknown as { setToolDefinitions?: (t: ToolDefinition[]) => void })
328
+ .setToolDefinitions?.(availableTools);
329
+
330
+ let compileResult = await this.compileWithInjections(budget, injections);
331
+ let request = this.buildRequestFromCompileResult(compileResult, availableTools);
332
+ const contract = this.buildPrimarySummaryContractForRequest(request);
333
+ this.setPrimaryLaneContract(contract);
334
+
335
+ const projection = compileResult.primarySummaryProjection;
336
+ if (this.primarySummaryFallbackEnabled() && projection) {
337
+ const quarantined = this.matchingPrimarySummaryQuarantine(projection, contract);
338
+ if (quarantined.length > 0) {
339
+ compileResult = this.expandPrimarySummaryProjectionRaw(compileResult, quarantined);
340
+ request = this.buildRequestFromCompileResult(compileResult, availableTools);
341
+ }
342
+ }
343
+
344
+ return {
345
+ request,
346
+ artifacts: {
347
+ compileResult,
348
+ contract,
349
+ },
350
+ };
351
+ }
352
+
353
+ getRuntimeSettings(): AgentRuntimeSettingsSnapshot {
354
+ const hot = this.getHotContextSettings();
355
+ return {
356
+ contextBudgetTokens:
357
+ this.contextBudgetTargetTokens ??
358
+ this.contextBudgetTokens ??
359
+ DEFAULT_CONTEXT_BUDGET_TOKENS,
360
+ ...(hot ? { tailTokens: hot.tailTokens } : {}),
361
+ ...(hot?.transitionPaceTokens !== undefined
362
+ ? { transitionPaceTokens: hot.transitionPaceTokens }
363
+ : {}),
364
+ sameRoundThinkTextPolicy: this.getEffectiveSameRoundThinkTextPolicy(),
365
+ sameRoundThinkTextPolicySource: this.getSameRoundThinkTextPolicySource(),
366
+ transition: this.contextBudgetTargetTokens === undefined
367
+ ? 'stable'
368
+ : hot?.blocked
369
+ ? 'blocked'
370
+ : 'converging',
371
+ ...(hot?.blocked === 'transition-pace-floor'
372
+ ? { transitionReason: 'transition_pace_too_small' as const }
373
+ : hot?.blocked === 'prepared-window-floor'
374
+ ? { transitionReason: 'protected_context_exceeds_target' as const }
375
+ : {}),
376
+ };
377
+ }
378
+
379
+ getRuntimeSettingsOverrides(): AgentRuntimeSettingsOverrides {
380
+ return { ...this.runtimeSettingsOverrides };
381
+ }
382
+
383
+ getEffectiveSameRoundThinkTextPolicy(): SameRoundThinkTextPolicy {
384
+ return this.runtimeSettingsOverrides.sameRoundThinkTextPolicy
385
+ ?? this.configuredSameRoundThinkTextPolicy
386
+ ?? DEFAULT_SAME_ROUND_THINK_TEXT_POLICY;
387
+ }
388
+
389
+ getSameRoundThinkTextPolicySource(): SameRoundThinkTextPolicySource {
390
+ if (this.runtimeSettingsOverrides.sameRoundThinkTextPolicy !== undefined) {
391
+ return 'runtime_override';
392
+ }
393
+ if (this.configuredSameRoundThinkTextPolicy !== undefined) {
394
+ return 'recipe';
395
+ }
396
+ return 'compatibility_default';
397
+ }
398
+
399
+ getCurrentBranchGeneration():
400
+ | { id: string; name: string; head: number; generation: number; parentId?: string; branchPoint?: number; created: Date }
401
+ | null {
402
+ const manager = this.contextManager as ContextManager & {
403
+ currentBranchGeneration?: () => {
404
+ id: string;
405
+ name: string;
406
+ head: number;
407
+ generation: number;
408
+ parentId?: string;
409
+ branchPoint?: number;
410
+ created: Date;
411
+ };
412
+ };
413
+ return manager.currentBranchGeneration?.() ?? null;
414
+ }
415
+
416
+ buildPrimarySummaryRawExpansionRequest(
417
+ artifacts: ActivationCompileArtifacts,
418
+ summaries: ReadonlyArray<PrimarySummaryIdentity>,
419
+ availableTools: ToolDefinition[],
420
+ ): { request: NormalizedRequest; artifacts: ActivationCompileArtifacts } {
421
+ this.setPrimaryLaneContract(artifacts.contract);
422
+ const alreadyExpanded = new Set(
423
+ artifacts.compileResult.primarySummaryProjection?.selectedSummaries
424
+ .filter((selection) => selection.renderedAs === 'raw_expansion')
425
+ .map((selection) => this.primarySummaryIdentityKey(selection.identity)) ?? [],
426
+ );
427
+ const expandable = summaries.filter((summary) =>
428
+ !alreadyExpanded.has(this.primarySummaryIdentityKey(summary)),
429
+ );
430
+ const compileResult = expandable.length > 0
431
+ ? this.expandPrimarySummaryProjectionRaw(
432
+ artifacts.compileResult,
433
+ expandable,
434
+ )
435
+ : artifacts.compileResult;
436
+ return {
437
+ request: this.buildRequestFromCompileResult(compileResult, availableTools),
438
+ artifacts: {
439
+ compileResult,
440
+ contract: artifacts.contract,
441
+ },
442
+ };
443
+ }
444
+
445
+ updateRuntimeSettings(patch: AgentRuntimeSettingsPatch): AgentRuntimeSettingsSnapshot {
446
+ this.validateRuntimeSettingsPatch(patch);
447
+ let nextContextBudget = this.contextBudgetTokens;
448
+ let nextContextTarget = this.contextBudgetTargetTokens;
449
+ let appliedDefaultPace = false;
450
+ const hotPatch: {
451
+ tailTokens?: number;
452
+ transitionPaceTokens?: number | null;
453
+ preparedWindowTokens?: number | null;
454
+ } = {};
455
+ if (patch.tailTokens !== undefined) hotPatch.tailTokens = patch.tailTokens;
456
+ if (patch.transitionPaceTokens !== undefined) {
457
+ hotPatch.transitionPaceTokens = patch.transitionPaceTokens;
458
+ }
459
+
460
+ if (patch.contextBudgetTokens !== undefined) {
461
+ const live = this.contextBudgetTokens ?? DEFAULT_CONTEXT_BUDGET_TOKENS;
462
+ if (patch.contextBudgetTokens >= live) {
463
+ nextContextBudget = patch.contextBudgetTokens;
464
+ nextContextTarget = undefined;
465
+ if (this.getHotContextSettings()) hotPatch.preparedWindowTokens = null;
466
+ } else {
467
+ const hot = this.getHotContextSettings();
468
+ if (!hot) {
469
+ throw new Error('The active context strategy cannot prepare a smaller window live');
470
+ }
471
+ if (patch.transitionPaceTokens === undefined && hot.transitionPaceTokens === undefined) {
472
+ hotPatch.transitionPaceTokens = DEFAULT_TRANSITION_PACE_TOKENS;
473
+ appliedDefaultPace = true;
474
+ }
475
+ hotPatch.preparedWindowTokens = patch.contextBudgetTokens - this.maxTokens;
476
+ nextContextTarget = patch.contextBudgetTokens;
477
+ }
478
+ }
479
+
480
+ if (Object.keys(hotPatch).length > 0) {
481
+ this.updateHotContextSettings(hotPatch);
482
+ }
483
+ this.contextBudgetTokens = nextContextBudget;
484
+ this.contextBudgetTargetTokens = nextContextTarget;
485
+ if (appliedDefaultPace) {
486
+ this.runtimeSettingsOverrides.transitionPaceTokens = DEFAULT_TRANSITION_PACE_TOKENS;
487
+ }
488
+ for (const [key, value] of Object.entries(patch)) {
489
+ if (key === 'sameRoundThinkTextPolicy') continue;
490
+ if (value !== undefined) (this.runtimeSettingsOverrides as Record<string, unknown>)[key] = value;
491
+ }
492
+ if (patch.sameRoundThinkTextPolicy !== undefined) {
493
+ this.runtimeSettingsOverrides.sameRoundThinkTextPolicy = patch.sameRoundThinkTextPolicy;
494
+ }
495
+ return this.getRuntimeSettings();
496
+ }
497
+
498
+ resetRuntimeSettings(keys?: Array<keyof AgentRuntimeSettingsPatch>): AgentRuntimeSettingsSnapshot {
499
+ const reset = new Set(keys ?? ['contextBudgetTokens', 'tailTokens', 'transitionPaceTokens', 'sameRoundThinkTextPolicy']);
500
+ let nextContextBudget = this.contextBudgetTokens;
501
+ let nextContextTarget = this.contextBudgetTargetTokens;
502
+ const hotPatch: {
503
+ tailTokens?: number;
504
+ transitionPaceTokens?: number | null;
505
+ preparedWindowTokens?: number | null;
506
+ } = {};
507
+
508
+ if (reset.has('contextBudgetTokens')) {
509
+ const configured = this.configuredContextBudgetTokens ?? DEFAULT_CONTEXT_BUDGET_TOKENS;
510
+ const live = this.contextBudgetTokens ?? DEFAULT_CONTEXT_BUDGET_TOKENS;
511
+ if (configured < live) {
512
+ const hot = this.getHotContextSettings();
513
+ if (!hot) throw new Error('The active context strategy cannot prepare a smaller window live');
514
+ hotPatch.preparedWindowTokens = configured - this.maxTokens;
515
+ if (hot.transitionPaceTokens === undefined) {
516
+ hotPatch.transitionPaceTokens = DEFAULT_TRANSITION_PACE_TOKENS;
517
+ }
518
+ nextContextTarget = configured;
519
+ } else {
520
+ nextContextBudget = this.configuredContextBudgetTokens;
521
+ nextContextTarget = undefined;
522
+ if (this.getHotContextSettings()) hotPatch.preparedWindowTokens = null;
523
+ }
524
+ }
525
+ if (reset.has('tailTokens')) {
526
+ if (this.configuredTailTokens !== undefined) hotPatch.tailTokens = this.configuredTailTokens;
527
+ }
528
+ if (reset.has('transitionPaceTokens') && this.getHotContextSettings()) {
529
+ hotPatch.transitionPaceTokens =
530
+ nextContextTarget !== undefined && this.configuredTransitionPaceTokens === undefined
531
+ ? DEFAULT_TRANSITION_PACE_TOKENS
532
+ : this.configuredTransitionPaceTokens ?? null;
533
+ }
534
+ if (Object.keys(hotPatch).length > 0) this.updateHotContextSettings(hotPatch);
535
+ this.contextBudgetTokens = nextContextBudget;
536
+ this.contextBudgetTargetTokens = nextContextTarget;
537
+ if (reset.has('contextBudgetTokens')) delete this.runtimeSettingsOverrides.contextBudgetTokens;
538
+ if (reset.has('tailTokens')) delete this.runtimeSettingsOverrides.tailTokens;
539
+ if (reset.has('transitionPaceTokens')) {
540
+ delete this.runtimeSettingsOverrides.transitionPaceTokens;
541
+ }
542
+ if (reset.has('sameRoundThinkTextPolicy')) {
543
+ delete this.runtimeSettingsOverrides.sameRoundThinkTextPolicy;
544
+ }
545
+ return this.getRuntimeSettings();
546
+ }
547
+
548
+ cancelRuntimeSettingsTransition(): AgentRuntimeSettingsSnapshot {
549
+ if (this.contextBudgetTargetTokens !== undefined) {
550
+ this.updateHotContextSettings({ preparedWindowTokens: null });
551
+ this.contextBudgetTargetTokens = undefined;
552
+ const live = this.contextBudgetTokens ?? DEFAULT_CONTEXT_BUDGET_TOKENS;
553
+ if (live === (this.configuredContextBudgetTokens ?? DEFAULT_CONTEXT_BUDGET_TOKENS)) {
554
+ delete this.runtimeSettingsOverrides.contextBudgetTokens;
555
+ } else {
556
+ this.runtimeSettingsOverrides.contextBudgetTokens = live;
557
+ }
558
+ }
559
+ return this.getRuntimeSettings();
560
+ }
561
+
562
+ restoreRuntimeSettings(overrides: AgentRuntimeSettingsOverrides): AgentRuntimeSettingsSnapshot {
563
+ return this.updateRuntimeSettings(overrides);
564
+ }
565
+
566
+ private validateRuntimeSettingsPatch(patch: AgentRuntimeSettingsPatch): void {
567
+ if (Object.keys(patch).length === 0) throw new Error('At least one setting is required');
568
+ if (patch.contextBudgetTokens !== undefined) {
569
+ if (!Number.isSafeInteger(patch.contextBudgetTokens) || patch.contextBudgetTokens <= this.maxTokens) {
570
+ throw new Error(`contextBudgetTokens must be a safe integer greater than max response tokens (${this.maxTokens})`);
571
+ }
572
+ }
573
+ if (patch.tailTokens !== undefined &&
574
+ (!Number.isSafeInteger(patch.tailTokens) || patch.tailTokens < 0)) {
575
+ throw new Error('tailTokens must be a non-negative safe integer');
576
+ }
577
+ if (patch.transitionPaceTokens !== undefined &&
578
+ (!Number.isSafeInteger(patch.transitionPaceTokens) || patch.transitionPaceTokens <= 0)) {
579
+ throw new Error('transitionPaceTokens must be a positive safe integer');
580
+ }
581
+ if (patch.sameRoundThinkTextPolicy !== undefined &&
582
+ patch.sameRoundThinkTextPolicy !== 'public' &&
583
+ patch.sameRoundThinkTextPolicy !== 'private') {
584
+ throw new Error("sameRoundThinkTextPolicy must be 'public' or 'private'");
585
+ }
586
+ if ((patch.tailTokens !== undefined || patch.transitionPaceTokens !== undefined) &&
587
+ !this.getHotContextSettings()) {
588
+ throw new Error('The active context strategy does not support live tail/transition settings');
589
+ }
590
+ }
591
+
592
+ private settleRuntimeSettingsTransition(): void {
593
+ if (this.contextBudgetTargetTokens === undefined) return;
594
+ const hot = this.getHotContextSettings();
595
+ if (!hot?.prepared) return;
596
+ this.contextBudgetTokens = this.contextBudgetTargetTokens;
597
+ this.contextBudgetTargetTokens = undefined;
598
+ this.updateHotContextSettings({ preparedWindowTokens: null });
599
+ }
600
+
151
601
  async compileContext(budget?: TokenBudget): Promise<CompileResult> {
152
- return this.contextManager.compile(this.resolveBudget(budget));
602
+ const result = await this.contextManager.compile(this.resolveBudget(budget));
603
+ if (!budget) this.settleRuntimeSettingsTransition();
604
+ return result;
153
605
  }
154
606
 
155
607
  /**
@@ -161,7 +613,9 @@ export class Agent {
161
613
  budget?: TokenBudget,
162
614
  injections?: ContextInjection[]
163
615
  ): Promise<CompileResult> {
164
- return this.contextManager.compile(this.resolveBudget(budget), injections);
616
+ const result = await this.contextManager.compile(this.resolveBudget(budget), injections);
617
+ if (!budget) this.settleRuntimeSettingsTransition();
618
+ return result;
165
619
  }
166
620
 
167
621
  // ==========================================================================
@@ -329,64 +783,8 @@ export class Agent {
329
783
  injections?: ContextInjection[],
330
784
  budget?: TokenBudget
331
785
  ): Promise<NormalizedRequest> {
332
- // Keep the context manager's view of the live tool surface current: the
333
- // autobiographical strategy must declare the same tools on its
334
- // summarizer/compression requests, or transcripts containing tool blocks
335
- // are refused by Anthropic's reasoning_extraction classifier (labclaude
336
- // incident, 2026-07-09). Optional chaining: older context-manager
337
- // versions don't have the hook.
338
- (this.contextManager as unknown as { setToolDefinitions?: (t: ToolDefinition[]) => void })
339
- .setToolDefinitions?.(availableTools);
340
-
341
- let { messages, systemInjections } = await this.compileWithInjections(budget, injections);
342
-
343
- // Sanitize: strip empty/whitespace text blocks and drop messages left with
344
- // no content. The Anthropic API rejects empty text blocks with 400
345
- // "messages: text content blocks must be non-empty"; when such a block
346
- // reaches a live activation request the agent cannot complete ANY turn
347
- // ([inference-failed] on every attempt) until the offending message ages
348
- // out of the window. Empty text blocks occur naturally: tool-only turns,
349
- // delivery-failure placeholders, silent/skip turns. Non-text blocks pass
350
- // through unchanged, so tool pairing is unaffected. (Field-observed
351
- // 2026-07-10 on a resident agent: one empty block muted the agent's live
352
- // path entirely; twin of context-manager's stripEmptyTextBlocks on the
353
- // compression path.)
354
- messages = messages
355
- .map((m) => ({
356
- ...m,
357
- content: m.content.filter(
358
- // typeof guard is deliberate runtime defense: history loaded from
359
- // disk can carry a non-string `text` despite what the types claim.
360
- (b: ContentBlock) => !(b.type === "text" && (typeof b.text !== "string" || b.text.trim() === "")),
361
- ),
362
- }))
363
- .filter((m) => m.content.length > 0);
364
-
365
- // Safety: ensure messages don't end with an assistant message.
366
- // Some models reject trailing assistant messages ("prefill not supported"),
367
- // and after context compression a stale assistant turn can end up last.
368
- if (messages.length > 0 && messages[messages.length - 1]!.participant === this.name) {
369
- messages = [...messages, {
370
- participant: 'user',
371
- content: [{ type: 'text', text: '[Continue]' }],
372
- }];
373
- }
374
-
375
- return {
376
- messages,
377
- system: this.buildSystemPrompt(systemInjections),
378
- config: {
379
- model: this.model,
380
- maxTokens: this.maxTokens,
381
- ...(this.temperature !== undefined && { temperature: this.temperature }),
382
- ...(this.thinking !== undefined && { thinking: this.thinking }),
383
- },
384
- tools: availableTools.length > 0 ? availableTools : undefined,
385
- promptCaching: true,
386
- cacheTtl: this.cacheTtl,
387
- ...(this.providerParams && { providerParams: this.providerParams }),
388
- assistantParticipant: this.name,
389
- };
786
+ const { request } = await this.prepareActivationRequest(availableTools, injections, budget);
787
+ return request;
390
788
  }
391
789
 
392
790
  /**
@@ -406,7 +804,11 @@ export class Agent {
406
804
  this._inferenceStartedAt = Date.now();
407
805
  this.lastStreamInputTokens = 0;
408
806
 
409
- const request = await this.buildActivationRequest(availableTools, injections, budget);
807
+ const { request, artifacts } = await this.prepareActivationRequest(
808
+ availableTools,
809
+ injections,
810
+ budget,
811
+ );
410
812
 
411
813
  const stream = this.membrane.streamYielding(request, {
412
814
  emitTokens: true,
@@ -415,7 +817,27 @@ export class Agent {
415
817
  });
416
818
 
417
819
  this._state = { status: 'streaming', stream };
418
- return { stream, request };
820
+ return { stream, request, artifacts };
821
+ }
822
+
823
+ startStreamFromRequest(
824
+ request: NormalizedRequest,
825
+ artifacts: ActivationCompileArtifacts,
826
+ ): StartStreamResult {
827
+ if (this._state.status !== 'streaming' && this._state.status !== 'idle') {
828
+ throw new Error(`Agent ${this.name} cannot replace stream in state ${this._state.status}`);
829
+ }
830
+ this._streamId++;
831
+ this._inferenceStartedAt = Date.now();
832
+ this.lastStreamInputTokens = 0;
833
+ this.setPrimaryLaneContract(artifacts.contract);
834
+ const stream = this.membrane.streamYielding(request, {
835
+ emitTokens: true,
836
+ emitBlocks: false,
837
+ emitUsage: true,
838
+ });
839
+ this._state = { status: 'streaming', stream };
840
+ return { stream, request, artifacts };
419
841
  }
420
842
 
421
843
  /**