@gajae-code/agent-core 0.13.2 → 0.13.3
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 +6 -0
- package/dist/types/compaction/compaction.d.ts +5 -2
- package/dist/types/types.d.ts +5 -0
- package/package.json +4 -4
- package/src/agent-loop.ts +54 -0
- package/src/compaction/compaction.ts +12 -1
- package/src/types.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.13.3] - 2026-08-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Emergency compaction now considers managed transcript file size so sessions compact before the managed per-file limit (#4411).
|
|
9
|
+
- Managed runs discard assistant turns whose tool calls carried `\uXXXX`-escaped arguments and report them through the typed `escaped_arguments_discarded` outcome instead of executing unverifiable text; unmanaged runs reject such calls per-call with an actionable error (#4515).
|
|
10
|
+
|
|
5
11
|
## [0.13.2] - 2026-08-13
|
|
6
12
|
|
|
7
13
|
## [0.13.1] - 2026-08-11
|
|
@@ -80,7 +80,7 @@ export declare function effectiveReserveTokens(contextWindow: number, settings:
|
|
|
80
80
|
*/
|
|
81
81
|
export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings, maxOutputTokens?: number): boolean;
|
|
82
82
|
/** Reason a compaction was triggered. `token` is the normal user-configurable path; the rest are emergency floors. */
|
|
83
|
-
export type CompactionTriggerReason = "token" | "heap" | "retainedMemory" | "providerBytes" | "messageCount" | "imageBytes";
|
|
83
|
+
export type CompactionTriggerReason = "token" | "heap" | "retainedMemory" | "transcriptFile" | "providerBytes" | "messageCount" | "imageBytes";
|
|
84
84
|
/** A point-in-time resource sample. Supplied by an injectable sampler so tests never read real RSS. */
|
|
85
85
|
export interface EmergencyCompactionSample {
|
|
86
86
|
/** Resident heap bytes (e.g. process.memoryUsage().heapUsed). */
|
|
@@ -99,6 +99,8 @@ export interface EmergencyCompactionSample {
|
|
|
99
99
|
tuiChatChildren?: number;
|
|
100
100
|
/** Bytes retained by TUI render caches. */
|
|
101
101
|
tuiCachedRenderBytes?: number;
|
|
102
|
+
/** On-disk JSONL transcript file size in bytes; 0/undefined when unknown. */
|
|
103
|
+
transcriptFileBytes?: number;
|
|
102
104
|
}
|
|
103
105
|
export interface EmergencyCompactionLimits {
|
|
104
106
|
heapUsedBytes: number;
|
|
@@ -109,6 +111,7 @@ export interface EmergencyCompactionLimits {
|
|
|
109
111
|
retainedMemoryDiagnosticBytes?: number;
|
|
110
112
|
tuiChatChildren?: number;
|
|
111
113
|
tuiChatChildrenDiagnostic?: number;
|
|
114
|
+
transcriptFileBytes?: number;
|
|
112
115
|
}
|
|
113
116
|
export declare function resetEmergencyRetainedMemoryDiagnosticsForTests(): void;
|
|
114
117
|
export declare function resolveEmergencyCompactionLimits(totalMemoryBytes?: number): EmergencyCompactionLimits;
|
|
@@ -119,7 +122,7 @@ export declare function resolveEmergencyCompactionLimits(totalMemoryBytes?: numb
|
|
|
119
122
|
*/
|
|
120
123
|
export declare const DEFAULT_EMERGENCY_COMPACTION_LIMITS: EmergencyCompactionLimits;
|
|
121
124
|
/**
|
|
122
|
-
* Returns the first emergency limit exceeded (heap > retainedMemory > providerBytes > imageBytes > messageCount),
|
|
125
|
+
* Returns the first emergency limit exceeded (heap > retainedMemory > transcriptFile > providerBytes > imageBytes > messageCount),
|
|
123
126
|
* or null when none is. Pure apart from retained-memory diagnostics; the caller routes the result through the
|
|
124
127
|
* normal pair-safe `compact()` cut logic so a tool_use/tool_result pair is never split.
|
|
125
128
|
*/
|
package/dist/types/types.d.ts
CHANGED
|
@@ -162,6 +162,11 @@ export type ManagedAttemptOutcome = {
|
|
|
162
162
|
transportFailure?: TransportFailureFacts;
|
|
163
163
|
};
|
|
164
164
|
scope?: AttemptScope;
|
|
165
|
+
} | {
|
|
166
|
+
type: "escaped_arguments_discarded";
|
|
167
|
+
/** The defective assistant turn; already removed from usable history by the loop. */
|
|
168
|
+
message: AssistantMessage;
|
|
169
|
+
scope?: AttemptScope;
|
|
165
170
|
} | {
|
|
166
171
|
type: "context_overflow_discarded";
|
|
167
172
|
message: AssistantMessage;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/agent-core",
|
|
4
|
-
"version": "0.13.
|
|
4
|
+
"version": "0.13.3",
|
|
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.13.
|
|
36
|
-
"@gajae-code/natives": "0.13.
|
|
37
|
-
"@gajae-code/utils": "0.13.
|
|
35
|
+
"@gajae-code/ai": "0.13.3",
|
|
36
|
+
"@gajae-code/natives": "0.13.3",
|
|
37
|
+
"@gajae-code/utils": "0.13.3",
|
|
38
38
|
"@opentelemetry/api": "^1.9.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -263,6 +263,15 @@ function managedContextOverflowOutcome(message: AssistantMessage, scope?: Attemp
|
|
|
263
263
|
return { type: "context_overflow_discarded", message, scope };
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
/** Whether any tool call in the turn carried `\uXXXX`-escaped arguments. */
|
|
267
|
+
function hasEscapedNonAsciiToolCall(message: AssistantMessage): boolean {
|
|
268
|
+
return message.content.some(block => block.type === "toolCall" && block.escapedNonAsciiArguments === true);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function managedEscapedArgumentsOutcome(message: AssistantMessage, scope?: AttemptScope): ManagedAttemptOutcome {
|
|
272
|
+
return { type: "escaped_arguments_discarded", message, scope };
|
|
273
|
+
}
|
|
274
|
+
|
|
266
275
|
function managedFailureMessage(error: unknown, config: AgentLoopConfig): AssistantMessage {
|
|
267
276
|
const errorMessage = managedProperty(error, "message");
|
|
268
277
|
const transportFailure = managedTransportFailure(error);
|
|
@@ -809,6 +818,7 @@ function managedAssistantContent(value: unknown): AssistantMessage["content"][nu
|
|
|
809
818
|
const intent = managedProperty(value, "intent");
|
|
810
819
|
const customWireName = managedProperty(value, "customWireName");
|
|
811
820
|
const incompleteArguments = managedProperty(value, "incompleteArguments");
|
|
821
|
+
const escapedNonAsciiArguments = managedProperty(value, "escapedNonAsciiArguments");
|
|
812
822
|
return {
|
|
813
823
|
type,
|
|
814
824
|
id,
|
|
@@ -818,6 +828,7 @@ function managedAssistantContent(value: unknown): AssistantMessage["content"][nu
|
|
|
818
828
|
...(typeof intent === "string" ? { intent } : {}),
|
|
819
829
|
...(typeof customWireName === "string" ? { customWireName } : {}),
|
|
820
830
|
...(typeof incompleteArguments === "boolean" ? { incompleteArguments } : {}),
|
|
831
|
+
...(typeof escapedNonAsciiArguments === "boolean" ? { escapedNonAsciiArguments } : {}),
|
|
821
832
|
};
|
|
822
833
|
}
|
|
823
834
|
|
|
@@ -1773,6 +1784,35 @@ async function runLoopBody(
|
|
|
1773
1784
|
}
|
|
1774
1785
|
}
|
|
1775
1786
|
|
|
1787
|
+
// Escaped-non-ASCII tool arguments: managed bounded turn resample.
|
|
1788
|
+
//
|
|
1789
|
+
// Arguments that spell a printable non-ASCII character as `\uXXXX`
|
|
1790
|
+
// instead of literal UTF-8 are a wire-format defect, not a decision the
|
|
1791
|
+
// model needs to be told about. The payload parses cleanly, but one
|
|
1792
|
+
// mistyped nibble decodes to a different, equally valid character, so it
|
|
1793
|
+
// can never be verified or repaired after the fact. Reporting it as a
|
|
1794
|
+
// tool error spends the whole turn and writes the literal escape syntax
|
|
1795
|
+
// back into the context the model samples from next. A managed
|
|
1796
|
+
// invocation instead drops the defective turn and reports it through the
|
|
1797
|
+
// typed `escaped_arguments_discarded` outcome so the session policy owns
|
|
1798
|
+
// a bounded same-model retry; the defect is never treated as provider
|
|
1799
|
+
// evidence, so the fallback chain never advances on it. Unmanaged runs
|
|
1800
|
+
// keep the per-call rejection in `executeToolCalls` as the terminal
|
|
1801
|
+
// answer.
|
|
1802
|
+
if (
|
|
1803
|
+
config.fallbackManaged &&
|
|
1804
|
+
message.stopReason !== "error" &&
|
|
1805
|
+
message.stopReason !== "aborted" &&
|
|
1806
|
+
hasEscapedNonAsciiToolCall(message)
|
|
1807
|
+
) {
|
|
1808
|
+
transaction?.discard();
|
|
1809
|
+
currentContext.messages.splice(contextMessageCount);
|
|
1810
|
+
newMessages.splice(newMessageCount);
|
|
1811
|
+
await config.onManagedAttemptOutcome?.(managedEscapedArgumentsOutcome(message, transaction?.scope));
|
|
1812
|
+
stream.end(newMessages);
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1776
1816
|
const overflow = managedContextOverflow(message, config);
|
|
1777
1817
|
if (config.fallbackManaged && overflow) {
|
|
1778
1818
|
transaction?.discard();
|
|
@@ -2743,6 +2783,20 @@ async function executeToolCalls(
|
|
|
2743
2783
|
`Re-issue the call with complete arguments, splitting the work into smaller steps if needed.`,
|
|
2744
2784
|
);
|
|
2745
2785
|
}
|
|
2786
|
+
if (toolCall.escapedNonAsciiArguments) {
|
|
2787
|
+
record.argumentValidationFailed = true;
|
|
2788
|
+
// The arguments decoded cleanly, but they were spelled as `\uXXXX`
|
|
2789
|
+
// escapes rather than literal UTF-8. Hand-written hex is where models
|
|
2790
|
+
// mistype digits, and every mistyped nibble decodes to a different but
|
|
2791
|
+
// equally valid character — the payload is unverifiable and cannot be
|
|
2792
|
+
// repaired after parsing, so it is rejected rather than executed on
|
|
2793
|
+
// silently corrupted text.
|
|
2794
|
+
throw new Error(
|
|
2795
|
+
`Tool call "${toolCall.name}" spelled non-ASCII text as \\uXXXX escapes instead of literal UTF-8. ` +
|
|
2796
|
+
`Escaped text cannot be verified — a single wrong hex digit silently becomes a different character — ` +
|
|
2797
|
+
`so the call was not executed. Re-issue it writing every non-ASCII character literally.`,
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2746
2800
|
if (!tool) {
|
|
2747
2801
|
// A discoverable tool that hasn't been activated yet resolves to
|
|
2748
2802
|
// undefined here. The model often "remembers" such a tool (e.g.
|
|
@@ -253,6 +253,7 @@ export type CompactionTriggerReason =
|
|
|
253
253
|
| "token"
|
|
254
254
|
| "heap"
|
|
255
255
|
| "retainedMemory"
|
|
256
|
+
| "transcriptFile"
|
|
256
257
|
| "providerBytes"
|
|
257
258
|
| "messageCount"
|
|
258
259
|
| "imageBytes";
|
|
@@ -275,6 +276,8 @@ export interface EmergencyCompactionSample {
|
|
|
275
276
|
tuiChatChildren?: number;
|
|
276
277
|
/** Bytes retained by TUI render caches. */
|
|
277
278
|
tuiCachedRenderBytes?: number;
|
|
279
|
+
/** On-disk JSONL transcript file size in bytes; 0/undefined when unknown. */
|
|
280
|
+
transcriptFileBytes?: number;
|
|
278
281
|
}
|
|
279
282
|
|
|
280
283
|
export interface EmergencyCompactionLimits {
|
|
@@ -286,6 +289,7 @@ export interface EmergencyCompactionLimits {
|
|
|
286
289
|
retainedMemoryDiagnosticBytes?: number;
|
|
287
290
|
tuiChatChildren?: number;
|
|
288
291
|
tuiChatChildrenDiagnostic?: number;
|
|
292
|
+
transcriptFileBytes?: number;
|
|
289
293
|
}
|
|
290
294
|
|
|
291
295
|
const MAX_EMERGENCY_HEAP_FLOOR_BYTES = 1_536 * 1024 * 1024; // 1.5 GiB resident heap
|
|
@@ -293,6 +297,7 @@ const EMERGENCY_RETAINED_MEMORY_BYTES = 128 * 1024 * 1024;
|
|
|
293
297
|
const DIAGNOSTIC_RETAINED_MEMORY_BYTES = 64 * 1024 * 1024;
|
|
294
298
|
const EMERGENCY_TUI_CHAT_CHILDREN = 1000;
|
|
295
299
|
const DIAGNOSTIC_TUI_CHAT_CHILDREN = 700;
|
|
300
|
+
const EMERGENCY_TRANSCRIPT_FILE_BYTES = 48 * 1024 * 1024; // 48 MiB (75% of the 64 MiB managed cap)
|
|
296
301
|
let retainedMemoryDiagnosticActive = false;
|
|
297
302
|
let tuiChatChildrenDiagnosticActive = false;
|
|
298
303
|
|
|
@@ -315,6 +320,7 @@ export function resolveEmergencyCompactionLimits(totalMemoryBytes: number = os.t
|
|
|
315
320
|
retainedMemoryDiagnosticBytes: DIAGNOSTIC_RETAINED_MEMORY_BYTES,
|
|
316
321
|
tuiChatChildren: EMERGENCY_TUI_CHAT_CHILDREN,
|
|
317
322
|
tuiChatChildrenDiagnostic: DIAGNOSTIC_TUI_CHAT_CHILDREN,
|
|
323
|
+
transcriptFileBytes: EMERGENCY_TRANSCRIPT_FILE_BYTES,
|
|
318
324
|
};
|
|
319
325
|
}
|
|
320
326
|
|
|
@@ -326,7 +332,7 @@ export function resolveEmergencyCompactionLimits(totalMemoryBytes: number = os.t
|
|
|
326
332
|
export const DEFAULT_EMERGENCY_COMPACTION_LIMITS: EmergencyCompactionLimits = resolveEmergencyCompactionLimits();
|
|
327
333
|
|
|
328
334
|
/**
|
|
329
|
-
* Returns the first emergency limit exceeded (heap > retainedMemory > providerBytes > imageBytes > messageCount),
|
|
335
|
+
* Returns the first emergency limit exceeded (heap > retainedMemory > transcriptFile > providerBytes > imageBytes > messageCount),
|
|
330
336
|
* or null when none is. Pure apart from retained-memory diagnostics; the caller routes the result through the
|
|
331
337
|
* normal pair-safe `compact()` cut logic so a tool_use/tool_result pair is never split.
|
|
332
338
|
*/
|
|
@@ -360,6 +366,11 @@ export function emergencyCompactionReason(
|
|
|
360
366
|
tuiChatChildren >= (limits.tuiChatChildren ?? EMERGENCY_TUI_CHAT_CHILDREN)
|
|
361
367
|
)
|
|
362
368
|
return "retainedMemory";
|
|
369
|
+
if (
|
|
370
|
+
sample.transcriptFileBytes &&
|
|
371
|
+
sample.transcriptFileBytes > (limits.transcriptFileBytes ?? EMERGENCY_TRANSCRIPT_FILE_BYTES)
|
|
372
|
+
)
|
|
373
|
+
return "transcriptFile";
|
|
363
374
|
if (sample.providerBytes > limits.providerBytes) return "providerBytes";
|
|
364
375
|
if (sample.imageBytes > limits.imageBytes) return "imageBytes";
|
|
365
376
|
if (sample.messageCount > limits.messageCount) return "messageCount";
|
package/src/types.ts
CHANGED
|
@@ -182,6 +182,12 @@ export type ManagedAttemptOutcome =
|
|
|
182
182
|
};
|
|
183
183
|
scope?: AttemptScope;
|
|
184
184
|
}
|
|
185
|
+
| {
|
|
186
|
+
type: "escaped_arguments_discarded";
|
|
187
|
+
/** The defective assistant turn; already removed from usable history by the loop. */
|
|
188
|
+
message: AssistantMessage;
|
|
189
|
+
scope?: AttemptScope;
|
|
190
|
+
}
|
|
185
191
|
| { type: "context_overflow_discarded"; message: AssistantMessage; scope?: AttemptScope }
|
|
186
192
|
| { type: "run_terminal"; reason: "cancelled" | "error" | "exhausted"; scope?: AttemptScope };
|
|
187
193
|
|