@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 +22 -0
- package/lib/cjs/agents/ai-agent.js +5 -4
- package/lib/cjs/prompt/agent-session.d.ts +12 -2
- package/lib/cjs/prompt/agent-session.js +123 -80
- package/lib/cjs/prompt/compact/compactor.js +1 -0
- package/lib/cjs/prompt/compact/session-memory-extractor.js +1 -0
- package/lib/cjs/prompt/compact/user-memory-extractor.js +1 -0
- package/lib/cjs/prompt/prompt-builder.js +2 -2
- package/lib/cjs/prompt/skills/afs/agent-skill/skill-loader.d.ts +1 -0
- package/lib/cjs/prompt/skills/afs/agent-skill/skill-loader.js +7 -2
- package/lib/cjs/utils/mcp-utils.js +1 -1
- package/lib/dts/prompt/agent-session.d.ts +12 -2
- package/lib/dts/prompt/skills/afs/agent-skill/skill-loader.d.ts +1 -0
- package/lib/esm/agents/ai-agent.js +5 -4
- package/lib/esm/prompt/agent-session.d.ts +12 -2
- package/lib/esm/prompt/agent-session.js +123 -80
- package/lib/esm/prompt/compact/compactor.js +1 -0
- package/lib/esm/prompt/compact/session-memory-extractor.js +1 -0
- package/lib/esm/prompt/compact/user-memory-extractor.js +1 -0
- package/lib/esm/prompt/prompt-builder.js +2 -2
- package/lib/esm/prompt/skills/afs/agent-skill/skill-loader.d.ts +1 -0
- package/lib/esm/prompt/skills/afs/agent-skill/skill-loader.js +6 -2
- package/lib/esm/utils/mcp-utils.js +1 -1
- package/package.json +3 -3
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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 >
|
|
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
|
-
|
|
403
|
-
|
|
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
|
|
411
|
-
|
|
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 +=
|
|
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.
|
|
467
|
-
const currentTokens = this.estimateMessagesTokens(uncompressedMessages
|
|
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
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if (
|
|
501
|
-
|
|
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
|
-
|
|
504
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
552
|
-
`\n\n[...
|
|
553
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
|
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
|
|
1001
|
+
get keepRecentTokens() {
|
|
959
1002
|
return Math.floor(this.maxTokens * this.keepRecentRatio);
|
|
960
1003
|
}
|
|
961
1004
|
get singleMessageLimit() {
|
|
962
|
-
return this.
|
|
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
|
|
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({
|
|
@@ -80,7 +80,9 @@ export declare class AgentSession {
|
|
|
80
80
|
private maybeCompactCurrentEntry;
|
|
81
81
|
private maybeAutoCompact;
|
|
82
82
|
/**
|
|
83
|
-
* Estimate token count for
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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 >
|
|
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
|
-
|
|
364
|
-
|
|
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
|
|
372
|
-
|
|
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 +=
|
|
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.
|
|
428
|
-
const currentTokens = this.estimateMessagesTokens(uncompressedMessages
|
|
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
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (
|
|
462
|
-
|
|
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
|
-
|
|
465
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
513
|
-
`\n\n[...
|
|
514
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
|
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
|
|
962
|
+
get keepRecentTokens() {
|
|
920
963
|
return Math.floor(this.maxTokens * this.keepRecentRatio);
|
|
921
964
|
}
|
|
922
965
|
get singleMessageLimit() {
|
|
923
|
-
return this.
|
|
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
|
|
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({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aigne/core",
|
|
3
|
-
"version": "1.72.0-beta.
|
|
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",
|