@animalabs/agent-framework 0.6.6 → 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 (128) hide show
  1. package/README.md +78 -0
  2. package/dist/src/agent.d.ts +39 -2
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +172 -56
  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 +147 -3
  15. package/dist/src/framework.d.ts.map +1 -1
  16. package/dist/src/framework.js +2719 -125
  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 +9 -2
  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 +3 -1
  26. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  27. package/dist/src/mcpl/channel-registry.js +29 -15
  28. package/dist/src/mcpl/channel-registry.js.map +1 -1
  29. package/dist/src/mcpl/server-connection.d.ts +36 -3
  30. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  31. package/dist/src/mcpl/server-connection.js +142 -28
  32. package/dist/src/mcpl/server-connection.js.map +1 -1
  33. package/dist/src/modules/workspace/index.d.ts +12 -1
  34. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  35. package/dist/src/modules/workspace/index.js +661 -2
  36. package/dist/src/modules/workspace/index.js.map +1 -1
  37. package/dist/src/modules/workspace/types.d.ts +4 -0
  38. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  39. package/dist/src/modules/workspace/types.js.map +1 -1
  40. package/dist/src/recovery/discord-awareness-outbox.d.ts +99 -0
  41. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -0
  42. package/dist/src/recovery/discord-awareness-outbox.js +334 -0
  43. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -0
  44. package/dist/src/recovery/offline-branch.d.ts +45 -0
  45. package/dist/src/recovery/offline-branch.d.ts.map +1 -0
  46. package/dist/src/recovery/offline-branch.js +362 -0
  47. package/dist/src/recovery/offline-branch.js.map +1 -0
  48. package/dist/src/recovery/recover-cli.d.ts +3 -0
  49. package/dist/src/recovery/recover-cli.d.ts.map +1 -0
  50. package/dist/src/recovery/recover-cli.js +129 -0
  51. package/dist/src/recovery/recover-cli.js.map +1 -0
  52. package/dist/src/types/agent.d.ts +22 -0
  53. package/dist/src/types/agent.d.ts.map +1 -1
  54. package/dist/src/types/framework.d.ts +14 -0
  55. package/dist/src/types/framework.d.ts.map +1 -1
  56. package/dist/src/types/module.d.ts +31 -0
  57. package/dist/src/types/module.d.ts.map +1 -1
  58. package/dist/src/types/trace.d.ts +2 -0
  59. package/dist/src/types/trace.d.ts.map +1 -1
  60. package/dist/test/agent-runtime-settings.test.js +200 -93
  61. package/dist/test/agent-runtime-settings.test.js.map +1 -1
  62. package/dist/test/busy-agent-scheduler.test.d.ts +2 -0
  63. package/dist/test/busy-agent-scheduler.test.d.ts.map +1 -0
  64. package/dist/test/busy-agent-scheduler.test.js +62 -0
  65. package/dist/test/busy-agent-scheduler.test.js.map +1 -0
  66. package/dist/test/channel-lifecycle-provenance.test.d.ts +2 -0
  67. package/dist/test/channel-lifecycle-provenance.test.d.ts.map +1 -0
  68. package/dist/test/channel-lifecycle-provenance.test.js +47 -0
  69. package/dist/test/channel-lifecycle-provenance.test.js.map +1 -0
  70. package/dist/test/discord-awareness-outbox.test.d.ts +2 -0
  71. package/dist/test/discord-awareness-outbox.test.d.ts.map +1 -0
  72. package/dist/test/discord-awareness-outbox.test.js +248 -0
  73. package/dist/test/discord-awareness-outbox.test.js.map +1 -0
  74. package/dist/test/helpers/mock-membrane.js +1 -1
  75. package/dist/test/helpers/mock-membrane.js.map +1 -1
  76. package/dist/test/maintenance-timer.test.js +100 -0
  77. package/dist/test/maintenance-timer.test.js.map +1 -1
  78. package/dist/test/mcpl-awareness-barrier.test.d.ts +2 -0
  79. package/dist/test/mcpl-awareness-barrier.test.d.ts.map +1 -0
  80. package/dist/test/mcpl-awareness-barrier.test.js +845 -0
  81. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -0
  82. package/dist/test/mcpl-reconnect-reregistration.test.js +22 -3
  83. package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
  84. package/dist/test/mcpl-request-timeout.test.js +15 -0
  85. package/dist/test/mcpl-request-timeout.test.js.map +1 -1
  86. package/dist/test/offline-recovery-branch.test.d.ts +2 -0
  87. package/dist/test/offline-recovery-branch.test.d.ts.map +1 -0
  88. package/dist/test/offline-recovery-branch.test.js +226 -0
  89. package/dist/test/offline-recovery-branch.test.js.map +1 -0
  90. package/dist/test/present-while-acting.test.d.ts +3 -2
  91. package/dist/test/present-while-acting.test.d.ts.map +1 -1
  92. package/dist/test/present-while-acting.test.js +31 -3
  93. package/dist/test/present-while-acting.test.js.map +1 -1
  94. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts +2 -0
  95. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts.map +1 -0
  96. package/dist/test/primary-summary-refusal-fallback-integration.test.js +1955 -0
  97. package/dist/test/primary-summary-refusal-fallback-integration.test.js.map +1 -0
  98. package/dist/test/primary-summary-refusal-fallback.test.d.ts +2 -0
  99. package/dist/test/primary-summary-refusal-fallback.test.d.ts.map +1 -0
  100. package/dist/test/primary-summary-refusal-fallback.test.js +753 -0
  101. package/dist/test/primary-summary-refusal-fallback.test.js.map +1 -0
  102. package/dist/test/same-round-think-policy.test.d.ts +2 -0
  103. package/dist/test/same-round-think-policy.test.d.ts.map +1 -0
  104. package/dist/test/same-round-think-policy.test.js +853 -0
  105. package/dist/test/same-round-think-policy.test.js.map +1 -0
  106. package/dist/test/workspace-read-image.test.d.ts +2 -0
  107. package/dist/test/workspace-read-image.test.d.ts.map +1 -0
  108. package/dist/test/workspace-read-image.test.js +518 -0
  109. package/dist/test/workspace-read-image.test.js.map +1 -0
  110. package/dist/tsconfig.tsbuildinfo +1 -1
  111. package/package.json +5 -3
  112. package/src/agent.ts +265 -62
  113. package/src/api/server.ts +8 -2
  114. package/src/canonical-json.ts +22 -0
  115. package/src/framework.ts +3561 -145
  116. package/src/gate/event-gate.ts +15 -3
  117. package/src/index.ts +20 -0
  118. package/src/mcpl/channel-registry.ts +31 -16
  119. package/src/mcpl/server-connection.ts +163 -28
  120. package/src/modules/workspace/index.ts +759 -2
  121. package/src/modules/workspace/types.ts +5 -0
  122. package/src/recovery/discord-awareness-outbox.ts +459 -0
  123. package/src/recovery/offline-branch.ts +510 -0
  124. package/src/recovery/recover-cli.ts +130 -0
  125. package/src/types/agent.ts +26 -0
  126. package/src/types/framework.ts +17 -0
  127. package/src/types/module.ts +33 -0
  128. 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.6",
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,7 +46,7 @@
44
46
  "license": "MIT",
