@aigne/core 1.72.0-beta.10 → 1.72.0-beta.12

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.12](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.72.0-beta.11...core-v1.72.0-beta.12) (2026-01-07)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **afs:** support `~` in the local path for local-fs & add agent-skill example ([#877](https://github.com/AIGNE-io/aigne-framework/issues/877)) ([c86293f](https://github.com/AIGNE-io/aigne-framework/commit/c86293f3d70447974395d02e238305a42b256b66))
9
+
10
+ ## [1.72.0-beta.11](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.72.0-beta.10...core-v1.72.0-beta.11) (2026-01-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **core:** preserve Agent Skill in session compact and support complex tool result content ([#876](https://github.com/AIGNE-io/aigne-framework/issues/876)) ([edb86ae](https://github.com/AIGNE-io/aigne-framework/commit/edb86ae2b9cfe56a8f08b276f843606e310566cf))
16
+
17
+
18
+ ### Dependencies
19
+
20
+ * The following workspace dependencies were updated
21
+ * dependencies
22
+ * @aigne/afs bumped to 1.4.0-beta.6
23
+ * @aigne/afs-history bumped to 1.2.0-beta.7
24
+
3
25
  ## [1.72.0-beta.10](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.72.0-beta.9...core-v1.72.0-beta.10) (2026-01-06)
4
26
 
5
27
 
@@ -513,6 +513,7 @@ export declare abstract class Agent<I extends Message = any, O extends Message =
513
513
  */
514
514
  protected postprocess(input: I, output: O, options: AgentInvokeOptions): Promise<void>;
515
515
  protected publishToTopics(output: Message, options: AgentInvokeOptions): Promise<void>;
516
+ formatOutput(output: O): PromiseOrValue<string>;
516
517
  /**
517
518
  * Core processing method of the agent, must be implemented in subclasses
518
519
  *
@@ -715,6 +715,9 @@ class Agent {
715
715
  });
716
716
  }
717
717
  }
718
+ formatOutput(output) {
719
+ return JSON.stringify(output);
720
+ }
718
721
  /**
719
722
  * Shut down the agent and clean up resources
720
723
  *
@@ -454,21 +454,6 @@ class AIAgent extends agent_js_1.Agent {
454
454
  }
455
455
  const message = { role: "agent", toolCalls };
456
456
  yield { progress: { event: "message", message } };
457
- const skillToolUse = toolCallsWithTools.find((i) => i.tool instanceof agent_skill_js_1.AgentSkill);
458
- if (skillToolUse) {
459
- await session.endMessage(skillToolUse.function, {
460
- role: "agent",
461
- content: JSON.stringify({ ...skillToolUse.function }),
462
- }, options);
463
- const skillResult = await this.invokeSkill(skillToolUse.tool, { ...input, ...skillToolUse.function.arguments }, options);
464
- await session.startMessage(skillToolUse.function.arguments, {
465
- role: "user",
466
- content: [
467
- { type: "text", text: agent_skill_js_1.AgentSkill.formatOutput(skillResult), isAgentSkill: true },
468
- ],
469
- }, options);
470
- continue;
471
- }
472
457
  await session.appendCurrentMessages(message, options);
473
458
  const executedToolCalls = [];
474
459
  let error;
@@ -502,11 +487,13 @@ class AIAgent extends agent_js_1.Agent {
502
487
  throw error;
503
488
  // Continue LLM function calling loop if any tools were executed
504
489
  if (executedToolCalls.length) {
505
- for (const { call, output } of executedToolCalls) {
490
+ for (const { call, tool, output } of executedToolCalls) {
491
+ const isAgentSkill = !output.isError && tool instanceof agent_skill_js_1.AgentSkill ? true : undefined;
492
+ const text = await tool.formatOutput(output);
506
493
  const message = {
507
494
  role: "tool",
508
495
  toolCallId: call.id,
509
- content: JSON.stringify(output),
496
+ content: [{ type: "text", text, isAgentSkill }],
510
497
  };
511
498
  yield { progress: { event: "message", message: message } };
512
499
  await session.appendCurrentMessages(message, options);
@@ -103,8 +103,8 @@ export declare class AgentSession {
103
103
  */
104
104
  private loadUserMemory;
105
105
  /**
106
- * Load session history including compact summary and history entries
107
- * @returns Object containing compact summary and history entries
106
+ * Load session history including compact content and history entries
107
+ * @returns Object containing history compact and history entries
108
108
  */
109
109
  private loadSessionHistory;
110
110
  /**
@@ -125,6 +125,24 @@ export declare class AgentSession {
125
125
  * Internal method that performs the actual user memory extraction
126
126
  */
127
127
  private doUpdateUserMemory;
128
+ /**
129
+ * Find Agent Skill content from a single message
130
+ * @param msg - Message to search in
131
+ * @returns The skill content text if found, undefined otherwise
132
+ */
133
+ private findSkillContentInMessage;
134
+ /**
135
+ * Find the last Agent Skill from a list of messages
136
+ * @param messages - Messages to search through
137
+ * @returns The last Agent Skill found, or undefined if none found
138
+ */
139
+ private findLastAgentSkillFromMessages;
140
+ /**
141
+ * Find the last Agent Skill from a list of history entries
142
+ * @param entries - History entries to search through
143
+ * @returns The last Agent Skill found, or undefined if none found
144
+ */
145
+ private findLastAgentSkill;
128
146
  private initializeDefaultCompactor;
129
147
  private initializeDefaultSessionMemoryExtractor;
130
148
  private initializeDefaultUserMemoryExtractor;
@@ -87,42 +87,59 @@ class AgentSession {
87
87
  }
88
88
  async getMessages() {
89
89
  await this.ensureInitialized();
90
- const { systemMessages, userMemory, sessionMemory, compactSummary, historyEntries, currentEntry, currentEntryCompression, } = this.runtimeState;
90
+ const { systemMessages, userMemory, sessionMemory, historyCompact, historyEntries, currentEntry, currentEntryCompact, } = this.runtimeState;
91
91
  let currentMessages = [];
92
92
  if (currentEntry?.messages?.length) {
93
- if (currentEntryCompression) {
94
- const { compressedCount, summary } = currentEntryCompression;
95
- const firstMsg = currentEntry.messages[0];
96
- const hasSkill = firstMsg?.role === "user" &&
97
- Array.isArray(firstMsg.content) &&
98
- firstMsg.content.some((block) => block.type === "text" && block.isAgentSkill === true);
99
- const skillMessage = hasSkill ? [firstMsg] : [];
93
+ if (currentEntryCompact) {
94
+ const { compressedCount, summary } = currentEntryCompact;
100
95
  const summaryMessage = {
101
96
  role: "user",
102
97
  content: `[Earlier messages in this conversation (${compressedCount} messages compressed)]\n${summary}`,
103
98
  };
104
99
  const remainingMessages = currentEntry.messages.slice(compressedCount);
105
- currentMessages = [...skillMessage, summaryMessage, ...remainingMessages];
100
+ currentMessages = [summaryMessage, ...remainingMessages];
106
101
  }
107
102
  else {
108
103
  currentMessages = currentEntry.messages;
109
104
  }
110
105
  }
106
+ // Flatten history entries messages once
107
+ const historyMessages = historyEntries.flatMap((entry) => entry.content?.messages ?? []);
108
+ // Check if there's an Agent Skill in current uncompressed messages
109
+ const hasSkillInCurrentMessages = [...historyMessages, ...currentMessages].some((msg) => msg.role === "user" &&
110
+ Array.isArray(msg.content) &&
111
+ msg.content.some((block) => block.type === "text" && block.isAgentSkill === true));
112
+ // Prefer currentEntryCompact's lastAgentSkill over historyCompact's (newer takes priority)
113
+ const lastAgentSkillToInject = currentEntryCompact?.lastAgentSkill ?? historyCompact?.lastAgentSkill;
111
114
  const messages = [
112
115
  ...(systemMessages ?? []),
113
116
  ...(userMemory && userMemory.length > 0 ? [this.formatUserMemory(userMemory)] : []),
114
117
  ...(sessionMemory && sessionMemory.length > 0
115
118
  ? [this.formatSessionMemory(sessionMemory)]
116
119
  : []),
117
- ...(compactSummary
120
+ ...(historyCompact?.summary
118
121
  ? [
119
122
  {
120
123
  role: "system",
121
- content: `Previous conversation summary:\n${compactSummary}`,
124
+ content: `Previous conversation summary:\n${historyCompact.summary}`,
122
125
  },
123
126
  ]
124
127
  : []),
125
- ...historyEntries.flatMap((entry) => entry.content?.messages ?? []),
128
+ // Only inject lastAgentSkill if there's no skill in current messages
129
+ ...(lastAgentSkillToInject && !hasSkillInCurrentMessages
130
+ ? [
131
+ {
132
+ role: "user",
133
+ content: [
134
+ {
135
+ type: "text",
136
+ text: lastAgentSkillToInject.content,
137
+ },
138
+ ],
139
+ },
140
+ ]
141
+ : []),
142
+ ...historyMessages,
126
143
  ...currentMessages,
127
144
  ];
128
145
  // Filter out thinking messages and truncate large messages
@@ -213,7 +230,7 @@ ${"```"}
213
230
  if (this.compactionPromise)
214
231
  await this.compactionPromise;
215
232
  }
216
- this.runtimeState.currentEntryCompression = undefined;
233
+ this.runtimeState.currentEntryCompact = undefined;
217
234
  this.runtimeState.currentEntry = { input, messages: [message] };
218
235
  }
219
236
  async endMessage(output, message, options) {
@@ -249,7 +266,7 @@ ${"```"}
249
266
  }
250
267
  this.runtimeState.historyEntries.push(newEntry);
251
268
  this.runtimeState.currentEntry = null;
252
- this.runtimeState.currentEntryCompression = undefined;
269
+ this.runtimeState.currentEntryCompact = undefined;
253
270
  // Only run compact and memory extraction if mode is not disabled
254
271
  if (this.mode !== "disabled") {
255
272
  await Promise.all([
@@ -333,7 +350,7 @@ ${"```"}
333
350
  // Split into batches to avoid context overflow
334
351
  const batches = this.splitIntoBatches(entriesToCompact, maxTokens);
335
352
  // Process batches incrementally, each summary becomes input for the next
336
- let currentSummary = this.runtimeState.compactSummary;
353
+ let currentSummary = this.runtimeState.historyCompact?.summary;
337
354
  for (const batch of batches) {
338
355
  const messages = batch
339
356
  .flatMap((e) => e.content?.messages ?? [])
@@ -345,19 +362,30 @@ ${"```"}
345
362
  });
346
363
  currentSummary = result.summary;
347
364
  }
365
+ // Extract last Agent Skill from entries to compact
366
+ let lastAgentSkill = this.findLastAgentSkill(entriesToCompact);
367
+ // If no skill found in entries to compact, inherit from previous compact
368
+ if (!lastAgentSkill && this.runtimeState.historyCompact?.lastAgentSkill) {
369
+ lastAgentSkill = this.runtimeState.historyCompact.lastAgentSkill;
370
+ }
371
+ // Create compact content
372
+ const historyCompact = {
373
+ summary: currentSummary ?? "",
374
+ lastAgentSkill,
375
+ };
348
376
  // Write compact entry to AFS
349
377
  if (this.afs && this.historyModulePath) {
350
378
  await this.afs.write((0, ufo_1.joinURL)(this.historyModulePath, "by-session", this.sessionId, "@metadata/compact/new"), {
351
379
  userId: this.userId,
352
380
  agentId: this.agentId,
353
- content: { summary: currentSummary },
381
+ content: historyCompact,
354
382
  metadata: {
355
383
  latestEntryId: latestCompactedEntry.id,
356
384
  },
357
385
  });
358
386
  }
359
387
  // Update runtime state: keep the summary and recent entries
360
- this.runtimeState.compactSummary = currentSummary;
388
+ this.runtimeState.historyCompact = historyCompact;
361
389
  this.runtimeState.historyEntries = entriesToKeep;
362
390
  }
363
391
  async compactCurrentEntry(options) {
@@ -367,7 +395,7 @@ ${"```"}
367
395
  const currentEntry = this.runtimeState.currentEntry;
368
396
  if (!currentEntry?.messages?.length)
369
397
  return;
370
- const alreadyCompressedCount = this.runtimeState.currentEntryCompression?.compressedCount ?? 0;
398
+ const alreadyCompressedCount = this.runtimeState.currentEntryCompact?.compressedCount ?? 0;
371
399
  const uncompressedMessages = currentEntry.messages.slice(alreadyCompressedCount);
372
400
  if (uncompressedMessages.length === 0)
373
401
  return;
@@ -415,13 +443,17 @@ ${"```"}
415
443
  if (messagesToCompact.length === 0)
416
444
  return;
417
445
  const result = await options.context.invoke(compactor, {
418
- previousSummary: this.runtimeState.currentEntryCompression?.summary
419
- ? [this.runtimeState.currentEntryCompression.summary]
446
+ previousSummary: this.runtimeState.currentEntryCompact?.summary
447
+ ? [this.runtimeState.currentEntryCompact.summary]
420
448
  : undefined,
421
449
  messages: messagesToCompact,
422
450
  });
423
- this.runtimeState.currentEntryCompression = {
451
+ // Find last Agent Skill from messages being compacted
452
+ const lastAgentSkill = this.findLastAgentSkillFromMessages(messagesToCompact) ??
453
+ this.runtimeState.currentEntryCompact?.lastAgentSkill;
454
+ this.runtimeState.currentEntryCompact = {
424
455
  summary: result.summary,
456
+ lastAgentSkill,
425
457
  compressedCount: alreadyCompressedCount + messagesToCompact.length,
426
458
  };
427
459
  }
@@ -429,7 +461,7 @@ ${"```"}
429
461
  const currentEntry = this.runtimeState.currentEntry;
430
462
  if (!currentEntry?.messages?.length)
431
463
  return;
432
- const compressedCount = this.runtimeState.currentEntryCompression?.compressedCount ?? 0;
464
+ const compressedCount = this.runtimeState.currentEntryCompact?.compressedCount ?? 0;
433
465
  const uncompressedMessages = currentEntry.messages.slice(compressedCount);
434
466
  const threshold = this.keepTokenBudget;
435
467
  const currentTokens = this.estimateMessagesTokens(uncompressedMessages, this.singleMessageLimit);
@@ -546,7 +578,7 @@ ${"```"}
546
578
  // Update runtime state with loaded data
547
579
  this.runtimeState.userMemory = userMemory;
548
580
  this.runtimeState.sessionMemory = sessionMemory;
549
- this.runtimeState.compactSummary = sessionHistory.compactSummary;
581
+ this.runtimeState.historyCompact = sessionHistory.historyCompact;
550
582
  this.runtimeState.historyEntries = sessionHistory.historyEntries;
551
583
  }
552
584
  }
@@ -599,8 +631,8 @@ ${"```"}
599
631
  return facts;
600
632
  }
601
633
  /**
602
- * Load session history including compact summary and history entries
603
- * @returns Object containing compact summary and history entries
634
+ * Load session history including compact content and history entries
635
+ * @returns Object containing history compact and history entries
604
636
  */
605
637
  async loadSessionHistory() {
606
638
  if (!this.afs || !this.historyModulePath) {
@@ -614,7 +646,7 @@ ${"```"}
614
646
  limit: 1,
615
647
  });
616
648
  const latestCompact = compactResult.data[0];
617
- const compactSummary = latestCompact?.content?.summary;
649
+ const historyCompact = latestCompact?.content;
618
650
  // Load history entries (after compact point if exists)
619
651
  const afsEntries = (await this.afs.list((0, ufo_1.joinURL)(this.historyModulePath, "by-session", this.sessionId), {
620
652
  filter: {
@@ -630,7 +662,7 @@ ${"```"}
630
662
  })).data;
631
663
  const historyEntries = afsEntries.reverse().filter((entry) => (0, type_utils_js_1.isNonNullable)(entry.content));
632
664
  return {
633
- compactSummary,
665
+ historyCompact,
634
666
  historyEntries,
635
667
  };
636
668
  }
@@ -864,6 +896,50 @@ ${"```"}
864
896
  }
