@gajae-code/agent-core 0.8.1 → 0.8.2
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 +9 -0
- package/dist/types/agent.d.ts +4 -0
- package/dist/types/compaction/compaction.d.ts +19 -0
- package/package.json +4 -4
- package/src/agent.ts +28 -0
- package/src/append-only-context.ts +5 -5
- package/src/compaction/compaction.ts +46 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.8.2] - 2026-07-06
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Agent queues now expose ordered move helpers for steering and follow-up messages so callers can reorder pending work without removing and re-adding messages.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Preserved inherited fork-context seed messages when a compacted child rebase receives only child-local normalized messages, avoiding seed loss after task-child compaction (#1567).
|
|
13
|
+
|
|
5
14
|
## [0.7.7] - 2026-06-28
|
|
6
15
|
|
|
7
16
|
### Fixed
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -349,11 +349,15 @@ export declare class Agent {
|
|
|
349
349
|
* Used by dequeue keybinding.
|
|
350
350
|
*/
|
|
351
351
|
popLastSteer(): AgentMessage | undefined;
|
|
352
|
+
removeSteerAt(index: number): AgentMessage | undefined;
|
|
353
|
+
moveSteer(fromIndex: number, toIndex: number): boolean;
|
|
352
354
|
/**
|
|
353
355
|
* Remove and return the last follow-up message from the queue (LIFO).
|
|
354
356
|
* Used by dequeue keybinding.
|
|
355
357
|
*/
|
|
356
358
|
popLastFollowUp(): AgentMessage | undefined;
|
|
359
|
+
removeFollowUpAt(index: number): AgentMessage | undefined;
|
|
360
|
+
moveFollowUp(fromIndex: number, toIndex: number): boolean;
|
|
357
361
|
/** Remove queued steering+follow-up messages matching `predicate`, preserving order of the rest. */
|
|
358
362
|
removeQueuedMessages(predicate: (message: AgentMessage) => boolean): {
|
|
359
363
|
steering: number;
|
|
@@ -176,6 +176,25 @@ export interface SummaryOptions {
|
|
|
176
176
|
/** Hint that websocket transport should be preferred when supported by the provider implementation. */
|
|
177
177
|
preferWebsockets?: boolean;
|
|
178
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Cap the serialized conversation fed to a summarization request so the request
|
|
181
|
+
* itself fits inside the model's context window.
|
|
182
|
+
*
|
|
183
|
+
* Without this, summarizing a near-full context serializes (nearly) the entire
|
|
184
|
+
* history back into a single summary request; on strict backends (e.g.
|
|
185
|
+
* OpenAI-code/Codex `context_length_exceeded`) that request itself overflows and
|
|
186
|
+
* throws, so context-overflow recovery cannot produce a summary and the agent
|
|
187
|
+
* fails to compact-and-continue — a non-interactive `gjc -p` run then terminates
|
|
188
|
+
* on the very overflow the recovery was meant to absorb.
|
|
189
|
+
*
|
|
190
|
+
* The budget reserves the summary's own output tokens plus prompt/system/template
|
|
191
|
+
* overhead, and applies a conservative safety factor because the chars/4 heuristic
|
|
192
|
+
* undercounts dense or CJK text (the reason the original overflow was missed).
|
|
193
|
+
* Truncation keeps the head (origin/goals) and the tail (most recent state) and
|
|
194
|
+
* elides the middle; it is a last resort that only triggers when the input would
|
|
195
|
+
* otherwise not fit.
|
|
196
|
+
*/
|
|
197
|
+
export declare function boundConversationTextForSummary(conversationText: string, model: Model, outputMaxTokens: number): string;
|
|
179
198
|
export declare function generateSummary(currentMessages: AgentMessage[], model: Model, reserveTokens: number, apiKey: string, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, options?: SummaryOptions): Promise<string>;
|
|
180
199
|
export interface HandoffOptions {
|
|
181
200
|
/** Live agent system prompt — passed verbatim so providers hit the cached prefix. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/agent-core",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.2",
|
|
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",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@gajae-code/ai": "0.8.
|
|
39
|
-
"@gajae-code/natives": "0.8.
|
|
40
|
-
"@gajae-code/utils": "0.8.
|
|
38
|
+
"@gajae-code/ai": "0.8.2",
|
|
39
|
+
"@gajae-code/natives": "0.8.2",
|
|
40
|
+
"@gajae-code/utils": "0.8.2",
|
|
41
41
|
"@opentelemetry/api": "^1.9.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
package/src/agent.ts
CHANGED
|
@@ -954,6 +954,15 @@ export class Agent {
|
|
|
954
954
|
popLastSteer(): AgentMessage | undefined {
|
|
955
955
|
return this.#steeringQueue.pop();
|
|
956
956
|
}
|
|
957
|
+
removeSteerAt(index: number): AgentMessage | undefined {
|
|
958
|
+
if (index < 0 || index >= this.#steeringQueue.length) return undefined;
|
|
959
|
+
const [removed] = this.#steeringQueue.splice(index, 1);
|
|
960
|
+
return removed;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
moveSteer(fromIndex: number, toIndex: number): boolean {
|
|
964
|
+
return this.#moveQueuedMessage(this.#steeringQueue, fromIndex, toIndex);
|
|
965
|
+
}
|
|
957
966
|
|
|
958
967
|
/**
|
|
959
968
|
* Remove and return the last follow-up message from the queue (LIFO).
|
|
@@ -962,6 +971,25 @@ export class Agent {
|
|
|
962
971
|
popLastFollowUp(): AgentMessage | undefined {
|
|
963
972
|
return this.#followUpQueue.pop();
|
|
964
973
|
}
|
|
974
|
+
removeFollowUpAt(index: number): AgentMessage | undefined {
|
|
975
|
+
if (index < 0 || index >= this.#followUpQueue.length) return undefined;
|
|
976
|
+
const [removed] = this.#followUpQueue.splice(index, 1);
|
|
977
|
+
return removed;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
moveFollowUp(fromIndex: number, toIndex: number): boolean {
|
|
981
|
+
return this.#moveQueuedMessage(this.#followUpQueue, fromIndex, toIndex);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
#moveQueuedMessage<T>(queue: T[], fromIndex: number, toIndex: number): boolean {
|
|
985
|
+
if (fromIndex < 0 || fromIndex >= queue.length) return false;
|
|
986
|
+
if (toIndex < 0 || toIndex >= queue.length) return false;
|
|
987
|
+
if (fromIndex === toIndex) return true;
|
|
988
|
+
const [item] = queue.splice(fromIndex, 1);
|
|
989
|
+
if (item === undefined) return false;
|
|
990
|
+
queue.splice(toIndex, 0, item);
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
965
993
|
|
|
966
994
|
/** Remove queued steering+follow-up messages matching `predicate`, preserving order of the rest. */
|
|
967
995
|
removeQueuedMessages(predicate: (message: AgentMessage) => boolean): {
|
|
@@ -252,7 +252,7 @@ export class AppendOnlyContextManager {
|
|
|
252
252
|
if (this.#seededPrefixCount > 0) {
|
|
253
253
|
// F9: a seeded fork whose inherited prefix changed (e.g. after compaction)
|
|
254
254
|
// rebases onto the new provider context instead of throwing.
|
|
255
|
-
this.#rebaseToBaseline(
|
|
255
|
+
this.#rebaseToBaseline(messagesToSync, seededPrefixLength);
|
|
256
256
|
return;
|
|
257
257
|
}
|
|
258
258
|
this.log.clear();
|
|
@@ -265,7 +265,7 @@ export class AppendOnlyContextManager {
|
|
|
265
265
|
// while a seed prefix is active; a genuine seeded compaction rebases (F9).
|
|
266
266
|
if (messagesToSync.length < this.#lastSyncCount) {
|
|
267
267
|
if (this.#seededPrefixCount > 0) {
|
|
268
|
-
this.#rebaseToBaseline(
|
|
268
|
+
this.#rebaseToBaseline(messagesToSync, seededPrefixLength);
|
|
269
269
|
return;
|
|
270
270
|
}
|
|
271
271
|
this.log.clear();
|
|
@@ -359,12 +359,12 @@ export class AppendOnlyContextManager {
|
|
|
359
359
|
return false;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
/** F9: reset the
|
|
363
|
-
#rebaseToBaseline(messages: readonly unknown[]): void {
|
|
362
|
+
/** F9: reset the log to a new provider-visible baseline after seeded compaction/rebase. */
|
|
363
|
+
#rebaseToBaseline(messages: readonly unknown[], seededPrefixCount = 0): void {
|
|
364
364
|
this.log.clear();
|
|
365
365
|
this.log.extend([...messages]);
|
|
366
366
|
this.#lastSyncCount = messages.length;
|
|
367
|
-
this.#seededPrefixCount =
|
|
367
|
+
this.#seededPrefixCount = seededPrefixCount;
|
|
368
368
|
this.#syncedHashes = this.#hashRange(messages, 0, messages.length);
|
|
369
369
|
}
|
|
370
370
|
}
|
|
@@ -679,6 +679,49 @@ export interface SummaryOptions {
|
|
|
679
679
|
preferWebsockets?: boolean;
|
|
680
680
|
}
|
|
681
681
|
|
|
682
|
+
/**
|
|
683
|
+
* Cap the serialized conversation fed to a summarization request so the request
|
|
684
|
+
* itself fits inside the model's context window.
|
|
685
|
+
*
|
|
686
|
+
* Without this, summarizing a near-full context serializes (nearly) the entire
|
|
687
|
+
* history back into a single summary request; on strict backends (e.g.
|
|
688
|
+
* OpenAI-code/Codex `context_length_exceeded`) that request itself overflows and
|
|
689
|
+
* throws, so context-overflow recovery cannot produce a summary and the agent
|
|
690
|
+
* fails to compact-and-continue — a non-interactive `gjc -p` run then terminates
|
|
691
|
+
* on the very overflow the recovery was meant to absorb.
|
|
692
|
+
*
|
|
693
|
+
* The budget reserves the summary's own output tokens plus prompt/system/template
|
|
694
|
+
* overhead, and applies a conservative safety factor because the chars/4 heuristic
|
|
695
|
+
* undercounts dense or CJK text (the reason the original overflow was missed).
|
|
696
|
+
* Truncation keeps the head (origin/goals) and the tail (most recent state) and
|
|
697
|
+
* elides the middle; it is a last resort that only triggers when the input would
|
|
698
|
+
* otherwise not fit.
|
|
699
|
+
*/
|
|
700
|
+
export function boundConversationTextForSummary(
|
|
701
|
+
conversationText: string,
|
|
702
|
+
model: Model,
|
|
703
|
+
outputMaxTokens: number,
|
|
704
|
+
): string {
|
|
705
|
+
const contextWindow = model.contextWindow;
|
|
706
|
+
if (!Number.isFinite(contextWindow) || contextWindow <= 0) return conversationText;
|
|
707
|
+
|
|
708
|
+
const OVERHEAD_TOKENS = 4096;
|
|
709
|
+
const SAFETY_FACTOR = 0.6;
|
|
710
|
+
const inputBudgetTokens = Math.floor(
|
|
711
|
+
(contextWindow - Math.max(0, outputMaxTokens) - OVERHEAD_TOKENS) * SAFETY_FACTOR,
|
|
712
|
+
);
|
|
713
|
+
if (inputBudgetTokens <= 0) return conversationText;
|
|
714
|
+
if (estimateTextTokensHeuristic(conversationText) <= inputBudgetTokens) return conversationText;
|
|
715
|
+
|
|
716
|
+
const budgetChars = inputBudgetTokens * HEURISTIC_BYTES_PER_TOKEN;
|
|
717
|
+
const headChars = Math.floor(budgetChars * 0.35);
|
|
718
|
+
const tailChars = Math.max(0, budgetChars - headChars);
|
|
719
|
+
const head = conversationText.slice(0, headChars);
|
|
720
|
+
const tail = tailChars > 0 ? conversationText.slice(conversationText.length - tailChars) : "";
|
|
721
|
+
const elided = conversationText.length - head.length - tail.length;
|
|
722
|
+
return `${head}\n\n[... ${elided} characters of older conversation elided so this summarization request fits within the model context window ...]\n\n${tail}`;
|
|
723
|
+
}
|
|
724
|
+
|
|
682
725
|
export async function generateSummary(
|
|
683
726
|
currentMessages: AgentMessage[],
|
|
684
727
|
model: Model,
|
|
@@ -703,7 +746,7 @@ export async function generateSummary(
|
|
|
703
746
|
// Serialize conversation to text so model doesn't try to continue it
|
|
704
747
|
// Convert to LLM messages first (handles custom app messages when caller provides a transformer).
|
|
705
748
|
const llmMessages = (options?.convertToLlm ?? convertToLlm)(currentMessages);
|
|
706
|
-
const conversationText = serializeConversation(llmMessages);
|
|
749
|
+
const conversationText = boundConversationTextForSummary(serializeConversation(llmMessages), model, maxTokens);
|
|
707
750
|
|
|
708
751
|
// Build the prompt with conversation wrapped in tags
|
|
709
752
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
@@ -859,7 +902,7 @@ async function generateShortSummary(
|
|
|
859
902
|
): Promise<string> {
|
|
860
903
|
const maxTokens = Math.min(512, Math.floor(0.2 * reserveTokens));
|
|
861
904
|
const llmMessages = (options?.convertToLlm ?? convertToLlm)(recentMessages);
|
|
862
|
-
const conversationText = serializeConversation(llmMessages);
|
|
905
|
+
const conversationText = boundConversationTextForSummary(serializeConversation(llmMessages), model, maxTokens);
|
|
863
906
|
|
|
864
907
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
865
908
|
if (historySummary) {
|
|
@@ -1235,7 +1278,7 @@ async function generateTurnPrefixSummary(
|
|
|
1235
1278
|
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
|
|
1236
1279
|
|
|
1237
1280
|
const llmMessages = (options?.convertToLlm ?? convertToLlm)(messages);
|
|
1238
|
-
const conversationText = serializeConversation(llmMessages);
|
|
1281
|
+
const conversationText = boundConversationTextForSummary(serializeConversation(llmMessages), model, maxTokens);
|
|
1239
1282
|
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
1240
1283
|
const summarizationMessages = [
|
|
1241
1284
|
{
|