@animalabs/agent-framework 0.6.7 → 0.6.10

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 (54) hide show
  1. package/dist/src/agent.d.ts +5 -34
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +62 -137
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/framework.d.ts +14 -56
  6. package/dist/src/framework.d.ts.map +1 -1
  7. package/dist/src/framework.js +192 -1742
  8. package/dist/src/framework.js.map +1 -1
  9. package/dist/src/modules/workspace/index.d.ts +10 -0
  10. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  11. package/dist/src/modules/workspace/index.js +48 -2
  12. package/dist/src/modules/workspace/index.js.map +1 -1
  13. package/dist/src/modules/workspace/types.d.ts +2 -0
  14. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  15. package/dist/src/modules/workspace/types.js.map +1 -1
  16. package/dist/src/modules/workspace/watcher.d.ts +17 -0
  17. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  18. package/dist/src/modules/workspace/watcher.js +143 -7
  19. package/dist/src/modules/workspace/watcher.js.map +1 -1
  20. package/dist/src/types/agent.d.ts +15 -12
  21. package/dist/src/types/agent.d.ts.map +1 -1
  22. package/dist/test/helpers/mock-membrane.js +1 -1
  23. package/dist/test/helpers/mock-membrane.js.map +1 -1
  24. package/dist/test/mcpl-awareness-barrier.test.js +1 -228
  25. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  26. package/dist/test/refusal-single-call.test.d.ts +2 -0
  27. package/dist/test/refusal-single-call.test.d.ts.map +1 -0
  28. package/dist/test/refusal-single-call.test.js +177 -0
  29. package/dist/test/refusal-single-call.test.js.map +1 -0
  30. package/dist/test/trunk-channel-routing.test.js +5 -1
  31. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  32. package/dist/test/workspace-watcher.test.js +161 -9
  33. package/dist/test/workspace-watcher.test.js.map +1 -1
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +4 -4
  36. package/src/agent.ts +67 -226
  37. package/src/framework.ts +226 -2394
  38. package/src/modules/workspace/index.ts +47 -2
  39. package/src/modules/workspace/types.ts +2 -0
  40. package/src/modules/workspace/watcher.ts +160 -7
  41. package/src/types/agent.ts +17 -12
  42. package/dist/src/canonical-json.d.ts +0 -2
  43. package/dist/src/canonical-json.d.ts.map +0 -1
  44. package/dist/src/canonical-json.js +0 -23
  45. package/dist/src/canonical-json.js.map +0 -1
  46. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts +0 -2
  47. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts.map +0 -1
  48. package/dist/test/primary-summary-refusal-fallback-integration.test.js +0 -1955
  49. package/dist/test/primary-summary-refusal-fallback-integration.test.js.map +0 -1
  50. package/dist/test/primary-summary-refusal-fallback.test.d.ts +0 -2
  51. package/dist/test/primary-summary-refusal-fallback.test.d.ts.map +0 -1
  52. package/dist/test/primary-summary-refusal-fallback.test.js +0 -753
  53. package/dist/test/primary-summary-refusal-fallback.test.js.map +0 -1
  54. package/src/canonical-json.ts +0 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animalabs/agent-framework",
3
- "version": "0.6.7",
3
+ "version": "0.6.10",
4
4
  "description": "Multi-agent framework with pluggable modules, persistent state, and concurrent tool execution",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,8 +46,8 @@
46
46
  "license": "MIT",
47
47
  "dependencies": {
48
48
  "@animalabs/chronicle": "^0.2.2",
49
- "@animalabs/context-manager": "^0.5.12",
50
- "@animalabs/membrane": "^0.5.73",
49
+ "@animalabs/context-manager": "^0.5.13",
50
+ "@animalabs/membrane": "^0.5.74",
51
51
  "chokidar": "^4.0.3",
52
52
  "discord.js": "^14.25.1",
53
53
  "ws": "^8.18.0"
@@ -62,4 +62,4 @@
62
62
  "engines": {
63
63
  "node": ">=20.0.0"
64
64
  }
65
- }
65
+ }
package/src/agent.ts CHANGED
@@ -1,20 +1,10 @@
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';
4
3
  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