865
897
  }
866
898
  }
899
+ /**
900
+ * Find Agent Skill content from a single message
901
+ * @param msg - Message to search in
902
+ * @returns The skill content text if found, undefined otherwise
903
+ */
904
+ findSkillContentInMessage(msg) {
905
+ if (msg.role === "user" && Array.isArray(msg.content)) {
906
+ const skillBlock = msg.content.find((block) => block.type === "text" && block.isAgentSkill === true);
907
+ if (skillBlock && skillBlock.type === "text") {
908
+ return skillBlock.text;
909
+ }
910
+ }
911
+ return undefined;
912
+ }
913
+ /**
914
+ * Find the last Agent Skill from a list of messages
915
+ * @param messages - Messages to search through
916
+ * @returns The last Agent Skill found, or undefined if none found
917
+ */
918
+ findLastAgentSkillFromMessages(messages) {
919
+ // Search backwards through messages to find the last Agent Skill
920
+ for (let i = messages.length - 1; i >= 0; i--) {
921
+ const msg = messages[i];
922
+ if (!msg)
923
+ continue;
924
+ const skillContent = this.findSkillContentInMessage(msg);
925
+ if (skillContent) {
926
+ return {
927
+ content: skillContent,
928
+ };
929
+ }
930
+ }
931
+ return undefined;
932
+ }
933
+ /**
934
+ * Find the last Agent Skill from a list of history entries
935
+ * @param entries - History entries to search through
936
+ * @returns The last Agent Skill found, or undefined if none found
937
+ */
938
+ findLastAgentSkill(entries) {
939
+ // Flatten all messages from entries
940
+ const allMessages = entries.flatMap((entry) => entry.content?.messages ?? []);
941
+ return this.findLastAgentSkillFromMessages(allMessages);
942
+ }
867
943
  async initializeDefaultCompactor() {
868
944
  this.compactConfig.compactor ??= await Promise.resolve().then(() => __importStar(require("./compact/compactor.js"))).then((m) => new m.AISessionCompactor());
869
945
  }