45
47
  "dependencies": {
46
48
  "@animalabs/chronicle": "^0.2.2",
47
- "@animalabs/context-manager": "^0.5.11",
49
+ "@animalabs/context-manager": "^0.5.12",
48
50
  "@animalabs/membrane": "^0.5.73",
49
51
  "chokidar": "^4.0.3",
50
52
  "discord.js": "^14.25.1",
package/src/agent.ts CHANGED
@@ -1,10 +1,20 @@
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
19
  import type {
10
20
  ContextManager,
@@ -13,10 +23,15 @@ import type {
13
23
  CompileResult,
14
24
  HotContextSettingsStatus,
15
25
  HotContextSettingsUpdate,
26
+ PrimarySummaryContract,
27
+ PrimarySummaryIdentity,
28
+ PrimarySummaryProjection,
16
29
  } from '@animalabs/context-manager';
17
30
  import type {
18
31
  AgentConfig,
19
32
  AgentState,
33
+ SameRoundThinkTextPolicy,
34
+ SameRoundThinkTextPolicySource,
20
35
  PendingToolCall,
21
36
  CompletedToolCall,
22
37
  ToolCallId,
@@ -31,8 +46,13 @@ import type {
31
46
  AgentRuntimeSettingsOverrides,
32
47
  } from './types/index.js';
33
48
 
49
+ type PrimarySummaryContractWithSystemHash = PrimarySummaryContract & {
50
+ systemHash: string;
51
+ };
52
+
34
53
  const DEFAULT_CONTEXT_BUDGET_TOKENS = 100_000;
35
54
  const DEFAULT_TRANSITION_PACE_TOKENS = 16_000;
55
+ const DEFAULT_SAME_ROUND_THINK_TEXT_POLICY: SameRoundThinkTextPolicy = 'public';
36
56
 
37
57
  /**
38
58
  * An agent wraps a context manager and manages inference state.
@@ -63,6 +83,7 @@ export class Agent {
63
83
  readonly cacheTtl: NonNullable<AgentConfig['cacheTtl']>;
64
84
  /** Provider-specific request parameters forwarded unchanged by Membrane. */
65
85
  readonly providerParams?: Record<string, unknown>;
86
+ readonly sameRoundThinkTextPolicy: AgentConfig['sameRoundThinkTextPolicy'];
66
87
 
67
88
  private _state: AgentState = { status: 'idle' };
68
89
  private _inferenceStartedAt = 0;
@@ -76,6 +97,7 @@ export class Agent {
76
97
  private readonly configuredContextBudgetTokens?: number;
77
98
  private readonly configuredTailTokens?: number;
78
99
  private readonly configuredTransitionPaceTokens?: number;
100
+ private readonly configuredSameRoundThinkTextPolicy?: SameRoundThinkTextPolicy;
79
101
  private contextBudgetTargetTokens?: number;
80
102
  private runtimeSettingsOverrides: AgentRuntimeSettingsOverrides = {};
81
103
  private contextManager: ContextManager;
@@ -97,6 +119,7 @@ export class Agent {
97
119
  this.refusalHandling = config.refusalHandling;
98
120
  this.cacheTtl = config.cacheTtl ?? '1h';
99
121
  this.providerParams = config.providerParams;
122
+ this.sameRoundThinkTextPolicy = config.sameRoundThinkTextPolicy;
100
123
  this.maxStreamTokens = config.maxStreamTokens ?? 150_000;
101
124
  this.contextBudgetTokens = config.contextBudgetTokens;
102
125
  this.configuredContextBudgetTokens = config.contextBudgetTokens;
@@ -105,6 +128,7 @@ export class Agent {
105
128
  const hot = this.getHotContextSettings();
106
129
  this.configuredTailTokens = hot?.tailTokens;
107
130
  this.configuredTransitionPaceTokens = hot?.transitionPaceTokens;
131
+ this.configuredSameRoundThinkTextPolicy = config.sameRoundThinkTextPolicy;
108
132
  }
109
133
 
110
134
  /**
@@ -191,6 +215,141 @@ export class Agent {
191
215
  return manager.updateHotContextSettings(update);
192
216
  }
193
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
+
194
353
  getRuntimeSettings(): AgentRuntimeSettingsSnapshot {
195
354
  const hot = this.getHotContextSettings();
196
355
  return {
@@ -202,6 +361,8 @@ export class Agent {
202
361
  ...(hot?.transitionPaceTokens !== undefined
203
362
  ? { transitionPaceTokens: hot.transitionPaceTokens }
204
363
  : {}),
364
+ sameRoundThinkTextPolicy: this.getEffectiveSameRoundThinkTextPolicy(),
365
+ sameRoundThinkTextPolicySource: this.getSameRoundThinkTextPolicySource(),
205
366
  transition: this.contextBudgetTargetTokens === undefined
206
367
  ? 'stable'
207
368
  : hot?.blocked
@@ -219,6 +380,68 @@ export class Agent {
219
380
  return { ...this.runtimeSettingsOverrides };
220
381
  }
221
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
+
222
445
  updateRuntimeSettings(patch: AgentRuntimeSettingsPatch): AgentRuntimeSettingsSnapshot {
223
446
  this.validateRuntimeSettingsPatch(patch);
224
447
  let nextContextBudget = this.contextBudgetTokens;
@@ -263,13 +486,17 @@ export class Agent {
263
486
  this.runtimeSettingsOverrides.transitionPaceTokens = DEFAULT_TRANSITION_PACE_TOKENS;
264
487
  }
265
488
  for (const [key, value] of Object.entries(patch)) {
266
- if (value !== undefined) (this.runtimeSettingsOverrides as Record<string, number>)[key] = value;
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;
267
494
  }
268
495
  return this.getRuntimeSettings();
269
496
  }
270
497
 
271
498
  resetRuntimeSettings(keys?: Array<keyof AgentRuntimeSettingsPatch>): AgentRuntimeSettingsSnapshot {
272
- const reset = new Set(keys ?? ['contextBudgetTokens', 'tailTokens', 'transitionPaceTokens']);
499
+ const reset = new Set(keys ?? ['contextBudgetTokens', 'tailTokens', 'transitionPaceTokens', 'sameRoundThinkTextPolicy']);
273
500
  let nextContextBudget = this.contextBudgetTokens;
274
501
  let nextContextTarget = this.contextBudgetTargetTokens;
275
502
  const hotPatch: {
@@ -312,6 +539,9 @@ export class Agent {
312
539
  if (reset.has('transitionPaceTokens')) {
313
540
  delete this.runtimeSettingsOverrides.transitionPaceTokens;
314
541
  }
542
+ if (reset.has('sameRoundThinkTextPolicy')) {
543
+ delete this.runtimeSettingsOverrides.sameRoundThinkTextPolicy;
544
+ }
315
545
  return this.getRuntimeSettings();
316
546
  }
317
547
 
@@ -348,6 +578,11 @@ export class Agent {
348
578
  (!Number.isSafeInteger(patch.transitionPaceTokens) || patch.transitionPaceTokens <= 0)) {
349
579
  throw new Error('transitionPaceTokens must be a positive safe integer');
350
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
+ }
351
586
  if ((patch.tailTokens !== undefined || patch.transitionPaceTokens !== undefined) &&
352
587
  !this.getHotContextSettings()) {
353
588
  throw new Error('The active context strategy does not support live tail/transition settings');
@@ -548,64 +783,8 @@ export class Agent {
548
783
  injections?: ContextInjection[],
549
784
  budget?: TokenBudget
550
785
  ): Promise<NormalizedRequest> {
551
- // Keep the context manager's view of the live tool surface current: the
552
- // autobiographical strategy must declare the same tools on its
553
- // summarizer/compression requests, or transcripts containing tool blocks
554
- // are refused by Anthropic's reasoning_extraction classifier (labclaude
555
- // incident, 2026-07-09). Optional chaining: older context-manager
556
- // versions don't have the hook.
557
- (this.contextManager as unknown as { setToolDefinitions?: (t: ToolDefinition[]) => void })
558
- .setToolDefinitions?.(availableTools);
559
-
560
- let { messages, systemInjections } = await this.compileWithInjections(budget, injections);
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
-
584
- // Safety: ensure messages don't end with an assistant message.
585
- // Some models reject trailing assistant messages ("prefill not supported"),
586
- // and after context compression a stale assistant turn can end up last.
587
- if (messages.length > 0 && messages[messages.length - 1]!.participant === this.name) {
588
- messages = [...messages, {
589
- participant: 'user',
590
- content: [{ type: 'text', text: '[Continue]' }],
591
- }];
592
- }
593
-
594
- return {
595
- messages,
596
- system: this.buildSystemPrompt(systemInjections),
597
- config: {
598
- model: this.model,
599
- maxTokens: this.maxTokens,
600
- ...(this.temperature !== undefined && { temperature: this.temperature }),
601
- ...(this.thinking !== undefined && { thinking: this.thinking }),
602
- },
603
- tools: availableTools.length > 0 ? availableTools : undefined,
604
- promptCaching: true,
605
- cacheTtl: this.cacheTtl,
606
- ...(this.providerParams && { providerParams: this.providerParams }),
607
- assistantParticipant: this.name,
608
- };
786
+ const { request } = await this.prepareActivationRequest(availableTools, injections, budget);
787
+ return request;
609
788
  }
610
789
 
611
790
  /**
@@ -625,7 +804,11 @@ export class Agent {
625
804
  this._inferenceStartedAt = Date.now();
626
805
  this.lastStreamInputTokens = 0;
627
806
 
628
- const request = await this.buildActivationRequest(availableTools, injections, budget);
807
+ const { request, artifacts } = await this.prepareActivationRequest(
808
+ availableTools,
809
+ injections,
810
+ budget,
811
+ );
629
812
 
630
813
  const stream = this.membrane.streamYielding(request, {
631
814
  emitTokens: true,
@@ -634,7 +817,27 @@ export class Agent {
634
817
  });
635
818
 
636
819
  this._state = { status: 'streaming', stream };
637
- 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 };
638
841
  }
639
842
 
640
843
  /**
package/src/api/server.ts CHANGED
@@ -468,6 +468,10 @@ export class ApiServer {
468
468
  store.switchBranch(params.name);
469
469
  this.currentBranch = params.name;
470
470
 
471
+ // Reversible Discord awareness markers are part of the branch projection.
472
+ // Do not acknowledge the switch until add/remove operations were attempted.
473
+ await this.framework.syncDiscordAwarenessMarkers();
474
+
471
475
  // Materialize config files from the new branch
472
476
  const ws = this.framework.getModule('workspace');
473
477
  if (ws && 'materializeMount' in ws) {
@@ -511,7 +515,7 @@ export class ApiServer {
511
515
  // Undo/Redo Commands
512
516
  // ==========================================================================
513
517
 
514
- private cmdUndo(params: { agentName: string }): unknown {
518
+ private async cmdUndo(params: { agentName: string }): Promise<unknown> {
515
519
  if (!params.agentName) {
516
520
  throw new Error('agentName is required');
517
521
  }
@@ -520,6 +524,7 @@ export class ApiServer {
520
524
 
521
525
  if (result.undone) {
522
526
  this.currentBranch = result.toBranch!;
527
+ await this.framework.syncDiscordAwarenessMarkers();
523
528
  this.broadcast('branch:switched', {
524
529
  from: result.fromBranch,
525
530
  to: result.toBranch,
@@ -530,7 +535,7 @@ export class ApiServer {
530
535
  return result;
531
536
  }
532
537
 
533
- private cmdRedo(params: { agentName: string }): unknown {
538
+ private async cmdRedo(params: { agentName: string }): Promise<unknown> {
534
539
  if (!params.agentName) {
535
540
  throw new Error('agentName is required');
536
541
  }
@@ -539,6 +544,7 @@ export class ApiServer {
539
544
 
540
545
  if (result.redone) {
541
546
  this.currentBranch = result.toBranch!;
547
+ await this.framework.syncDiscordAwarenessMarkers();
542
548
  this.broadcast('branch:switched', {
543
549
  from: result.fromBranch,
544
550
  to: result.toBranch,
@@ -0,0 +1,22 @@
1
+ function normalizeCanonicalJson(value: unknown): unknown {
2
+ if (Array.isArray(value)) {
3
+ return value.map((entry) => normalizeCanonicalJson(entry));
4
+ }
5
+ if (value && typeof value === 'object') {
6
+ const withToJson = value as { toJSON?: () => unknown };
7
+ if (typeof withToJson.toJSON === 'function') {
8
+ return normalizeCanonicalJson(withToJson.toJSON());
9
+ }
10
+ const normalized: Record<string, unknown> = {};
11
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
12
+ const child = normalizeCanonicalJson((value as Record<string, unknown>)[key]);
13
+ if (child !== undefined) normalized[key] = child;
14
+ }
15
+ return normalized;
16
+ }
17
+ return value;
18
+ }
19
+
20
+ export function canonicalJsonStringify(value: unknown): string {
21
+ return JSON.stringify(normalizeCanonicalJson(value)) ?? 'null';
22
+ }