@gajae-code/agent-core 0.8.0 → 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 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
@@ -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.0",
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.0",
39
- "@gajae-code/natives": "0.8.0",
40
- "@gajae-code/utils": "0.8.0",
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(normalizedMessages);
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(normalizedMessages);
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 seeded log to a new provider-visible baseline (seeded compaction/rebase). */
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 = 0;
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
  {