@@ -37,6 +37,13 @@ export interface EntryContent {
37
37
  */
38
38
  export interface CompactContent extends Message {
39
39
  summary: string;
40
+ /**
41
+ * Last Agent Skill content in the session
42
+ * Preserved across compactions to maintain skill instructions
43
+ */
44
+ lastAgentSkill?: {
45
+ content: string;
46
+ };
40
47
  }
41
48
  /**
42
49
  * Input structure for the compactor agent
@@ -1,4 +1,5 @@
1
1
  import { Agent, type AgentOptions, type Message } from "../../../../agents/agent.js";
2
+ import type { PromiseOrValue } from "../../../../utils/type-utils.js";
2
3
  import type { Skill } from "./skill-loader.js";
3
4
  export interface SkillToolInput extends Message {
4
5
  skill: string;
@@ -11,7 +12,7 @@ export interface SkillToolOptions extends AgentOptions<SkillToolInput, SkillTool
11
12
  agentSkills: Skill[];
12
13
  }
13
14
  export declare class AgentSkill extends Agent<SkillToolInput, SkillToolOutput> {
14
- static formatOutput(output: SkillToolOutput | Message): string;
15
+ formatOutput(output: SkillToolOutput | Message): PromiseOrValue<string>;
15
16
  constructor(options: SkillToolOptions);
16
17
  private agentSkills;
17
18
  process(input: SkillToolInput): Promise<SkillToolOutput>;
@@ -8,11 +8,11 @@ const skillToolInputSchema = zod_1.z.object({
8
8
  args: zod_1.z.string().optional().describe("The arguments to pass to the skill."),
9
9
  });
10
10
  class AgentSkill extends agent_js_1.Agent {
11
- static formatOutput(output) {
12
- if (!("result" in output) || typeof output.result !== "string") {
13
- throw new Error("Invalid SkillToolOutput: missing 'result' field");
11
+ formatOutput(output) {
12
+ if ("result" in output && typeof output.result === "string") {
13
+ return output.result;
14
14
  }
15
- return output.result;
15
+ return super.formatOutput(output);
16
16
  }
17
17
  constructor(options) {
18
18
  super({
@@ -42,10 +42,12 @@ async function loadAgentSkillFromAFS({ afs, }) {
42
42
  return;
43
43
  const skills = [];
44
44
  for (const module of filtered) {
45
- const data = (await afs.list(module.path, {
45
+ const data = (await afs
46
+ .list(module.path, {
46
47
  pattern: "**/SKILL.md",
47
48
  maxDepth: 10,
48
- })).data;
49
+ })
50
+ .catch(() => ({ data: [] }))).data;
49
51
  for (const entry of data) {
50
52
  const { data: file } = await afs.read(entry.path);
51
53
  if (typeof file?.content !== "string")
@@ -513,6 +513,7 @@ export declare abstract class Agent<I extends Message = any, O extends Message =
513
513
  */
514
514
  protected postprocess(input: I, output: O, options: AgentInvokeOptions): Promise<void>;
515
515
  protected publishToTopics(output: Message, options: AgentInvokeOptions): Promise<void>;
516
+ formatOutput(output: O): PromiseOrValue<string>;
516
517
  /**
517
518
  * Core processing method of the agent, must be implemented in subclasses
518
519
  *
@@ -103,8 +103,8 @@ export declare class AgentSession {
103
103
  */
104
104
  private loadUserMemory;
105
105
  /**
106
- * Load session history including compact summary and history entries
107
- * @returns Object containing compact summary and history entries
106
+ * Load session history including compact content and history entries
107
+ * @returns Object containing history compact and history entries
108
108
  */
109
109
  private loadSessionHistory;
110
110
  /**
@@ -125,6 +125,24 @@ export declare class AgentSession {
125
125
  * Internal method that performs the actual user memory extraction
126
126
  */
127
127
  private doUpdateUserMemory;
128
+ /**
129
+ * Find Agent Skill content from a single message
130
+ * @param msg - Message to search in
131
+ * @returns The skill content text if found, undefined otherwise
132
+ */
133
+ private findSkillContentInMessage;
134
+ /**
135
+ * Find the last Agent Skill from a list of messages
136
+ * @param messages - Messages to search through
137
+ * @returns The last Agent Skill found, or undefined if none found
138
+ */
139
+ private findLastAgentSkillFromMessages;
140
+ /**
141
+ * Find the last Agent Skill from a list of history entries
142
+ * @param entries - History entries to search through
143
+ * @returns The last Agent Skill found, or undefined if none found
144
+ */
145
+ private findLastAgentSkill;
128
146
  private initializeDefaultCompactor;
129
147
  private initializeDefaultSessionMemoryExtractor;
130
148
  private initializeDefaultUserMemoryExtractor;
@@ -37,6 +37,13 @@ export interface EntryContent {
37
37
  */
38
38
  export interface CompactContent extends Message {
39
39
  summary: string;
40
+ /**
41
+ * Last Agent Skill content in the session
42
+ * Preserved across compactions to maintain skill instructions
43
+ */
44
+ lastAgentSkill?: {
45
+ content: string;
46
+ };
40
47
  }
41
48
  /**
42
49
  * Input structure for the compactor agent
@@ -1,4 +1,5 @@
1
1
  import { Agent, type AgentOptions, type Message } from "../../../../agents/agent.js";
2
+ import type { PromiseOrValue } from "../../../../utils/type-utils.js";
2
3
  import type { Skill } from "./skill-loader.js";
3
4
  export interface SkillToolInput extends Message {
4
5
  skill: string;
@@ -11,7 +12,7 @@ export interface SkillToolOptions extends AgentOptions<SkillToolInput, SkillTool
11
12
  agentSkills: Skill[];
12
13
  }
13
14
  export declare class AgentSkill extends Agent<SkillToolInput, SkillToolOutput> {
14
- static formatOutput(output: SkillToolOutput | Message): string;
15
+ formatOutput(output: SkillToolOutput | Message): PromiseOrValue<string>;
15
16
  constructor(options: SkillToolOptions);
16
17
  private agentSkills;
17
18
  process(input: SkillToolInput): Promise<SkillToolOutput>;
@@ -513,6 +513,7 @@ export declare abstract class Agent<I extends Message = any, O extends Message =
513
513
  */
514
514
  protected postprocess(input: I, output: O, options: AgentInvokeOptions): Promise<void>;
515
515
  protected publishToTopics(output: Message, options: AgentInvokeOptions): Promise<void>;
516
+ formatOutput(output: O): PromiseOrValue<string>;
516
517
  /**
517
518
  * Core processing method of the agent, must be implemented in subclasses
518
519
  *
@@ -667,6 +667,9 @@ export class Agent {
667
667
  });
668
668
  }
669
669
  }
670
+ formatOutput(output) {
671
+ return JSON.stringify(output);
672
+ }
670
673
  /**
671
674
  * Shut down the agent and clean up resources
672
675
  *
@@ -418,21 +418,6 @@ export class AIAgent extends Agent {
418
418
  }
419
419
  const message = { role: "agent", toolCalls };
420
420
  yield { progress: { event: "message", message } };
421
- const skillToolUse = toolCallsWithTools.find((i) => i.tool instanceof AgentSkill);
422
- if (skillToolUse) {
423
- await session.endMessage(skillToolUse.function, {
424
- role: "agent",
425
- content: JSON.stringify({ ...skillToolUse.function }),
426
- }, options);
427
- const skillResult = await this.invokeSkill(skillToolUse.tool, { ...input, ...skillToolUse.function.arguments }, options);
428
- await session.startMessage(skillToolUse.function.arguments, {
429
- role: "user",
430
- content: [
431
- { type: "text", text: AgentSkill.formatOutput(skillResult), isAgentSkill: true },
432
- ],
433
- }, options);
434
- continue;
435
- }
436
421
  await session.appendCurrentMessages(message, options);
437
422
  const executedToolCalls = [];
438
423
  let error;
@@ -466,11 +451,13 @@ export class AIAgent extends Agent {
466
451
  throw error;
467
452
  // Continue LLM function calling loop if any tools were executed
468
453
  if (executedToolCalls.length) {
469
- for (const { call, output } of executedToolCalls) {
454
+ for (const { call, tool, output } of executedToolCalls) {
455
+ const isAgentSkill = !output.isError && tool instanceof AgentSkill ? true : undefined;
456
+ const text = await tool.formatOutput(output);
470
457
  const message = {
471
458
  role: "tool",
472
459
  toolCallId: call.id,
473
- content: JSON.stringify(output),
460
+ content: [{ type: "text", text, isAgentSkill }],
474
461
  };
475
462
  yield { progress: { event: "message", message: message } };
476
463
  await session.appendCurrentMessages(message, options);
@@ -103,8 +103,8 @@ export declare class AgentSession {
103
103
  */
104
104
  private loadUserMemory;
105
105
  /**
106
- * Load session history including compact summary and history entries
107
- * @returns Object containing compact summary and history entries
106
+ * Load session history including compact content and history entries
107
+ * @returns Object containing history compact and history entries
108
108
  */
109
109
  private loadSessionHistory;
110
110
  /**
@@ -125,6 +125,24 @@ export declare class AgentSession {
125
125
  * Internal method that performs the actual user memory extraction
126
126
  */
127
127
  private doUpdateUserMemory;
128
+ /**
129
+ * Find Agent Skill content from a single message
130
+ * @param msg - Message to search in
131
+ * @returns The skill content text if found, undefined otherwise
132
+ */
133
+ private findSkillContentInMessage;
134
+ /**
135
+ * Find the last Agent Skill from a list of messages
136
+ * @param messages - Messages to search through
137
+ * @returns The last Agent Skill found, or undefined if none found
138
+ */
139
+ private findLastAgentSkillFromMessages;
140
+ /**
141
+ * Find the last Agent Skill from a list of history entries
142
+ * @param entries - History entries to search through
143
+ * @returns The last Agent Skill found, or undefined if none found
144
+ */
145
+ private findLastAgentSkill;
128
146
  private initializeDefaultCompactor;
129
147
  private initializeDefaultSessionMemoryExtractor;
130
148
  private initializeDefaultUserMemoryExtractor;
@@ -48,42 +48,59 @@ export class AgentSession {
48
48
  }
49
49
  async getMessages() {
50
50
  await this.ensureInitialized();
51
- const { systemMessages, userMemory, sessionMemory, compactSummary, historyEntries, currentEntry, currentEntryCompression, } = this.runtimeState;
51
+ const { systemMessages, userMemory, sessionMemory, historyCompact, historyEntries, currentEntry, currentEntryCompact, } = this.runtimeState;
52
52
  let currentMessages = [];
53
53
  if (currentEntry?.messages?.length) {
54
- if (currentEntryCompression) {
55
- const { compressedCount, summary } = currentEntryCompression;
56
- const firstMsg = currentEntry.messages[0];
57
- const hasSkill = firstMsg?.role === "user" &&
58
- Array.isArray(firstMsg.content) &&
59
- firstMsg.content.some((block) => block.type === "text" && block.isAgentSkill === true);
60
- const skillMessage = hasSkill ? [firstMsg] : [];
54
+ if (currentEntryCompact) {
55
+ const { compressedCount, summary } = currentEntryCompact;
61
56
  const summaryMessage = {
62
57
  role: "user",
63
58
  content: `[Earlier messages in this conversation (${compressedCount} messages compressed)]\n${summary}`,
64
59
  };
65
60
  const remainingMessages = currentEntry.messages.slice(compressedCount);
66
- currentMessages = [...skillMessage, summaryMessage, ...remainingMessages];
61
+ currentMessages = [summaryMessage, ...remainingMessages];
67
62
  }
68
63
  else {
69
64
  currentMessages = currentEntry.messages;
70
65
  }
71
66
  }
67
+ // Flatten history entries messages once
68
+ const historyMessages = historyEntries.flatMap((entry) => entry.content?.messages ?? []);
69
+ // Check if there's an Agent Skill in current uncompressed messages
70
+ const hasSkillInCurrentMessages = [...historyMessages, ...currentMessages].some((msg) => msg.role === "user" &&
71
+ Array.isArray(msg.content) &&
72
+ msg.content.some((block) => block.type === "text" && block.isAgentSkill === true));
73
+ // Prefer currentEntryCompact's lastAgentSkill over historyCompact's (newer takes priority)
74
+ const lastAgentSkillToInject = currentEntryCompact?.lastAgentSkill ?? historyCompact?.lastAgentSkill;
72
75
  const messages = [
73
76
  ...(systemMessages ?? []),
74
77
  ...(userMemory && userMemory.length > 0 ? [this.formatUserMemory(userMemory)] : []),
75
78
  ...(sessionMemory && sessionMemory.length > 0
76
79
  ? [this.formatSessionMemory(sessionMemory)]
77
80
  : []),
78
- ...(compactSummary
81
+ ...(historyCompact?.summary
79
82
  ? [
80
83
  {
81
84
  role: "system",
82
- content: `Previous conversation summary:\n${compactSummary}`,
85
+ content: `Previous conversation summary:\n${historyCompact.summary}`,
83
86
  },
84
87
  ]
85
88
  : []),
86
- ...historyEntries.flatMap((entry) => entry.content?.messages ?? []),
89
+ // Only inject lastAgentSkill if there's no skill in current messages
90
+ ...(lastAgentSkillToInject && !hasSkillInCurrentMessages
91
+ ? [
92
+ {
93
+ role: "user",
94
+ content: [
95
+ {
96
+ type: "text",
97
+ text: lastAgentSkillToInject.content,
98
+ },
99
+ ],
100
+ },
101
+ ]
102
+ : []),
103
+ ...historyMessages,
87
104
  ...currentMessages,
88
105
  ];
89
106
  // Filter out thinking messages and truncate large messages
@@ -174,7 +191,7 @@ ${"```"}
174
191
  if (this.compactionPromise)
175
192
  await this.compactionPromise;
176
193
  }
177
- this.runtimeState.currentEntryCompression = undefined;
194
+ this.runtimeState.currentEntryCompact = undefined;
178
195
  this.runtimeState.currentEntry = { input, messages: [message] };
179
196
  }
180
197
  async endMessage(output, message, options) {
@@ -210,7 +227,7 @@ ${"```"}
210
227
  }
211
228
  this.runtimeState.historyEntries.push(newEntry);
212
229
  this.runtimeState.currentEntry = null;
213
- this.runtimeState.currentEntryCompression = undefined;
230
+ this.runtimeState.currentEntryCompact = undefined;
214
231
  // Only run compact and memory extraction if mode is not disabled
215
232
  if (this.mode !== "disabled") {
216
233
  await Promise.all([
@@ -294,7 +311,7 @@ ${"```"}
294
311
  // Split into batches to avoid context overflow
295
312
  const batches = this.splitIntoBatches(entriesToCompact, maxTokens);
296
313
  // Process batches incrementally, each summary becomes input for the next
297
- let currentSummary = this.runtimeState.compactSummary;
314
+ let currentSummary = this.runtimeState.historyCompact?.summary;
298
315
  for (const batch of batches) {
299
316
  const messages = batch
300
317
  .flatMap((e) => e.content?.messages ?? [])
@@ -306,19 +323,30 @@ ${"```"}
306
323
  });
307
324
  currentSummary = result.summary;
308
325
  }
326
+ // Extract last Agent Skill from entries to compact
327
+ let lastAgentSkill = this.findLastAgentSkill(entriesToCompact);
328
+ // If no skill found in entries to compact, inherit from previous compact
329
+ if (!lastAgentSkill && this.runtimeState.historyCompact?.lastAgentSkill) {
330
+ lastAgentSkill = this.runtimeState.historyCompact.lastAgentSkill;
331
+ }
332
+ // Create compact content
333
+ const historyCompact = {
334
+ summary: currentSummary ?? "",
335
+ lastAgentSkill,
336
+ };
309
337
  // Write compact entry to AFS
310
338
  if (this.afs && this.historyModulePath) {
311
339
  await this.afs.write(joinURL(this.historyModulePath, "by-session", this.sessionId, "@metadata/compact/new"), {
312
340
  userId: this.userId,
313
341
  agentId: this.agentId,
314
- content: { summary: currentSummary },
342
+ content: historyCompact,
315
343
  metadata: {
316
344
  latestEntryId: latestCompactedEntry.id,
317
345
  },
318
346
  });
319
347
  }
320
348
  // Update runtime state: keep the summary and recent entries
321
- this.runtimeState.compactSummary = currentSummary;
349
+ this.runtimeState.historyCompact = historyCompact;
322
350
  this.runtimeState.historyEntries = entriesToKeep;
323
351
  }
324
352
  async compactCurrentEntry(options) {
@@ -328,7 +356,7 @@ ${"```"}
328
356
  const currentEntry = this.runtimeState.currentEntry;
329
357
  if (!currentEntry?.messages?.length)
330
358
  return;
331
- const alreadyCompressedCount = this.runtimeState.currentEntryCompression?.compressedCount ?? 0;
359
+ const alreadyCompressedCount = this.runtimeState.currentEntryCompact?.compressedCount ?? 0;
332
360
  const uncompressedMessages = currentEntry.messages.slice(alreadyCompressedCount);
333
361
  if (uncompressedMessages.length === 0)
334
362
  return;
@@ -376,13 +404,17 @@ ${"```"}
376
404
  if (messagesToCompact.length === 0)
377
405
  return;
378
406
  const result = await options.context.invoke(compactor, {
379
- previousSummary: this.runtimeState.currentEntryCompression?.summary
380
- ? [this.runtimeState.currentEntryCompression.summary]
407
+ previousSummary: this.runtimeState.currentEntryCompact?.summary
408
+ ? [this.runtimeState.currentEntryCompact.summary]
381
409
  : undefined,
382
410
  messages: messagesToCompact,
383
411
  });
384
- this.runtimeState.currentEntryCompression = {
412
+ // Find last Agent Skill from messages being compacted
413
+ const lastAgentSkill = this.findLastAgentSkillFromMessages(messagesToCompact) ??
414
+ this.runtimeState.currentEntryCompact?.lastAgentSkill;
415
+ this.runtimeState.currentEntryCompact = {
385
416
  summary: result.summary,
417
+ lastAgentSkill,
386
418
  compressedCount: alreadyCompressedCount + messagesToCompact.length,
387
419
  };
388
420
  }
@@ -390,7 +422,7 @@ ${"```"}
390
422
  const currentEntry = this.runtimeState.currentEntry;
391
423
  if (!currentEntry?.messages?.length)
392
424
  return;
393
- const compressedCount = this.runtimeState.currentEntryCompression?.compressedCount ?? 0;
425
+ const compressedCount = this.runtimeState.currentEntryCompact?.compressedCount ?? 0;
394
426
  const uncompressedMessages = currentEntry.messages.slice(compressedCount);
395
427
  const threshold = this.keepTokenBudget;
396
428
  const currentTokens = this.estimateMessagesTokens(uncompressedMessages, this.singleMessageLimit);
@@ -507,7 +539,7 @@ ${"```"}
507
539
  // Update runtime state with loaded data
508
540
  this.runtimeState.userMemory = userMemory;
509
541
  this.runtimeState.sessionMemory = sessionMemory;
510
- this.runtimeState.compactSummary = sessionHistory.compactSummary;
542
+ this.runtimeState.historyCompact = sessionHistory.historyCompact;
511
543
  this.runtimeState.historyEntries = sessionHistory.historyEntries;
512
544
  }
513
545
  }
@@ -560,8 +592,8 @@ ${"```"}
560
592
  return facts;
561
593
  }
562
594
  /**
563
- * Load session history including compact summary and history entries
564
- * @returns Object containing compact summary and history entries
595
+ * Load session history including compact content and history entries
596
+ * @returns Object containing history compact and history entries
565
597
  */
566
598
  async loadSessionHistory() {
567
599
  if (!this.afs || !this.historyModulePath) {
@@ -575,7 +607,7 @@ ${"```"}
575
607
  limit: 1,
576
608
  });
577
609
  const latestCompact = compactResult.data[0];
578
- const compactSummary = latestCompact?.content?.summary;
610
+ const historyCompact = latestCompact?.content;
579
611
  // Load history entries (after compact point if exists)
580
612
  const afsEntries = (await this.afs.list(joinURL(this.historyModulePath, "by-session", this.sessionId), {
581
613
  filter: {
@@ -591,7 +623,7 @@ ${"```"}
591
623
  })).data;
592
624
  const historyEntries = afsEntries.reverse().filter((entry) => isNonNullable(entry.content));
593
625
  return {
594
- compactSummary,
626
+ historyCompact,
595
627
  historyEntries,
596
628
  };
597
629
  }
@@ -825,6 +857,50 @@ ${"```"}
825
857
  }
