@aigne/core 1.72.0-beta.16 → 1.72.0-beta.18

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.72.0-beta.18](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.72.0-beta.17...core-v1.72.0-beta.18) (2026-01-13)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * bump deps to latest and fix build error ([#897](https://github.com/AIGNE-io/aigne-framework/issues/897)) ([4059e79](https://github.com/AIGNE-io/aigne-framework/commit/4059e790ae63b9e4ebd66487665014b0cd7ce6ec))
9
+ * **core:** make async memory updates non-blocking ([#900](https://github.com/AIGNE-io/aigne-framework/issues/900)) ([314f2c3](https://github.com/AIGNE-io/aigne-framework/commit/314f2c35d8baa88b600cc4de3f5983fef03a804c))
10
+
11
+
12
+ ### Dependencies
13
+
14
+ * The following workspace dependencies were updated
15
+ * dependencies
16
+ * @aigne/observability-api bumped to 0.11.14-beta.3
17
+
18
+ ## [1.72.0-beta.17](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.72.0-beta.16...core-v1.72.0-beta.17) (2026-01-12)
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * **core:** optimize session compaction to reduce compression frequency ([#894](https://github.com/AIGNE-io/aigne-framework/issues/894)) ([bed53dc](https://github.com/AIGNE-io/aigne-framework/commit/bed53dc0311c69acd2c257fe93416d10ac1120e1))
24
+
3
25
  ## [1.72.0-beta.16](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.72.0-beta.15...core-v1.72.0-beta.16) (2026-01-12)
4
26
 
5
27
 
@@ -470,9 +470,8 @@ class AIAgent extends agent_js_1.Agent {
470
470
  else {
471
471
  yield { delta: { text: { [outputKey]: "\n\n" } } };
472
472
  }
473
- const message = { role: "agent", toolCalls };
474
- yield { progress: { event: "message", message } };
475
- await session.appendCurrentMessages(message, options);
473
+ const toolCallMessage = { role: "agent", toolCalls };
474
+ yield { progress: { event: "message", message: toolCallMessage } };
476
475
  const executedToolCalls = [];
477
476
  let error;
478
477
  const queue = fastq.promise(async ({ tool, call }) => {
@@ -503,6 +502,7 @@ class AIAgent extends agent_js_1.Agent {
503
502
  await queue.drained();
504
503
  if (error)
505
504
  throw error;
505
+ const toolResultMessages = [];
506
506
  // Continue LLM function calling loop if any tools were executed
507
507
  if (executedToolCalls.length) {
508
508
  for (const { call, tool, output } of executedToolCalls) {
@@ -514,11 +514,12 @@ class AIAgent extends agent_js_1.Agent {
514
514
  content: [{ type: "text", text, isAgentSkill }],
515
515
  };
516
516
  yield { progress: { event: "message", message: message } };
517
- await session.appendCurrentMessages(message, options);
517
+ toolResultMessages.push(message);
518
518
  }
519
519
  const transferOutput = executedToolCalls.find((i) => (0, types_js_1.isTransferAgentOutput)(i.output))?.output;
520
520
  if (transferOutput)
521
521
  return transferOutput;
522
+ await session.appendCurrentMessages([toolCallMessage, ...toolResultMessages], options);
522
523
  continue;
523
524
  }
524
525
  }
@@ -80,7 +80,9 @@ export declare class AgentSession {
80
80
  private maybeCompactCurrentEntry;
81
81
  private maybeAutoCompact;
82
82
  /**
83
- * Estimate token count for an array of messages
83
+ * Estimate token count for messages
84
+ * Applies singleMessageLimit to each text block individually
85
+ * Non-text tokens (images, tool calls) are always counted in full
84
86
  */
85
87
  private estimateMessagesTokens;
86
88
  /**
@@ -89,6 +91,14 @@ export declare class AgentSession {
89
91
  */
90
92
  private splitIntoBatches;
91
93
  appendCurrentMessages(messages: ChatModelInputMessage | ChatModelInputMessage[], options: AgentInvokeOptions): Promise<void>;
94
+ /**
95
+ * Truncate text content to fit within target token limit
96
+ * @param text The text to truncate
97
+ * @param currentTokens Current token count of the text
98
+ * @param targetTokens Target token count after truncation
99
+ * @returns Truncated text
100
+ */
101
+ private truncateText;
92
102
  private truncateLargeMessage;
93
103
  private ensureInitialized;
94
104
  private initialize;
@@ -148,6 +158,6 @@ export declare class AgentSession {
148
158
  private initializeDefaultUserMemoryExtractor;
149
159
  private get maxTokens();
150
160
  private get keepRecentRatio();
151
- private get keepTokenBudget();
161
+ private get keepRecentTokens();
152
162
  private get singleMessageLimit();
153
163
  }
@@ -41,6 +41,7 @@ const afs_history_1 = require("@aigne/afs-history");
41
41
  const uuid_1 = require("@aigne/uuid");
42
42
  const ufo_1 = require("ufo");
43
43
  const yaml_1 = require("yaml");
44
+ const logger_js_1 = require("../utils/logger.js");
44
45
  const token_estimator_js_1 = require("../utils/token-estimator.js");
45
46
  const type_utils_js_1 = require("../utils/type-utils.js");
46
47
  const types_js_1 = require("./compact/types.js");
@@ -290,7 +291,9 @@ ${"```"}
290
291
  this.compactionPromise = this.doCompact(options).finally(() => {
291
292
  this.compactionPromise = undefined;
292
293
  });
293
- return this.compactionPromise;
294
+ const isAsync = this.compactConfig.async ?? types_js_1.DEFAULT_COMPACT_ASYNC;
295
+ if (!isAsync)
296
+ await this.compactionPromise;
294
297
  }
295
298
  /**
296
299
  * Internal method that performs the actual compaction
@@ -304,20 +307,18 @@ ${"```"}
304
307
  if (historyEntries.length === 0)
305
308
  return;
306
309
  const maxTokens = this.maxTokens;
307
- let keepTokenBudget = this.keepTokenBudget;
308
- // Calculate tokens for system messages
309
- const systemTokens = (this.runtimeState.systemMessages ?? []).reduce((sum, msg) => {
310
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
311
- return sum + (0, token_estimator_js_1.estimateTokens)(content);
312
- }, 0);
313
- // Calculate tokens for current entry messages
314
- const currentTokens = (this.runtimeState.currentEntry?.messages ?? []).reduce((sum, msg) => {
315
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
316
- return sum + (0, token_estimator_js_1.estimateTokens)(content);
317
- }, 0);
318
- // Subtract system and current tokens from budget
319
- // This ensures total tokens (system + current + kept history) stays within ratio budget
320
- keepTokenBudget = Math.max(0, keepTokenBudget - systemTokens - currentTokens);
310
+ // Target to keep only 50% of keepRecentTokens to leave buffer room
311
+ // This avoids triggering compression again shortly after compaction
312
+ // Similar to compactCurrentEntry, we compress more aggressively to leave headroom
313
+ //
314
+ // Note: We don't subtract systemTokens or currentEntry tokens because:
315
+ // 1. keepRecentTokens is already a relative ratio (e.g., 50% of maxTokens)
316
+ // 2. systemTokens overhead is typically small (~1-2k, ~1-2% of maxTokens)
317
+ // 3. currentEntry is still being constructed (not yet added to history)
318
+ // 4. In tool use scenarios, currentEntry can be very large (many tool calls)
319
+ // 5. Subtracting them would complicate logic without significant benefit
320
+ // 6. Total token limit is enforced by maybeAutoCompact trigger condition
321
+ const keepRecentTokens = this.keepRecentTokens * 0.5;
321
322
  // Find split point by iterating backwards from most recent entry
322
323
  // The split point divides history into: [compact] | [keep]
323
324
  let splitIndex = historyEntries.length; // Default: keep all (no compaction)
@@ -328,7 +329,7 @@ ${"```"}
328
329
  continue;
329
330
  const entryTokens = this.estimateMessagesTokens(entry.content?.messages ?? []);
330
331
  // Check if adding this entry would exceed token budget
331
- if (accumulatedTokens + entryTokens > keepTokenBudget) {
332
+ if (accumulatedTokens + entryTokens > keepRecentTokens) {
332
333
  // Would exceed budget, split here (this entry and earlier ones will be compacted)
333
334
  splitIndex = i + 1;
334
335
  break;
@@ -399,22 +400,21 @@ ${"```"}
399
400
  const uncompressedMessages = currentEntry.messages.slice(alreadyCompressedCount);
400
401
  if (uncompressedMessages.length === 0)
401
402
  return;
402
- const keepTokenBudget = this.keepTokenBudget;
403
- const singleMessageLimit = this.singleMessageLimit;
403
+ // Target to keep only 50% of keepTokenBudget to leave buffer room
404
+ // This avoids frequent small-batch compressions in tool use scenarios
405
+ const keepTokenBudget = this.keepRecentTokens * 0.5;
404
406
  let splitIndex = uncompressedMessages.length;
405
407
  let accumulatedTokens = 0;
406
408
  for (let i = uncompressedMessages.length - 1; i >= 0; i--) {
407
409
  const msg = uncompressedMessages[i];
408
410
  if (!msg)
409
411
  continue;
410
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
411
- const msgTokens = (0, token_estimator_js_1.estimateTokens)(content);
412
- const effectiveTokens = msgTokens > singleMessageLimit ? singleMessageLimit : msgTokens;
413
- if (accumulatedTokens + effectiveTokens > keepTokenBudget) {
412
+ const msgTokens = this.estimateMessagesTokens([msg]);
413
+ if (accumulatedTokens + msgTokens > keepTokenBudget) {
414
414
  splitIndex = i + 1;
415
415
  break;
416
416
  }
417
- accumulatedTokens += effectiveTokens;
417
+ accumulatedTokens += msgTokens;
418
418
  splitIndex = i;
419
419
  }
420
420
  const keptMessages = uncompressedMessages.slice(splitIndex);
@@ -463,8 +463,8 @@ ${"```"}
463
463
  return;
464
464
  const compressedCount = this.runtimeState.currentEntryCompact?.compressedCount ?? 0;
465
465
  const uncompressedMessages = currentEntry.messages.slice(compressedCount);
466
- const threshold = this.keepTokenBudget;
467
- const currentTokens = this.estimateMessagesTokens(uncompressedMessages, this.singleMessageLimit);
466
+ const threshold = this.keepRecentTokens;
467
+ const currentTokens = this.estimateMessagesTokens(uncompressedMessages);
468
468
  if (currentTokens > threshold) {
469
469
  await this.compactCurrentEntry(options);
470
470
  }
@@ -472,8 +472,6 @@ ${"```"}
472
472
  async maybeAutoCompact(options) {
473
473
  if (this.compactionPromise)
474
474
  await this.compactionPromise;
475
- if (!this.compactConfig)
476
- return;
477
475
  const mode = this.compactConfig.mode ?? types_js_1.DEFAULT_COMPACT_MODE;
478
476
  if (mode === "disabled")
479
477
  return;
@@ -484,24 +482,48 @@ ${"```"}
484
482
  const messages = await this.getMessages();
485
483
  const currentTokens = this.estimateMessagesTokens(messages);
486
484
  if (currentTokens >= maxTokens) {
487
- this.compact(options);
488
- const isAsync = this.compactConfig.async ?? types_js_1.DEFAULT_COMPACT_ASYNC;
489
- if (!isAsync)
490
- await this.compactionPromise;
485
+ await this.compact(options);
491
486
  }
492
487
  }
493
488
  /**
494
- * Estimate token count for an array of messages
489
+ * Estimate token count for messages
490
+ * Applies singleMessageLimit to each text block individually
491
+ * Non-text tokens (images, tool calls) are always counted in full
495
492
  */
496
- estimateMessagesTokens(messages, singleMessageLimit) {
497
- return messages.reduce((sum, msg) => {
498
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
499
- const tokens = (0, token_estimator_js_1.estimateTokens)(content);
500
- if (singleMessageLimit && tokens > singleMessageLimit) {
501
- return sum + singleMessageLimit;
493
+ estimateMessagesTokens(messages, singleMessageLimit = this.singleMessageLimit) {
494
+ let totalTokens = 0;
495
+ for (const msg of messages) {
496
+ // 1. Estimate content tokens
497
+ if (typeof msg.content === "string") {
498
+ const textTokens = (0, token_estimator_js_1.estimateTokens)(msg.content);
499
+ const effectiveTokens = textTokens > singleMessageLimit ? singleMessageLimit : textTokens;
500
+ totalTokens += effectiveTokens;
502
501
  }
503
- return sum + tokens;
504
- }, 0);
502
+ else if (Array.isArray(msg.content)) {
503
+ for (const block of msg.content) {
504
+ if (block.type === "text" && typeof block.text === "string") {
505
+ // Text tokens (can be truncated) - apply limit to each block individually
506
+ const textTokens = (0, token_estimator_js_1.estimateTokens)(block.text);
507
+ const effectiveTokens = textTokens > singleMessageLimit ? singleMessageLimit : textTokens;
508
+ totalTokens += effectiveTokens;
509
+ }
510
+ else {
511
+ // Non-text blocks - always counted in full
512
+ totalTokens += 1000;
513
+ }
514
+ }
515
+ }
516
+ // 2. Estimate tool calls tokens (cannot be truncated)
517
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
518
+ for (const toolCall of msg.toolCalls) {
519
+ // Function name + arguments + overhead
520
+ totalTokens += (0, token_estimator_js_1.estimateTokens)(toolCall.function.name);
521
+ totalTokens += (0, token_estimator_js_1.estimateTokens)((0, yaml_1.stringify)(toolCall.function.arguments).replace(/\s+/g, " "));
522
+ totalTokens += 10; // Structure overhead
523
+ }
524
+ }
525
+ }
526
+ return totalTokens;
505
527
  }
506
528
  /**
507
529
  * Split entries into batches based on token limit
@@ -538,22 +560,54 @@ ${"```"}
538
560
  this.runtimeState.currentEntry.messages.push(...[messages].flat());
539
561
  await this.maybeCompactCurrentEntry(options);
540
562
  }
541
- truncateLargeMessage(msg) {
542
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
543
- const tokens = (0, token_estimator_js_1.estimateTokens)(content);
544
- const singleMessageLimit = this.singleMessageLimit;
545
- if (tokens <= singleMessageLimit)
546
- return msg;
547
- const keepRatio = (singleMessageLimit / tokens) * 0.9;
548
- const keepLength = Math.floor(content.length * keepRatio);
563
+ /**
564
+ * Truncate text content to fit within target token limit
565
+ * @param text The text to truncate
566
+ * @param currentTokens Current token count of the text
567
+ * @param targetTokens Target token count after truncation
568
+ * @returns Truncated text
569
+ */
570
+ truncateText(text, currentTokens, targetTokens) {
571
+ if (currentTokens <= targetTokens)
572
+ return text;
573
+ const keepRatio = (targetTokens / currentTokens) * 0.9;
574
+ const keepLength = Math.floor(text.length * keepRatio);
549
575
  const headLength = Math.floor(keepLength * 0.7);
550
576
  const tailLength = Math.floor(keepLength * 0.3);
551
- const truncated = content.slice(0, headLength) +
552
- `\n\n[... Content too large, truncated ${tokens - singleMessageLimit} tokens ...]\n\n` +
553
- content.slice(-tailLength);
577
+ return (text.slice(0, headLength) +
578
+ `\n\n[... truncated ${currentTokens - targetTokens} tokens ...]\n\n` +
579
+ text.slice(-tailLength));
580
+ }
581
+ truncateLargeMessage(msg) {
582
+ const singleMessageLimit = this.singleMessageLimit;
583
+ // Handle string content
554
584
  if (typeof msg.content === "string") {
585
+ const tokens = (0, token_estimator_js_1.estimateTokens)(msg.content);
586
+ if (tokens <= singleMessageLimit)
587
+ return msg;
588
+ const truncated = this.truncateText(msg.content, tokens, singleMessageLimit);
555
589
  return { ...msg, content: truncated };
556
590
  }
591
+ // Handle array content (UnionContent[])
592
+ if (Array.isArray(msg.content)) {
593
+ // Truncate each text block individually if it exceeds the limit
594
+ const truncatedContent = msg.content.map((block) => {
595
+ // Keep non-text blocks unchanged
596
+ if (block.type !== "text" || typeof block.text !== "string") {
597
+ return block;
598
+ }
599
+ // Check if this text block needs truncation
600
+ const blockTokens = (0, token_estimator_js_1.estimateTokens)(block.text);
601
+ if (blockTokens <= singleMessageLimit) {
602
+ return block;
603
+ }
604
+ // Truncate this text block independently
605
+ const truncatedText = this.truncateText(block.text, blockTokens, singleMessageLimit);
606
+ return { ...block, text: truncatedText };
607
+ });
608
+ return { ...msg, content: truncatedContent };
609
+ }
610
+ // Unknown content type, return as-is
557
611
  return msg;
558
612
  }
559
613
  async ensureInitialized() {
@@ -671,53 +725,46 @@ ${"```"}
671
725
  */
672
726
  async updateSessionMemory(options) {
673
727
  await this.ensureInitialized();
674
- // If session memory update is already in progress, wait for it to complete
675
- if (this.sessionMemoryUpdatePromise) {
676
- return this.sessionMemoryUpdatePromise;
677
- }
678
- // Start new session memory update task
679
- this.sessionMemoryUpdatePromise = this.doUpdateSessionMemory(options).finally(() => {
728
+ this.sessionMemoryUpdatePromise ??= this.doUpdateSessionMemory(options)
729
+ .then(() => {
730
+ // After session memory update succeeds, potentially trigger user memory consolidation
731
+ this.maybeAutoUpdateUserMemory(options).catch((err) => {
732
+ logger_js_1.logger.error("User memory update failed:", err);
733
+ });
734
+ })
735
+ .finally(() => {
680
736
  this.sessionMemoryUpdatePromise = undefined;
681
- // After session memory update completes, potentially trigger user memory consolidation
682
- this.maybeAutoUpdateUserMemory(options);
683
737
  });
684
738
  return this.sessionMemoryUpdatePromise;
685
739
  }
686
740
  async maybeAutoUpdateSessionMemory(options) {
687
- if (this.sessionMemoryUpdatePromise)
688
- await this.sessionMemoryUpdatePromise;
689
741
  // Check if memory extraction is enabled (requires AFS history module)
690
742
  if (!this.isMemoryEnabled)
691
743
  return;
692
- if (!this.sessionMemoryConfig)
693
- return;
694
744
  // Check if mode is disabled
695
745
  const mode = this.sessionMemoryConfig.mode ?? types_js_1.DEFAULT_SESSION_MEMORY_MODE;
696
746
  if (mode === "disabled")
697
747
  return;
698
748
  // Trigger session memory update
699
- this.updateSessionMemory(options);
749
+ this.updateSessionMemory(options).catch((err) => {
750
+ logger_js_1.logger.error("Session memory update failed:", err);
751
+ });
700
752
  const isAsync = this.sessionMemoryConfig.async ?? types_js_1.DEFAULT_SESSION_MEMORY_ASYNC;
701
753
  if (!isAsync)
702
754
  await this.sessionMemoryUpdatePromise;
703
755
  }
704
756
  async maybeAutoUpdateUserMemory(options) {
705
- if (this.userMemoryUpdatePromise)
706
- await this.userMemoryUpdatePromise;
707
757
  // Check if memory extraction is enabled (requires AFS history module)
708
- if (!this.isMemoryEnabled)
709
- return;
710
- if (!this.userMemoryConfig || !this.userId)
758
+ if (!this.isMemoryEnabled || !this.userId)
711
759
  return;
712
760
  // Check if mode is disabled
713
761
  const mode = this.userMemoryConfig.mode ?? types_js_1.DEFAULT_USER_MEMORY_MODE;
714
762
  if (mode === "disabled")
715
763
  return;
716
- // Wait for session memory update to complete first
717
- if (this.sessionMemoryUpdatePromise)
718
- await this.sessionMemoryUpdatePromise;
719
764
  // Trigger user memory consolidation
720
- this.updateUserMemory(options);
765
+ this.updateUserMemory(options).catch((err) => {
766
+ logger_js_1.logger.error("User memory update failed:", err);
767
+ });
721
768
  const isAsync = this.userMemoryConfig.async ?? types_js_1.DEFAULT_USER_MEMORY_ASYNC;
722
769
  if (!isAsync)
723
770
  await this.userMemoryUpdatePromise;
@@ -812,12 +859,8 @@ ${"```"}
812
859
  */
813
860
  async updateUserMemory(options) {
814
861
  await this.ensureInitialized();
815
- // If user memory update is already in progress, wait for it to complete
816
- if (this.userMemoryUpdatePromise) {
817
- return this.userMemoryUpdatePromise;
818
- }
819
862
  // Start new user memory update task
820
- this.userMemoryUpdatePromise = this.doUpdateUserMemory(options).finally(() => {
863
+ this.userMemoryUpdatePromise ??= this.doUpdateUserMemory(options).finally(() => {
821
864
  this.userMemoryUpdatePromise = undefined;
822
865
  });
823
866
  return this.userMemoryUpdatePromise;
@@ -955,11 +998,11 @@ ${"```"}
955
998
  get keepRecentRatio() {
956
999
  return this.compactConfig?.keepRecentRatio ?? types_js_1.DEFAULT_KEEP_RECENT_RATIO;
957
1000
  }
958
- get keepTokenBudget() {
1001
+ get keepRecentTokens() {
959
1002
  return Math.floor(this.maxTokens * this.keepRecentRatio);
960
1003
  }
961
1004
  get singleMessageLimit() {
962
- return this.keepTokenBudget * 0.5;
1005
+ return this.keepRecentTokens * 0.5;
963
1006
  }
964
1007
  }
965
1008
  exports.AgentSession = AgentSession;
@@ -41,6 +41,7 @@ class AISessionCompactor extends ai_agent_js_1.AIAgent {
41
41
  summary: zod_1.z.string().describe("A comprehensive summary of the conversation history"),
42
42
  }),
43
43
  instructions: COMPACTOR_INSTRUCTIONS,
44
+ taskRenderMode: "hide",
44
45
  ...(0, type_utils_js_1.omitBy)(options ?? {}, (v) => (0, type_utils_js_1.isNil)(v)),
45
46
  session: {
46
47
  mode: "disabled",
@@ -132,6 +132,7 @@ class AISessionMemoryExtractor extends ai_agent_js_1.AIAgent {
132
132
  removeFacts: (0, zod_1.optional)(zod_1.z.array(zod_1.z.string()).describe("Labels of facts to remove from memory")),
133
133
  }),
134
134
  instructions: EXTRACTOR_INSTRUCTIONS,
135
+ taskRenderMode: "hide",
135
136
  ...(0, type_utils_js_1.omitBy)(options ?? {}, (v) => (0, type_utils_js_1.isNil)(v)),
136
137
  session: {
137
138
  mode: "disabled",
@@ -113,6 +113,7 @@ class AIUserMemoryExtractor extends ai_agent_js_1.AIAgent {
113
113
  removeFacts: (0, zod_1.optional)(zod_1.z.array(zod_1.z.string()).describe("Labels of facts to remove from user memory")),
114
114
  }),
115
115
  instructions: EXTRACTOR_INSTRUCTIONS,
116
+ taskRenderMode: "hide",
116
117
  ...(0, type_utils_js_1.omitBy)(options ?? {}, (v) => (0, type_utils_js_1.isNil)(v)),
117
118
  session: {
118
119
  mode: "disabled",
@@ -40,10 +40,10 @@ class PromptBuilder {
40
40
  content = i.content.text;
41
41
  else if (i.content.type === "resource") {
42
42
  const { resource } = i.content;
43
- if (typeof resource.text === "string") {
43
+ if ("text" in resource && typeof resource.text === "string") {
44
44
  content = resource.text;
45
45
  }
46
- else if (typeof resource.blob === "string") {
46
+ else if ("blob" in resource && typeof resource.blob === "string") {
47
47
  content = [{ type: "url", url: resource.blob }];
48
48
  }
49
49
  }
@@ -8,6 +8,7 @@ export interface Skill {
8
8
  }
9
9
  export declare function loadSkill(path: string): Promise<Skill>;
10
10
  export declare function loadSkills(paths: string[]): Promise<Skill[]>;
11
+ export declare function discoverSkillsFromAFS(afs: AFS): Promise<Skill[]>;
11
12
  export declare function loadAgentSkillFromAFS({ afs, }: {
12
13
  afs: AFS;
13
14
  }): Promise<AgentSkill | undefined>;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.loadSkill = loadSkill;
7
7
  exports.loadSkills = loadSkills;
8
+ exports.discoverSkillsFromAFS = discoverSkillsFromAFS;
8
9
  exports.loadAgentSkillFromAFS = loadAgentSkillFromAFS;
9
10
  const index_js_1 = require("@aigne/platform-helpers/nodejs/index.js");
10
11
  const front_matter_1 = __importDefault(require("front-matter"));
@@ -31,7 +32,7 @@ async function loadSkills(paths) {
31
32
  }
32
33
  return skills;
33
34
  }
34
- async function loadAgentSkillFromAFS({ afs, }) {
35
+ async function discoverSkillsFromAFS(afs) {
35
36
  const modules = await afs.listModules();
36
37
  const filtered = modules.filter(({ module: m }) => "options" in m &&
37
38
  typeof m.options === "object" &&
@@ -39,7 +40,7 @@ async function loadAgentSkillFromAFS({ afs, }) {
39
40
  "agentSkills" in m.options &&
40
41
  m.options.agentSkills === true);
41
42
  if (!filtered.length)
42
- return;
43
+ return [];
43
44
  const skills = [];
44
45
  for (const module of filtered) {
45
46
  const data = (await afs
@@ -56,6 +57,10 @@ async function loadAgentSkillFromAFS({ afs, }) {
56
57
  skills.push(skill);
57
58
  }
58
59
  }
60
+ return skills;
61
+ }
62
+ async function loadAgentSkillFromAFS({ afs, }) {
63
+ const skills = await discoverSkillsFromAFS(afs);
59
64
  if (!skills.length)
60
65
  return;
61
66
  return new agent_skill_js_1.AgentSkill({
@@ -48,5 +48,5 @@ function resourceFromMCPResource(resource, options) {
48
48
  });
49
49
  }
50
50
  function isResourceTemplate(resource) {
51
- return typeof resource.uriTemplate === "string";
51
+ return "uriTemplate" in resource && typeof resource.uriTemplate === "string";
52
52
  }
@@ -80,7 +80,9 @@ export declare class AgentSession {
80
80
  private maybeCompactCurrentEntry;
81
81
  private maybeAutoCompact;
82
82
  /**
83
- * Estimate token count for an array of messages
83
+ * Estimate token count for messages
84
+ * Applies singleMessageLimit to each text block individually
85
+ * Non-text tokens (images, tool calls) are always counted in full
84
86
  */
85
87
  private estimateMessagesTokens;
86
88
  /**
@@ -89,6 +91,14 @@ export declare class AgentSession {
89
91
  */
90
92
  private splitIntoBatches;
91
93
  appendCurrentMessages(messages: ChatModelInputMessage | ChatModelInputMessage[], options: AgentInvokeOptions): Promise<void>;
94
+ /**
95
+ * Truncate text content to fit within target token limit
96
+ * @param text The text to truncate
97
+ * @param currentTokens Current token count of the text
98
+ * @param targetTokens Target token count after truncation
99
+ * @returns Truncated text
100
+ */
101
+ private truncateText;
92
102
  private truncateLargeMessage;
93
103
  private ensureInitialized;
94
104
  private initialize;
@@ -148,6 +158,6 @@ export declare class AgentSession {
148
158
  private initializeDefaultUserMemoryExtractor;
149
159
  private get maxTokens();
150
160
  private get keepRecentRatio();
151
- private get keepTokenBudget();
161
+ private get keepRecentTokens();
152
162
  private get singleMessageLimit();
153
163
  }
@@ -8,6 +8,7 @@ export interface Skill {
8
8
  }
9
9
  export declare function loadSkill(path: string): Promise<Skill>;
10
10
  export declare function loadSkills(paths: string[]): Promise<Skill[]>;
11
+ export declare function discoverSkillsFromAFS(afs: AFS): Promise<Skill[]>;
11
12
  export declare function loadAgentSkillFromAFS({ afs, }: {
12
13
  afs: AFS;
13
14
  }): Promise<AgentSkill | undefined>;
@@ -434,9 +434,8 @@ export class AIAgent extends Agent {
434
434
  else {
435
435
  yield { delta: { text: { [outputKey]: "\n\n" } } };
436
436
  }
437
- const message = { role: "agent", toolCalls };
438
- yield { progress: { event: "message", message } };
439
- await session.appendCurrentMessages(message, options);
437
+ const toolCallMessage = { role: "agent", toolCalls };
438
+ yield { progress: { event: "message", message: toolCallMessage } };
440
439
  const executedToolCalls = [];
441
440
  let error;
442
441
  const queue = fastq.promise(async ({ tool, call }) => {
@@ -467,6 +466,7 @@ export class AIAgent extends Agent {
467
466
  await queue.drained();
468
467
  if (error)
469
468
  throw error;
469
+ const toolResultMessages = [];
470
470
  // Continue LLM function calling loop if any tools were executed
471
471
  if (executedToolCalls.length) {
472
472
  for (const { call, tool, output } of executedToolCalls) {
@@ -478,11 +478,12 @@ export class AIAgent extends Agent {
478
478
  content: [{ type: "text", text, isAgentSkill }],
479
479
  };
480
480
  yield { progress: { event: "message", message: message } };
481
- await session.appendCurrentMessages(message, options);
481
+ toolResultMessages.push(message);
482
482
  }
483
483
  const transferOutput = executedToolCalls.find((i) => isTransferAgentOutput(i.output))?.output;
484
484
  if (transferOutput)
485
485
  return transferOutput;
486
+ await session.appendCurrentMessages([toolCallMessage, ...toolResultMessages], options);
486
487
  continue;
487
488
  }
488
489
  }
@@ -80,7 +80,9 @@ export declare class AgentSession {
80
80
  private maybeCompactCurrentEntry;
81
81
  private maybeAutoCompact;
82
82
  /**
83
- * Estimate token count for an array of messages
83
+ * Estimate token count for messages
84
+ * Applies singleMessageLimit to each text block individually
85
+ * Non-text tokens (images, tool calls) are always counted in full
84
86
  */
85
87
  private estimateMessagesTokens;
86
88
  /**
@@ -89,6 +91,14 @@ export declare class AgentSession {
89
91
  */
90
92
  private splitIntoBatches;
91
93
  appendCurrentMessages(messages: ChatModelInputMessage | ChatModelInputMessage[], options: AgentInvokeOptions): Promise<void>;
94
+ /**
95
+ * Truncate text content to fit within target token limit
96
+ * @param text The text to truncate
97
+ * @param currentTokens Current token count of the text
98
+ * @param targetTokens Target token count after truncation
99
+ * @returns Truncated text
100
+ */
101
+ private truncateText;
92
102
  private truncateLargeMessage;
93
103
  private ensureInitialized;
94
104
  private initialize;
@@ -148,6 +158,6 @@ export declare class AgentSession {
148
158
  private initializeDefaultUserMemoryExtractor;
149
159
  private get maxTokens();
150
160
  private get keepRecentRatio();
151
- private get keepTokenBudget();
161
+ private get keepRecentTokens();
152
162
  private get singleMessageLimit();
153
163
  }
@@ -2,6 +2,7 @@ import { AFSHistory } from "@aigne/afs-history";
2
2
  import { v7 } from "@aigne/uuid";
3
3
  import { joinURL } from "ufo";
4
4
  import { stringify } from "yaml";
5
+ import { logger } from "../utils/logger.js";
5
6
  import { estimateTokens } from "../utils/token-estimator.js";
6
7
  import { isNonNullable } from "../utils/type-utils.js";
7
8
  import { DEFAULT_COMPACT_ASYNC, DEFAULT_COMPACT_MODE, DEFAULT_KEEP_RECENT_RATIO, DEFAULT_MAX_TOKENS, DEFAULT_MEMORY_QUERY_LIMIT, DEFAULT_MEMORY_RATIO, DEFAULT_SESSION_MEMORY_ASYNC, DEFAULT_SESSION_MEMORY_MODE, DEFAULT_SESSION_MODE, DEFAULT_USER_MEMORY_ASYNC, DEFAULT_USER_MEMORY_MODE, } from "./compact/types.js";
@@ -251,7 +252,9 @@ ${"```"}
251
252
  this.compactionPromise = this.doCompact(options).finally(() => {
252
253
  this.compactionPromise = undefined;
253
254
  });
254
- return this.compactionPromise;
255
+ const isAsync = this.compactConfig.async ?? DEFAULT_COMPACT_ASYNC;
256
+ if (!isAsync)
257
+ await this.compactionPromise;
255
258
  }
256
259
  /**
257
260
  * Internal method that performs the actual compaction
@@ -265,20 +268,18 @@ ${"```"}
265
268
  if (historyEntries.length === 0)
266
269
  return;
267
270
  const maxTokens = this.maxTokens;
268
- let keepTokenBudget = this.keepTokenBudget;
269
- // Calculate tokens for system messages
270
- const systemTokens = (this.runtimeState.systemMessages ?? []).reduce((sum, msg) => {
271
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
272
- return sum + estimateTokens(content);
273
- }, 0);
274
- // Calculate tokens for current entry messages
275
- const currentTokens = (this.runtimeState.currentEntry?.messages ?? []).reduce((sum, msg) => {
276
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
277
- return sum + estimateTokens(content);
278
- }, 0);
279
- // Subtract system and current tokens from budget
280
- // This ensures total tokens (system + current + kept history) stays within ratio budget
281
- keepTokenBudget = Math.max(0, keepTokenBudget - systemTokens - currentTokens);
271
+ // Target to keep only 50% of keepRecentTokens to leave buffer room
272
+ // This avoids triggering compression again shortly after compaction
273
+ // Similar to compactCurrentEntry, we compress more aggressively to leave headroom
274
+ //
275
+ // Note: We don't subtract systemTokens or currentEntry tokens because:
276
+ // 1. keepRecentTokens is already a relative ratio (e.g., 50% of maxTokens)
277
+ // 2. systemTokens overhead is typically small (~1-2k, ~1-2% of maxTokens)
278
+ // 3. currentEntry is still being constructed (not yet added to history)
279
+ // 4. In tool use scenarios, currentEntry can be very large (many tool calls)
280
+ // 5. Subtracting them would complicate logic without significant benefit
281
+ // 6. Total token limit is enforced by maybeAutoCompact trigger condition
282
+ const keepRecentTokens = this.keepRecentTokens * 0.5;
282
283
  // Find split point by iterating backwards from most recent entry
283
284
  // The split point divides history into: [compact] | [keep]
284
285
  let splitIndex = historyEntries.length; // Default: keep all (no compaction)
@@ -289,7 +290,7 @@ ${"```"}
289
290
  continue;
290
291
  const entryTokens = this.estimateMessagesTokens(entry.content?.messages ?? []);
291
292
  // Check if adding this entry would exceed token budget
292
- if (accumulatedTokens + entryTokens > keepTokenBudget) {
293
+ if (accumulatedTokens + entryTokens > keepRecentTokens) {
293
294
  // Would exceed budget, split here (this entry and earlier ones will be compacted)
294
295
  splitIndex = i + 1;
295
296
  break;
@@ -360,22 +361,21 @@ ${"```"}
360
361
  const uncompressedMessages = currentEntry.messages.slice(alreadyCompressedCount);
361
362
  if (uncompressedMessages.length === 0)
362
363
  return;
363
- const keepTokenBudget = this.keepTokenBudget;
364
- const singleMessageLimit = this.singleMessageLimit;
364
+ // Target to keep only 50% of keepTokenBudget to leave buffer room
365
+ // This avoids frequent small-batch compressions in tool use scenarios
366
+ const keepTokenBudget = this.keepRecentTokens * 0.5;
365
367
  let splitIndex = uncompressedMessages.length;
366
368
  let accumulatedTokens = 0;
367
369
  for (let i = uncompressedMessages.length - 1; i >= 0; i--) {
368
370
  const msg = uncompressedMessages[i];
369
371
  if (!msg)
370
372
  continue;
371
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
372
- const msgTokens = estimateTokens(content);
373
- const effectiveTokens = msgTokens > singleMessageLimit ? singleMessageLimit : msgTokens;
374
- if (accumulatedTokens + effectiveTokens > keepTokenBudget) {
373
+ const msgTokens = this.estimateMessagesTokens([msg]);
374
+ if (accumulatedTokens + msgTokens > keepTokenBudget) {
375
375
  splitIndex = i + 1;
376
376
  break;
377
377
  }
378
- accumulatedTokens += effectiveTokens;
378
+ accumulatedTokens += msgTokens;
379
379
  splitIndex = i;
380
380
  }
381
381
  const keptMessages = uncompressedMessages.slice(splitIndex);
@@ -424,8 +424,8 @@ ${"```"}
424
424
  return;
425
425
  const compressedCount = this.runtimeState.currentEntryCompact?.compressedCount ?? 0;
426
426
  const uncompressedMessages = currentEntry.messages.slice(compressedCount);
427
- const threshold = this.keepTokenBudget;
428
- const currentTokens = this.estimateMessagesTokens(uncompressedMessages, this.singleMessageLimit);
427
+ const threshold = this.keepRecentTokens;
428
+ const currentTokens = this.estimateMessagesTokens(uncompressedMessages);
429
429
  if (currentTokens > threshold) {
430
430
  await this.compactCurrentEntry(options);
431
431
  }
@@ -433,8 +433,6 @@ ${"```"}
433
433
  async maybeAutoCompact(options) {
434
434
  if (this.compactionPromise)
435
435
  await this.compactionPromise;
436
- if (!this.compactConfig)
437
- return;
438
436
  const mode = this.compactConfig.mode ?? DEFAULT_COMPACT_MODE;
439
437
  if (mode === "disabled")
440
438
  return;
@@ -445,24 +443,48 @@ ${"```"}
445
443
  const messages = await this.getMessages();
446
444
  const currentTokens = this.estimateMessagesTokens(messages);
447
445
  if (currentTokens >= maxTokens) {
448
- this.compact(options);
449
- const isAsync = this.compactConfig.async ?? DEFAULT_COMPACT_ASYNC;
450
- if (!isAsync)
451
- await this.compactionPromise;
446
+ await this.compact(options);
452
447
  }
453
448
  }
454
449
  /**
455
- * Estimate token count for an array of messages
450
+ * Estimate token count for messages
451
+ * Applies singleMessageLimit to each text block individually
452
+ * Non-text tokens (images, tool calls) are always counted in full
456
453
  */
457
- estimateMessagesTokens(messages, singleMessageLimit) {
458
- return messages.reduce((sum, msg) => {
459
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
460
- const tokens = estimateTokens(content);
461
- if (singleMessageLimit && tokens > singleMessageLimit) {
462
- return sum + singleMessageLimit;
454
+ estimateMessagesTokens(messages, singleMessageLimit = this.singleMessageLimit) {
455
+ let totalTokens = 0;
456
+ for (const msg of messages) {
457
+ // 1. Estimate content tokens
458
+ if (typeof msg.content === "string") {
459
+ const textTokens = estimateTokens(msg.content);
460
+ const effectiveTokens = textTokens > singleMessageLimit ? singleMessageLimit : textTokens;
461
+ totalTokens += effectiveTokens;
463
462
  }
464
- return sum + tokens;
465
- }, 0);
463
+ else if (Array.isArray(msg.content)) {
464
+ for (const block of msg.content) {
465
+ if (block.type === "text" && typeof block.text === "string") {
466
+ // Text tokens (can be truncated) - apply limit to each block individually
467
+ const textTokens = estimateTokens(block.text);
468
+ const effectiveTokens = textTokens > singleMessageLimit ? singleMessageLimit : textTokens;
469
+ totalTokens += effectiveTokens;
470
+ }
471
+ else {
472
+ // Non-text blocks - always counted in full
473
+ totalTokens += 1000;
474
+ }
475
+ }
476
+ }
477
+ // 2. Estimate tool calls tokens (cannot be truncated)
478
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
479
+ for (const toolCall of msg.toolCalls) {
480
+ // Function name + arguments + overhead
481
+ totalTokens += estimateTokens(toolCall.function.name);
482
+ totalTokens += estimateTokens(stringify(toolCall.function.arguments).replace(/\s+/g, " "));
483
+ totalTokens += 10; // Structure overhead
484
+ }
485
+ }
486
+ }
487
+ return totalTokens;
466
488
  }
467
489
  /**
468
490
  * Split entries into batches based on token limit
@@ -499,22 +521,54 @@ ${"```"}
499
521
  this.runtimeState.currentEntry.messages.push(...[messages].flat());
500
522
  await this.maybeCompactCurrentEntry(options);
501
523
  }
502
- truncateLargeMessage(msg) {
503
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
504
- const tokens = estimateTokens(content);
505
- const singleMessageLimit = this.singleMessageLimit;
506
- if (tokens <= singleMessageLimit)
507
- return msg;
508
- const keepRatio = (singleMessageLimit / tokens) * 0.9;
509
- const keepLength = Math.floor(content.length * keepRatio);
524
+ /**
525
+ * Truncate text content to fit within target token limit
526
+ * @param text The text to truncate
527
+ * @param currentTokens Current token count of the text
528
+ * @param targetTokens Target token count after truncation
529
+ * @returns Truncated text
530
+ */
531
+ truncateText(text, currentTokens, targetTokens) {
532
+ if (currentTokens <= targetTokens)
533
+ return text;
534
+ const keepRatio = (targetTokens / currentTokens) * 0.9;
535
+ const keepLength = Math.floor(text.length * keepRatio);
510
536
  const headLength = Math.floor(keepLength * 0.7);
511
537
  const tailLength = Math.floor(keepLength * 0.3);
512
- const truncated = content.slice(0, headLength) +
513
- `\n\n[... Content too large, truncated ${tokens - singleMessageLimit} tokens ...]\n\n` +
514
- content.slice(-tailLength);
538
+ return (text.slice(0, headLength) +
539
+ `\n\n[... truncated ${currentTokens - targetTokens} tokens ...]\n\n` +
540
+ text.slice(-tailLength));
541
+ }
542
+ truncateLargeMessage(msg) {
543
+ const singleMessageLimit = this.singleMessageLimit;
544
+ // Handle string content
515
545
  if (typeof msg.content === "string") {
546
+ const tokens = estimateTokens(msg.content);
547
+ if (tokens <= singleMessageLimit)
548
+ return msg;
549
+ const truncated = this.truncateText(msg.content, tokens, singleMessageLimit);
516
550
  return { ...msg, content: truncated };
517
551
  }
552
+ // Handle array content (UnionContent[])
553
+ if (Array.isArray(msg.content)) {
554
+ // Truncate each text block individually if it exceeds the limit
555
+ const truncatedContent = msg.content.map((block) => {
556
+ // Keep non-text blocks unchanged
557
+ if (block.type !== "text" || typeof block.text !== "string") {
558
+ return block;
559
+ }
560
+ // Check if this text block needs truncation
561
+ const blockTokens = estimateTokens(block.text);
562
+ if (blockTokens <= singleMessageLimit) {
563
+ return block;
564
+ }
565
+ // Truncate this text block independently
566
+ const truncatedText = this.truncateText(block.text, blockTokens, singleMessageLimit);
567
+ return { ...block, text: truncatedText };
568
+ });
569
+ return { ...msg, content: truncatedContent };
570
+ }
571
+ // Unknown content type, return as-is
518
572
  return msg;
519
573
  }
520
574
  async ensureInitialized() {
@@ -632,53 +686,46 @@ ${"```"}
632
686
  */
633
687
  async updateSessionMemory(options) {
634
688
  await this.ensureInitialized();
635
- // If session memory update is already in progress, wait for it to complete
636
- if (this.sessionMemoryUpdatePromise) {
637
- return this.sessionMemoryUpdatePromise;
638
- }
639
- // Start new session memory update task
640
- this.sessionMemoryUpdatePromise = this.doUpdateSessionMemory(options).finally(() => {
689
+ this.sessionMemoryUpdatePromise ??= this.doUpdateSessionMemory(options)
690
+ .then(() => {
691
+ // After session memory update succeeds, potentially trigger user memory consolidation
692
+ this.maybeAutoUpdateUserMemory(options).catch((err) => {
693
+ logger.error("User memory update failed:", err);
694
+ });
695
+ })
696
+ .finally(() => {
641
697
  this.sessionMemoryUpdatePromise = undefined;
642
- // After session memory update completes, potentially trigger user memory consolidation
643
- this.maybeAutoUpdateUserMemory(options);
644
698
  });
645
699
  return this.sessionMemoryUpdatePromise;
646
700
  }
647
701
  async maybeAutoUpdateSessionMemory(options) {
648
- if (this.sessionMemoryUpdatePromise)
649
- await this.sessionMemoryUpdatePromise;
650
702
  // Check if memory extraction is enabled (requires AFS history module)
651
703
  if (!this.isMemoryEnabled)
652
704
  return;
653
- if (!this.sessionMemoryConfig)
654
- return;
655
705
  // Check if mode is disabled
656
706
  const mode = this.sessionMemoryConfig.mode ?? DEFAULT_SESSION_MEMORY_MODE;
657
707
  if (mode === "disabled")
658
708
  return;
659
709
  // Trigger session memory update
660
- this.updateSessionMemory(options);
710
+ this.updateSessionMemory(options).catch((err) => {
711
+ logger.error("Session memory update failed:", err);
712
+ });
661
713
  const isAsync = this.sessionMemoryConfig.async ?? DEFAULT_SESSION_MEMORY_ASYNC;
662
714
  if (!isAsync)
663
715
  await this.sessionMemoryUpdatePromise;
664
716
  }
665
717
  async maybeAutoUpdateUserMemory(options) {
666
- if (this.userMemoryUpdatePromise)
667
- await this.userMemoryUpdatePromise;
668
718
  // Check if memory extraction is enabled (requires AFS history module)
669
- if (!this.isMemoryEnabled)
670
- return;
671
- if (!this.userMemoryConfig || !this.userId)
719
+ if (!this.isMemoryEnabled || !this.userId)
672
720
  return;
673
721
  // Check if mode is disabled
674
722
  const mode = this.userMemoryConfig.mode ?? DEFAULT_USER_MEMORY_MODE;
675
723
  if (mode === "disabled")
676
724
  return;
677
- // Wait for session memory update to complete first
678
- if (this.sessionMemoryUpdatePromise)
679
- await this.sessionMemoryUpdatePromise;
680
725
  // Trigger user memory consolidation
681
- this.updateUserMemory(options);
726
+ this.updateUserMemory(options).catch((err) => {
727
+ logger.error("User memory update failed:", err);
728
+ });
682
729
  const isAsync = this.userMemoryConfig.async ?? DEFAULT_USER_MEMORY_ASYNC;
683
730
  if (!isAsync)
684
731
  await this.userMemoryUpdatePromise;
@@ -773,12 +820,8 @@ ${"```"}
773
820
  */
774
821
  async updateUserMemory(options) {
775
822
  await this.ensureInitialized();
776
- // If user memory update is already in progress, wait for it to complete
777
- if (this.userMemoryUpdatePromise) {
778
- return this.userMemoryUpdatePromise;
779
- }
780
823
  // Start new user memory update task
781
- this.userMemoryUpdatePromise = this.doUpdateUserMemory(options).finally(() => {
824
+ this.userMemoryUpdatePromise ??= this.doUpdateUserMemory(options).finally(() => {
782
825
  this.userMemoryUpdatePromise = undefined;
783
826
  });
784
827
  return this.userMemoryUpdatePromise;
@@ -916,10 +959,10 @@ ${"```"}
916
959
  get keepRecentRatio() {
917
960
  return this.compactConfig?.keepRecentRatio ?? DEFAULT_KEEP_RECENT_RATIO;
918
961
  }
919
- get keepTokenBudget() {
962
+ get keepRecentTokens() {
920
963
  return Math.floor(this.maxTokens * this.keepRecentRatio);
921
964
  }
922
965
  get singleMessageLimit() {
923
- return this.keepTokenBudget * 0.5;
966
+ return this.keepRecentTokens * 0.5;
924
967
  }
925
968
  }
@@ -38,6 +38,7 @@ export class AISessionCompactor extends AIAgent {
38
38
  summary: z.string().describe("A comprehensive summary of the conversation history"),
39
39
  }),
40
40
  instructions: COMPACTOR_INSTRUCTIONS,
41
+ taskRenderMode: "hide",
41
42
  ...omitBy(options ?? {}, (v) => isNil(v)),
42
43
  session: {
43
44
  mode: "disabled",
@@ -129,6 +129,7 @@ export class AISessionMemoryExtractor extends AIAgent {
129
129
  removeFacts: optional(z.array(z.string()).describe("Labels of facts to remove from memory")),
130
130
  }),
131
131
  instructions: EXTRACTOR_INSTRUCTIONS,
132
+ taskRenderMode: "hide",
132
133
  ...omitBy(options ?? {}, (v) => isNil(v)),
133
134
  session: {
134
135
  mode: "disabled",
@@ -110,6 +110,7 @@ export class AIUserMemoryExtractor extends AIAgent {
110
110
  removeFacts: optional(z.array(z.string()).describe("Labels of facts to remove from user memory")),
111
111
  }),
112
112
  instructions: EXTRACTOR_INSTRUCTIONS,
113
+ taskRenderMode: "hide",
113
114
  ...omitBy(options ?? {}, (v) => isNil(v)),
114
115
  session: {
115
116
  mode: "disabled",
@@ -37,10 +37,10 @@ export class PromptBuilder {
37
37
  content = i.content.text;
38
38
  else if (i.content.type === "resource") {
39
39
  const { resource } = i.content;
40
- if (typeof resource.text === "string") {
40
+ if ("text" in resource && typeof resource.text === "string") {
41
41
  content = resource.text;
42
42
  }
43
- else if (typeof resource.blob === "string") {
43
+ else if ("blob" in resource && typeof resource.blob === "string") {
44
44
  content = [{ type: "url", url: resource.blob }];
45
45
  }
46
46
  }
@@ -8,6 +8,7 @@ export interface Skill {
8
8
  }
9
9
  export declare function loadSkill(path: string): Promise<Skill>;
10
10
  export declare function loadSkills(paths: string[]): Promise<Skill[]>;
11
+ export declare function discoverSkillsFromAFS(afs: AFS): Promise<Skill[]>;
11
12
  export declare function loadAgentSkillFromAFS({ afs, }: {
12
13
  afs: AFS;
13
14
  }): Promise<AgentSkill | undefined>;
@@ -23,7 +23,7 @@ export async function loadSkills(paths) {
23
23
  }
24
24
  return skills;
25
25
  }
26
- export async function loadAgentSkillFromAFS({ afs, }) {
26
+ export async function discoverSkillsFromAFS(afs) {
27
27
  const modules = await afs.listModules();
28
28
  const filtered = modules.filter(({ module: m }) => "options" in m &&
29
29
  typeof m.options === "object" &&
@@ -31,7 +31,7 @@ export async function loadAgentSkillFromAFS({ afs, }) {
31
31
  "agentSkills" in m.options &&
32
32
  m.options.agentSkills === true);
33
33
  if (!filtered.length)
34
- return;
34
+ return [];
35
35
  const skills = [];
36
36
  for (const module of filtered) {
37
37
  const data = (await afs
@@ -48,6 +48,10 @@ export async function loadAgentSkillFromAFS({ afs, }) {
48
48
  skills.push(skill);
49
49
  }
50
50
  }
51
+ return skills;
52
+ }
53
+ export async function loadAgentSkillFromAFS({ afs, }) {
54
+ const skills = await discoverSkillsFromAFS(afs);
51
55
  if (!skills.length)
52
56
  return;
53
57
  return new AgentSkill({
@@ -43,5 +43,5 @@ export function resourceFromMCPResource(resource, options) {
43
43
  });
44
44
  }
45
45
  function isResourceTemplate(resource) {
46
- return typeof resource.uriTemplate === "string";
46
+ return "uriTemplate" in resource && typeof resource.uriTemplate === "string";
47
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/core",
3
- "version": "1.72.0-beta.16",
3
+ "version": "1.72.0-beta.18",
4
4
  "description": "The functional core of agentic AI",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -94,9 +94,9 @@
94
94
  "zod-from-json-schema": "^0.0.5",
95
95
  "zod-to-json-schema": "^3.24.6",
96
96
  "@aigne/afs": "^1.4.0-beta.8",
97
+ "@aigne/observability-api": "^0.11.14-beta.3",
97
98
  "@aigne/afs-history": "^1.2.0-beta.9",
98
- "@aigne/platform-helpers": "^0.6.7-beta.1",
99
- "@aigne/observability-api": "^0.11.14-beta.2"
99
+ "@aigne/platform-helpers": "^0.6.7-beta.1"
100
100
  },
101
101
  "devDependencies": {
102
102
  "@types/bun": "^1.2.22",