- }
13
4
 
14
5
  export interface StartStreamResult {
15
6
  stream: YieldingStream;
16
7
  request: NormalizedRequest;
17
- artifacts: ActivationCompileArtifacts;
18
8
  }
19
9
  import type {
20
10
  ContextManager,
@@ -23,9 +13,6 @@ import type {
23
13
  CompileResult,
24
14
  HotContextSettingsStatus,
25
15
  HotContextSettingsUpdate,
26
- PrimarySummaryContract,
27
- PrimarySummaryIdentity,
28
- PrimarySummaryProjection,
29
16
  } from '@animalabs/context-manager';
30
17
  import type {
31
18
  AgentConfig,
@@ -46,10 +33,6 @@ import type {
46
33
  AgentRuntimeSettingsOverrides,
47
34
  } from './types/index.js';
48
35
 
49
- type PrimarySummaryContractWithSystemHash = PrimarySummaryContract & {
50
- systemHash: string;
51
- };
52
-
53
36
  const DEFAULT_CONTEXT_BUDGET_TOKENS = 100_000;
54
37
  const DEFAULT_TRANSITION_PACE_TOKENS = 16_000;
55
38
  const DEFAULT_SAME_ROUND_THINK_TEXT_POLICY: SameRoundThinkTextPolicy = 'public';
@@ -81,6 +64,10 @@ export class Agent {
81
64
  readonly refusalHandling: AgentConfig['refusalHandling'];
82
65
  /** Prompt-cache TTL forwarded to the provider (see AgentConfig.cacheTtl). */
83
66
  readonly cacheTtl: NonNullable<AgentConfig['cacheTtl']>;
67
+ /** Emit prompt-cache markers on requests (see AgentConfig.promptCaching). */
68
+ readonly promptCaching: boolean;
69
+ /** Prefill scaffold user message (see AgentConfig.prefillUserMessage). */
70
+ readonly prefillUserMessage?: string;
84
71
  /** Provider-specific request parameters forwarded unchanged by Membrane. */
85
72
  readonly providerParams?: Record<string, unknown>;
86
73
  readonly sameRoundThinkTextPolicy: AgentConfig['sameRoundThinkTextPolicy'];
@@ -118,6 +105,8 @@ export class Agent {
118
105
  this.thinking = config.thinking;
119
106
  this.refusalHandling = config.refusalHandling;
120
107
  this.cacheTtl = config.cacheTtl ?? '1h';
108
+ this.promptCaching = config.promptCaching ?? true;
109
+ this.prefillUserMessage = config.prefillUserMessage;
121
110
  this.providerParams = config.providerParams;
122
111
  this.sameRoundThinkTextPolicy = config.sameRoundThinkTextPolicy;
123
112
  this.maxStreamTokens = config.maxStreamTokens ?? 150_000;
@@ -215,141 +204,6 @@ export class Agent {
215
204
  return manager.updateHotContextSettings(update);
216
205
  }
217
206
 
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
207
  getRuntimeSettings(): AgentRuntimeSettingsSnapshot {
354
208
  const hot = this.getHotContextSettings();
355
209
  return {
@@ -396,52 +250,6 @@ export class Agent {
396
250
  return 'compatibility_default';
397
251
  }
398
252
 
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
253
  updateRuntimeSettings(patch: AgentRuntimeSettingsPatch): AgentRuntimeSettingsSnapshot {
446
254
  this.validateRuntimeSettingsPatch(patch);
447
255
  let nextContextBudget = this.contextBudgetTokens;
@@ -783,8 +591,65 @@ export class Agent {
783
591
  injections?: ContextInjection[],
784
592
  budget?: TokenBudget
785
593
  ): Promise<NormalizedRequest> {
786
- const { request } = await this.prepareActivationRequest(availableTools, injections, budget);
787
- return request;
594
+ // Keep the context manager's view of the live tool surface current: the
595
+ // autobiographical strategy must declare the same tools on its
596
+ // summarizer/compression requests, or transcripts containing tool blocks
597
+ // are refused by Anthropic's reasoning_extraction classifier (labclaude
598
+ // incident, 2026-07-09). Optional chaining: older context-manager
599
+ // versions don't have the hook.
600
+ (this.contextManager as unknown as { setToolDefinitions?: (t: ToolDefinition[]) => void })
601
+ .setToolDefinitions?.(availableTools);
602
+
603
+ let { messages, systemInjections } = await this.compileWithInjections(budget, injections);
604
+
605
+ // Sanitize: strip empty/whitespace text blocks and drop messages left with
606
+ // no content. The Anthropic API rejects empty text blocks with 400
607
+ // "messages: text content blocks must be non-empty"; when such a block
608
+ // reaches a live activation request the agent cannot complete ANY turn
609
+ // ([inference-failed] on every attempt) until the offending message ages
610
+ // out of the window. Empty text blocks occur naturally: tool-only turns,
611
+ // delivery-failure placeholders, silent/skip turns. Non-text blocks pass
612
+ // through unchanged, so tool pairing is unaffected. (Field-observed
613
+ // 2026-07-10 on a resident agent: one empty block muted the agent's live
614
+ // path entirely; twin of context-manager's stripEmptyTextBlocks on the
615
+ // compression path.)
616
+ messages = messages
617
+ .map((m) => ({
618
+ ...m,
619
+ content: m.content.filter(
620
+ // typeof guard is deliberate runtime defense: history loaded from
621
+ // disk can carry a non-string `text` despite what the types claim.
622
+ (b: ContentBlock) => !(b.type === "text" && (typeof b.text !== "string" || b.text.trim() === "")),
623
+ ),
624
+ }))
625
+ .filter((m) => m.content.length > 0);
626
+
627
+ // Safety: ensure messages don't end with an assistant message.
628
+ // Some models reject trailing assistant messages ("prefill not supported"),
629
+ // and after context compression a stale assistant turn can end up last.
630
+ if (messages.length > 0 && messages[messages.length - 1]!.participant === this.name) {
631
+ messages = [...messages, {
632
+ participant: 'user',
633
+ content: [{ type: 'text', text: '[Continue]' }],
634
+ }];
635
+ }
636
+
637
+ return {
638
+ messages,
639
+ system: this.buildSystemPrompt(systemInjections),
640
+ config: {
641
+ model: this.model,
642
+ maxTokens: this.maxTokens,
643
+ ...(this.temperature !== undefined && { temperature: this.temperature }),
644
+ ...(this.thinking !== undefined && { thinking: this.thinking }),
645
+ },
646
+ tools: availableTools.length > 0 ? availableTools : undefined,
647
+ promptCaching: this.promptCaching,
648
+ cacheTtl: this.cacheTtl,
649
+ ...(this.prefillUserMessage && { prefillUserMessage: this.prefillUserMessage }),
650
+ ...(this.providerParams && { providerParams: this.providerParams }),
651
+ assistantParticipant: this.name,
652
+ };
788
653
  }
789
654
 
790
655
  /**
@@ -804,11 +669,7 @@ export class Agent {
804
669
  this._inferenceStartedAt = Date.now();
805
670
  this.lastStreamInputTokens = 0;
806
671
 
807
- const { request, artifacts } = await this.prepareActivationRequest(
808
- availableTools,
809
- injections,
810
- budget,
811
- );
672
+ const request = await this.buildActivationRequest(availableTools, injections, budget);
812
673
 
813
674
  const stream = this.membrane.streamYielding(request, {
814
675
  emitTokens: true,
@@ -817,27 +678,7 @@ export class Agent {
817
678
  });
818
679
 
819
680
  this._state = { status: 'streaming', stream };
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 };
681
+ return { stream, request };
841
682
  }
842
683
 
843
684
  /**