826
858
  }
827
859
  }
860
+ /**
861
+ * Find Agent Skill content from a single message
862
+ * @param msg - Message to search in
863
+ * @returns The skill content text if found, undefined otherwise
864
+ */
865
+ findSkillContentInMessage(msg) {
866
+ if (msg.role === "user" && Array.isArray(msg.content)) {
867
+ const skillBlock = msg.content.find((block) => block.type === "text" && block.isAgentSkill === true);
868
+ if (skillBlock && skillBlock.type === "text") {
869
+ return skillBlock.text;
870
+ }
871
+ }
872
+ return undefined;
873
+ }
874
+ /**
875
+ * Find the last Agent Skill from a list of messages
876
+ * @param messages - Messages to search through
877
+ * @returns The last Agent Skill found, or undefined if none found
878
+ */
879
+ findLastAgentSkillFromMessages(messages) {
880
+ // Search backwards through messages to find the last Agent Skill
881
+ for (let i = messages.length - 1; i >= 0; i--) {
882
+ const msg = messages[i];
883
+ if (!msg)
884
+ continue;
885
+ const skillContent = this.findSkillContentInMessage(msg);
886
+ if (skillContent) {
887
+ return {
888
+ content: skillContent,
889
+ };
890
+ }
891
+ }
892
+ return undefined;
893
+ }
894
+ /**
895
+ * Find the last Agent Skill from a list of history entries
896
+ * @param entries - History entries to search through
897
+ * @returns The last Agent Skill found, or undefined if none found
898
+ */
899
+ findLastAgentSkill(entries) {
900
+ // Flatten all messages from entries
901
+ const allMessages = entries.flatMap((entry) => entry.content?.messages ?? []);
902
+ return this.findLastAgentSkillFromMessages(allMessages);
903
+ }
828
904
  async initializeDefaultCompactor() {
829
905
  this.compactConfig.compactor ??= await import("./compact/compactor.js").then((m) => new m.AISessionCompactor());
830
906
  }
