@gajae-code/agent-core 0.11.2 → 0.11.4

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
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.11.3] - 2026-07-19
6
+
7
+ ### Fixed
8
+ - Pre-compaction pruning now preserves bounded, actionable error evidence instead of discarding it, while enforcing exact positive-savings admission and accounting so a prune is only applied when it demonstrably reduces context cost (#2635).
9
+
5
10
  ## [0.11.1] - 2026-07-16
6
11
 
7
12
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/agent-core",
4
- "version": "0.11.2",
4
+ "version": "0.11.4",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -32,9 +32,9 @@
32
32
  "fmt": "biome format --write ."
33
33
  },
34
34
  "dependencies": {
35
- "@gajae-code/ai": "0.11.2",
36
- "@gajae-code/natives": "0.11.2",
37
- "@gajae-code/utils": "0.11.2",
35
+ "@gajae-code/ai": "0.11.4",
36
+ "@gajae-code/natives": "0.11.4",
37
+ "@gajae-code/utils": "0.11.4",
38
38
  "@opentelemetry/api": "^1.9.0"
39
39
  },
40
40
  "devDependencies": {
@@ -9,8 +9,9 @@
9
9
  */
10
10
 
11
11
  import type { ToolCall, ToolResultMessage } from "@gajae-code/ai";
12
+ import { sanitizeText } from "@gajae-code/utils";
12
13
  import type { AgentMessage } from "../types";
13
- import { estimateEntryTokens } from "./compaction";
14
+ import { estimateEntryTokens, estimateTextTokensHeuristic } from "./compaction";
14
15
  import type { SessionEntry, SessionMessageEntry } from "./entries";
15
16
 
16
17
  export interface PruneConfig {
@@ -48,6 +49,7 @@ export interface PruneResult {
48
49
  }
49
50
 
50
51
  const DIGEST_NOTICE_TOKEN_CAP_MULTIPLIER = 1.25;
52
+ const ERROR_DIGEST_NOTICE_MIN_CHARS = 240;
51
53
 
52
54
  function createGenericPrunedNotice(tokens: number): string {
53
55
  return `[Output truncated - ${tokens} tokens]`;
@@ -66,6 +68,17 @@ function firstErrorLine(text: string): string | undefined {
66
68
  ?.trim();
67
69
  }
68
70
 
71
+ function firstNonEmptyLine(text: string): string | undefined {
72
+ return text
73
+ .split(/\r?\n/)
74
+ .find(line => line.trim().length > 0)
75
+ ?.trim();
76
+ }
77
+
78
+ function lastNonEmptyLine(text: string): string | undefined {
79
+ return text.trim().split(/\r?\n/).filter(Boolean).at(-1)?.trim();
80
+ }
81
+
69
82
  function truncateField(value: string, maxLength: number): string {
70
83
  if (value.length <= maxLength) return value;
71
84
  if (maxLength <= 1) return "…";
@@ -74,7 +87,7 @@ function truncateField(value: string, maxLength: number): string {
74
87
 
75
88
  function resultDigest(message: ToolResultMessage): string | undefined {
76
89
  const toolName = message.toolName.toLowerCase();
77
- const text = firstTextContent(message);
90
+ const text = sanitizeText(firstTextContent(message));
78
91
  if (toolName === "bash") {
79
92
  const details = message as { details?: { exitCode?: unknown } };
80
93
  const exitCode =
@@ -99,7 +112,12 @@ function resultDigest(message: ToolResultMessage): string | undefined {
99
112
  .join("; ") || "search digest unavailable"
100
113
  );
101
114
  }
102
- return undefined;
115
+ if (message.isError !== true) return undefined;
116
+ if (text.trim().length === 0) return "error=tool result failed without text";
117
+ const error = firstErrorLine(text);
118
+ if (error) return `error=${error}`;
119
+ const summary = firstNonEmptyLine(text) ?? lastNonEmptyLine(text);
120
+ return summary ? `summary=${summary}` : undefined;
103
121
  }
104
122
 
105
123
  function createPrunedNotice(tokens: number, message?: ToolResultMessage): string {
@@ -110,7 +128,9 @@ function createPrunedNotice(tokens: number, message?: ToolResultMessage): string
110
128
  const maxTokens = Math.max(genericTokens, Math.floor(genericTokens * DIGEST_NOTICE_TOKEN_CAP_MULTIPLIER));
111
129
  const prefix = `[Output truncated - ${tokens} tokens; `;
112
130
  const suffix = "]";
113
- const maxChars = Math.max(0, maxTokens * 4 - prefix.length - suffix.length);
131
+ const digestChars = maxTokens * 4 - prefix.length - suffix.length;
132
+ const maxChars =
133
+ message?.isError === true ? Math.max(ERROR_DIGEST_NOTICE_MIN_CHARS, digestChars) : Math.max(0, digestChars);
114
134
  return `${prefix}${truncateField(digest, maxChars)}${suffix}`;
115
135
  }
116
136
 
@@ -122,8 +142,7 @@ function getToolResultMessage(entry: SessionEntry): ToolResultMessage | undefine
122
142
  }
123
143
 
124
144
  function estimatePrunedSavings(tokens: number, notice: string): number {
125
- const noticeTokens = Math.ceil(notice.length / 4);
126
- return Math.max(0, tokens - noticeTokens);
145
+ return tokens - estimateTextTokensHeuristic(notice);
127
146
  }
128
147
 
129
148
  export interface AssistantArgumentPruneResult {
@@ -670,11 +689,17 @@ function collectToolOutputPruneCandidates(
670
689
  }
671
690
 
672
691
  const notice = createPrunedNotice(tokens, message);
692
+ const savings = estimatePrunedSavings(tokens, notice);
693
+ const errorNoticeGrows = message.isError === true && notice.length > firstTextContent(message).length;
694
+ if (savings <= 0 || errorNoticeGrows) {
695
+ accumulatedTokens += tokens;
696
+ continue;
697
+ }
673
698
  candidates.push({
674
699
  entry: entry as SessionMessageEntry,
675
700
  tokens,
676
701
  notice,
677
- savings: estimatePrunedSavings(tokens, notice),
702
+ savings,
678
703
  });
679
704
  accumulatedTokens += tokens;
680
705
  }