@@ -37,6 +37,13 @@ export interface EntryContent {
37
37
  */
38
38
  export interface CompactContent extends Message {
39
39
  summary: string;
40
+ /**
41
+ * Last Agent Skill content in the session
42
+ * Preserved across compactions to maintain skill instructions
43
+ */
44
+ lastAgentSkill?: {
45
+ content: string;
46
+ };
40
47
  }
41
48
  /**
42
49
  * Input structure for the compactor agent
@@ -1,4 +1,5 @@
1
1
  import { Agent, type AgentOptions, type Message } from "../../../../agents/agent.js";
2
+ import type { PromiseOrValue } from "../../../../utils/type-utils.js";
2
3
  import type { Skill } from "./skill-loader.js";
3
4
  export interface SkillToolInput extends Message {
4
5
  skill: string;
@@ -11,7 +12,7 @@ export interface SkillToolOptions extends AgentOptions<SkillToolInput, SkillTool
11
12
  agentSkills: Skill[];
12
13
  }
13
14
  export declare class AgentSkill extends Agent<SkillToolInput, SkillToolOutput> {
14
- static formatOutput(output: SkillToolOutput | Message): string;
15
+ formatOutput(output: SkillToolOutput | Message): PromiseOrValue<string>;
15
16
  constructor(options: SkillToolOptions);
16
17
  private agentSkills;
17
18
  process(input: SkillToolInput): Promise<SkillToolOutput>;
@@ -5,11 +5,11 @@ const skillToolInputSchema = z.object({
5
5
  args: z.string().optional().describe("The arguments to pass to the skill."),
6
6
  });
7
7
  export class AgentSkill extends Agent {
8
- static formatOutput(output) {
9
- if (!("result" in output) || typeof output.result !== "string") {
10
- throw new Error("Invalid SkillToolOutput: missing 'result' field");
8
+ formatOutput(output) {
9
+ if ("result" in output && typeof output.result === "string") {
10
+ return output.result;
11
11
  }
12
- return output.result;
12
+ return super.formatOutput(output);
13
13
  }
14
14
  constructor(options) {
15
15
  super({
@@ -34,10 +34,12 @@ export async function loadAgentSkillFromAFS({ afs, }) {
34
34
  return;
35
35
  const skills = [];
36
36
  for (const module of filtered) {
37
- const data = (await afs.list(module.path, {
37
+ const data = (await afs
38
+ .list(module.path, {
38
39
  pattern: "**/SKILL.md",
39
40
  maxDepth: 10,
40
- })).data;
41
+ })
42
+ .catch(() => ({ data: [] }))).data;
41
43
  for (const entry of data) {
42
44
  const { data: file } = await afs.read(entry.path);
43
45
  if (typeof file?.content !== "string")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/core",
3
- "version": "1.72.0-beta.10",
3
+ "version": "1.72.0-beta.12",
4
4
  "description": "The functional core of agentic AI",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -93,10 +93,10 @@
93
93
  "zod": "^3.25.67",
94
94
  "zod-from-json-schema": "^0.0.5",
95
95
  "zod-to-json-schema": "^3.24.6",
96
- "@aigne/afs": "^1.4.0-beta.5",
97
96
  "@aigne/observability-api": "^0.11.14-beta.1",
98
- "@aigne/afs-history": "^1.2.0-beta.6",
99
- "@aigne/platform-helpers": "^0.6.7-beta"
97
+ "@aigne/afs-history": "^1.2.0-beta.7",
98
+ "@aigne/platform-helpers": "^0.6.7-beta",
99
+ "@aigne/afs": "^1.4.0-beta.6"
100
100
  },
101
101
  "devDependencies": {
102
102
  "@types/bun": "^1.2.22",