@gajae-code/agent-core 0.2.2 → 0.2.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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.4] - 2026-06-02
6
+
7
+ ### Added
8
+
9
+ - Added agent options for provider request and stream retry budgets and thread them through streaming calls.
10
+
5
11
  ## [0.2.2] - 2026-05-31
6
12
 
7
13
  ### Changed
@@ -51,5 +51,6 @@ export declare function agentLoopContinueDetailed(context: AgentContext, config:
51
51
  readonly stream: EventStream<AgentEvent, AgentMessage[]>;
52
52
  readonly detailed: () => Promise<AgentLoopDetailedResult>;
53
53
  };
54
+ export declare function normalizeMessagesForProvider(messages: Context["messages"], model: AgentLoopConfig["model"]): Context["messages"];
54
55
  export declare const INTENT_FIELD = "_i";
55
56
  export declare function normalizeTools(tools: AgentContext["tools"], injectIntent: boolean): Context["tools"];
@@ -49,6 +49,8 @@ export interface AgentOptions {
49
49
  * Used by providers that support session-based caching (e.g., OpenAI code provider).
50
50
  */
51
51
  sessionId?: string;
52
+ /** Provider-facing cache/session affinity identifier. */
53
+ providerSessionId?: string;
52
54
  /**
53
55
  * Shared provider state map for session-scoped transport/session caches.
54
56
  */
@@ -108,6 +110,10 @@ export interface AgentOptions {
108
110
  * Default: 60000 (60 seconds). Set to 0 to disable the cap.
109
111
  */
110
112
  maxRetryDelayMs?: number;
113
+ /** Provider request retry budget. Counts retries, not the initial attempt. */
114
+ requestMaxRetries?: number;
115
+ /** Provider stream replay retry budget. Counts retries, not the initial attempt. */
116
+ streamMaxRetries?: number;
111
117
  /**
112
118
  * Provides tool execution context, resolved per tool call.
113
119
  * Use for late-bound UI or session state access.
@@ -157,6 +163,7 @@ export interface AgentPromptOptions {
157
163
  }
158
164
  export declare class Agent {
159
165
  #private;
166
+ get intentTracing(): boolean;
160
167
  streamFn: StreamFn;
161
168
  getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
162
169
  getAuthCredentialType?: (provider: string) => "api_key" | "oauth" | undefined;
@@ -180,6 +187,8 @@ export declare class Agent {
180
187
  * Call this when switching sessions (new session, branch, resume).
181
188
  */
182
189
  set sessionId(value: string | undefined);
190
+ get providerSessionId(): string | undefined;
191
+ set providerSessionId(value: string | undefined);
183
192
  /**
184
193
  * Static metadata forwarded to every API request when no resolver is installed
185
194
  * (e.g. `metadata.user_id` for Anthropic session attribution). Setting this
@@ -268,6 +277,10 @@ export declare class Agent {
268
277
  * Set to 0 to disable the cap.
269
278
  */
270
279
  set maxRetryDelayMs(value: number | undefined);
280
+ get requestMaxRetries(): number | undefined;
281
+ set requestMaxRetries(value: number | undefined);
282
+ get streamMaxRetries(): number | undefined;
283
+ set streamMaxRetries(value: number | undefined);
271
284
  get state(): AgentState;
272
285
  get appendOnlyContext(): AppendOnlyContextManager | undefined;
273
286
  setAppendOnlyContext(manager?: AppendOnlyContextManager): void;
@@ -39,6 +39,8 @@ export declare class StablePrefix {
39
39
  get fingerprint(): string;
40
40
  get version(): number;
41
41
  get built(): boolean;
42
+ exportSnapshot(): StablePrefixSnapshot | null;
43
+ importSnapshot(snapshot: StablePrefixSnapshot, options: BuildOptions): void;
42
44
  /**
43
45
  * Build or rebuild from live context.
44
46
  * Returns `true` if the prefix actually changed (cache miss imminent).
@@ -94,6 +96,11 @@ export declare class AppendOnlyContextManager {
94
96
  #private;
95
97
  readonly prefix: StablePrefix;
96
98
  readonly log: AppendOnlyLog;
99
+ static forkFromSeed(args: {
100
+ prefixSnapshot?: StablePrefixSnapshot;
101
+ messages?: readonly Message[];
102
+ options: BuildOptions;
103
+ }): AppendOnlyContextManager;
97
104
  build(context: AgentContext, options: BuildOptions): Context;
98
105
  /**
99
106
  * Sync normalized (provider-level) messages into the append-only log.
@@ -102,6 +109,9 @@ export declare class AppendOnlyContextManager {
102
109
  * (same length, changed content via a rolling digest).
103
110
  */
104
111
  syncMessages(normalizedMessages: any[]): void;
112
+ seedNormalizedMessages(messages: readonly Message[], options?: {
113
+ reset?: boolean;
114
+ }): void;
105
115
  /** Reset prefix + log for a model/provider switch while mode stays active. */
106
116
  invalidateForModelChange(): void;
107
117
  /** Reset the sync cursor AND clear the log. */
@@ -21,6 +21,12 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
21
21
  * Used by providers that support session-based caching (e.g., OpenAI code provider).
22
22
  */
23
23
  sessionId?: string;
24
+ /**
25
+ * Optional provider-facing cache/session affinity identifier. When set, this
26
+ * is forwarded to providers as StreamOptions.sessionId while `sessionId`
27
+ * remains the logical agent conversation id for telemetry/metadata.
28
+ */
29
+ providerSessionId?: string;
24
30
  /**
25
31
  * Optional resolver called per LLM request to produce request metadata.
26
32
  * When set, the agent loop evaluates it **after** `getApiKey` resolves the
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/agent-core",
4
- "version": "0.2.2",
4
+ "version": "0.2.4",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://gaebal-gajae.dev",
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.2.2",
39
- "@gajae-code/natives": "0.2.2",
40
- "@gajae-code/utils": "0.2.2",
38
+ "@gajae-code/ai": "0.2.4",
39
+ "@gajae-code/natives": "0.2.4",
40
+ "@gajae-code/utils": "0.2.4",
41
41
  "@opentelemetry/api": "^1.9.0"
42
42
  },
43
43
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -300,7 +300,7 @@ function createDetailedCapture(config: AgentLoopConfig): {
300
300
  };
301
301
  }
302
302
 
303
- function normalizeMessagesForProvider(
303
+ export function normalizeMessagesForProvider(
304
304
  messages: Context["messages"],
305
305
  model: AgentLoopConfig["model"],
306
306
  ): Context["messages"] {
@@ -735,6 +735,7 @@ async function streamAssistantResponse(
735
735
  apiKey: resolvedApiKey,
736
736
  authCredentialType,
737
737
  metadata: resolvedMetadata,
738
+ sessionId: config.providerSessionId ?? config.sessionId,
738
739
  toolChoice: effectiveToolChoice,
739
740
  reasoning: effectiveReasoning,
740
741
  temperature: effectiveTemperature,
@@ -857,7 +858,7 @@ async function streamAssistantResponse(
857
858
  }
858
859
 
859
860
  function emitAbortedAssistantMessage(
860
- _partialMessage: AssistantMessage | null,
861
+ partialMessage: AssistantMessage | null,
861
862
  addedPartial: boolean,
862
863
  context: AgentContext,
863
864
  config: AgentLoopConfig,
@@ -867,7 +868,7 @@ function emitAbortedAssistantMessage(
867
868
  const now = Date.now();
868
869
  const abortedMessage: AssistantMessage = {
869
870
  role: "assistant",
870
- content: [],
871
+ content: partialMessage ? structuredClone(partialMessage.content) : [],
871
872
  api: config.model.api,
872
873
  provider: config.model.provider,
873
874
  model: config.model.id,
package/src/agent.ts CHANGED
@@ -118,6 +118,8 @@ export interface AgentOptions {
118
118
  * Used by providers that support session-based caching (e.g., OpenAI code provider).
119
119
  */
120
120
  sessionId?: string;
121
+ /** Provider-facing cache/session affinity identifier. */
122
+ providerSessionId?: string;
121
123
  /**
122
124
  * Shared provider state map for session-scoped transport/session caches.
123
125
  */
@@ -183,6 +185,10 @@ export interface AgentOptions {
183
185
  * Default: 60000 (60 seconds). Set to 0 to disable the cap.
184
186
  */
185
187
  maxRetryDelayMs?: number;
188
+ /** Provider request retry budget. Counts retries, not the initial attempt. */
189
+ requestMaxRetries?: number;
190
+ /** Provider stream replay retry budget. Counts retries, not the initial attempt. */
191
+ streamMaxRetries?: number;
186
192
 
187
193
  /**
188
194
  * Provides tool execution context, resolved per tool call.
@@ -269,6 +275,7 @@ export class Agent {
269
275
  #followUpMode: "all" | "one-at-a-time";
270
276
  #interruptMode: "immediate" | "wait";
271
277
  #sessionId?: string;
278
+ #providerSessionId?: string;
272
279
  #metadata?: Record<string, unknown>;
273
280
  #metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
274
281
  #providerSessionState?: Map<string, ProviderSessionState>;
@@ -282,6 +289,8 @@ export class Agent {
282
289
  #serviceTier?: ServiceTier;
283
290
  #hideThinkingSummary?: boolean;
284
291
  #maxRetryDelayMs?: number;
292
+ #requestMaxRetries?: number;
293
+ #streamMaxRetries?: number;
285
294
  #getToolContext?: (toolCall?: ToolCallContext) => AgentToolContext | undefined;
286
295
  #cursorExecHandlers?: CursorExecHandlers;
287
296
  #cursorOnToolResult?: CursorToolResultHandler;
@@ -303,6 +312,10 @@ export class Agent {
303
312
  #telemetry?: AgentLoopConfig["telemetry"];
304
313
  #appendOnlyContext?: AppendOnlyContextManager;
305
314
 
315
+ get intentTracing(): boolean {
316
+ return this.#intentTracing;
317
+ }
318
+
306
319
  /** Buffered Cursor tool results with text length at time of call (for correct ordering) */
307
320
  #cursorToolResultBuffer: CursorToolResultEntry[] = [];
308
321
 
@@ -329,6 +342,7 @@ export class Agent {
329
342
  this.#interruptMode = opts.interruptMode || "immediate";
330
343
  this.streamFn = opts.streamFn || streamSimple;
331
344
  this.#sessionId = opts.sessionId;
345
+ this.#providerSessionId = opts.providerSessionId;
332
346
  this.#providerSessionState = opts.providerSessionState;
333
347
  this.#thinkingBudgets = opts.thinkingBudgets;
334
348
  this.#temperature = opts.temperature;
@@ -340,6 +354,8 @@ export class Agent {
340
354
  this.#serviceTier = opts.serviceTier;
341
355
  this.#hideThinkingSummary = opts.hideThinkingSummary;
342
356
  this.#maxRetryDelayMs = opts.maxRetryDelayMs;
357
+ this.#requestMaxRetries = opts.requestMaxRetries;
358
+ this.#streamMaxRetries = opts.streamMaxRetries;
343
359
  this.getApiKey = opts.getApiKey;
344
360
  this.getAuthCredentialType = opts.getAuthCredentialType;
345
361
  this.#onPayload = opts.onPayload;
@@ -376,6 +392,14 @@ export class Agent {
376
392
  this.#sessionId = value;
377
393
  }
378
394
 
395
+ get providerSessionId(): string | undefined {
396
+ return this.#providerSessionId;
397
+ }
398
+
399
+ set providerSessionId(value: string | undefined) {
400
+ this.#providerSessionId = value;
401
+ }
402
+
379
403
  /**
380
404
  * Static metadata forwarded to every API request when no resolver is installed
381
405
  * (e.g. `metadata.user_id` for Anthropic session attribution). Setting this
@@ -550,6 +574,22 @@ export class Agent {
550
574
  this.#maxRetryDelayMs = value;
551
575
  }
552
576
 
577
+ get requestMaxRetries(): number | undefined {
578
+ return this.#requestMaxRetries;
579
+ }
580
+
581
+ set requestMaxRetries(value: number | undefined) {
582
+ this.#requestMaxRetries = value;
583
+ }
584
+
585
+ get streamMaxRetries(): number | undefined {
586
+ return this.#streamMaxRetries;
587
+ }
588
+
589
+ set streamMaxRetries(value: number | undefined) {
590
+ this.#streamMaxRetries = value;
591
+ }
592
+
553
593
  get state(): AgentState {
554
594
  return this.#state;
555
595
  }
@@ -1070,11 +1110,14 @@ export class Agent {
1070
1110
  hideThinkingSummary: this.#hideThinkingSummary,
1071
1111
  interruptMode: this.#interruptMode,
1072
1112
  sessionId: this.#sessionId,
1113
+ providerSessionId: this.#providerSessionId,
1073
1114
  metadata: this.#metadataResolver ? undefined : this.#metadata,
1074
1115
  metadataResolver: this.#metadataResolver,
1075
1116
  providerSessionState: this.#providerSessionState,
1076
1117
  thinkingBudgets: this.#thinkingBudgets,
1077
1118
  maxRetryDelayMs: this.#maxRetryDelayMs,
1119
+ requestMaxRetries: this.#requestMaxRetries,
1120
+ streamMaxRetries: this.#streamMaxRetries,
1078
1121
  kimiApiFormat: this.#kimiApiFormat,
1079
1122
  preferWebsockets: this.#preferWebsockets,
1080
1123
  convertToLlm: this.#convertToLlm,
@@ -57,6 +57,23 @@ export class StablePrefix {
57
57
  return this.#snapshot !== null;
58
58
  }
59
59
 
60
+ exportSnapshot(): StablePrefixSnapshot | null {
61
+ return this.#snapshot ? cloneJson(this.#snapshot) : null;
62
+ }
63
+
64
+ importSnapshot(snapshot: StablePrefixSnapshot, options: BuildOptions): void {
65
+ const systemPrompt = cloneJson(snapshot.systemPrompt);
66
+ const tools = normalizeImportedTools(snapshot.tools, options);
67
+ const fingerprint = computeFingerprint(systemPrompt, tools, options);
68
+ if (fingerprint !== snapshot.fingerprint) {
69
+ throw new Error(
70
+ `StablePrefix.importSnapshot() fingerprint mismatch: expected ${fingerprint}, received ${snapshot.fingerprint}`,
71
+ );
72
+ }
73
+ this.#snapshot = { systemPrompt, tools, fingerprint };
74
+ this.#version++;
75
+ }
76
+
60
77
  /**
61
78
  * Build or rebuild from live context.
62
79
  * Returns `true` if the prefix actually changed (cache miss imminent).
@@ -83,7 +100,7 @@ export class StablePrefix {
83
100
  toContext(): { systemPrompt: string[]; tools: Tool[] } {
84
101
  const s = this.#snapshot;
85
102
  if (!s) throw new Error("StablePrefix.toContext() called before build()");
86
- return { systemPrompt: s.systemPrompt, tools: s.tools };
103
+ return { systemPrompt: cloneJson(s.systemPrompt), tools: cloneJson(s.tools) };
87
104
  }
88
105
  }
89
106
 
@@ -160,6 +177,23 @@ export class AppendOnlyContextManager {
160
177
  #lastSyncCount = 0;
161
178
  /** Rolling digest of synced message content — detects in-place rewrites. */
162
179
  #syncedDigest = 0;
180
+ /** Number of provider-normalized messages that were seeded before child-local messages. */
181
+ #seededPrefixCount = 0;
182
+
183
+ static forkFromSeed(args: {
184
+ prefixSnapshot?: StablePrefixSnapshot;
185
+ messages?: readonly Message[];
186
+ options: BuildOptions;
187
+ }): AppendOnlyContextManager {
188
+ const manager = new AppendOnlyContextManager();
189
+ if (args.prefixSnapshot) {
190
+ manager.prefix.importSnapshot(args.prefixSnapshot, args.options);
191
+ }
192
+ if (args.messages) {
193
+ manager.seedNormalizedMessages(args.messages);
194
+ }
195
+ return manager;
196
+ }
163
197
 
164
198
  build(context: AgentContext, options: BuildOptions): Context {
165
199
  this.prefix.build(context, options);
@@ -174,29 +208,57 @@ export class AppendOnlyContextManager {
174
208
  * (same length, changed content via a rolling digest).
175
209
  */
176
210
  syncMessages(normalizedMessages: any[]): void {
211
+ const seededPrefix = this.#seededPrefixCount > 0 ? this.log.toMessages().slice(0, this.#seededPrefixCount) : [];
212
+ const includesSeedPrefix =
213
+ seededPrefix.length > 0 &&
214
+ normalizedMessages.length >= seededPrefix.length &&
215
+ this.#computeDigest(normalizedMessages.slice(0, seededPrefix.length)) === this.#computeDigest(seededPrefix);
216
+ const messagesToSync =
217
+ seededPrefix.length > 0 && !includesSeedPrefix ? [...seededPrefix, ...normalizedMessages] : normalizedMessages;
218
+
177
219
  // Detect in-place rewrites of already-synced messages.
178
220
  if (
179
221
  this.#lastSyncCount > 0 &&
180
- this.#lastSyncCount <= normalizedMessages.length &&
181
- this.#computeDigest(normalizedMessages.slice(0, this.#lastSyncCount)) !== this.#syncedDigest
222
+ this.#lastSyncCount <= messagesToSync.length &&
223
+ this.#computeDigest(messagesToSync.slice(0, this.#lastSyncCount)) !== this.#syncedDigest
182
224
  ) {
225
+ if (this.#seededPrefixCount > 0) {
226
+ throw new Error("AppendOnlyContextManager.syncMessages() seed prefix changed");
227
+ }
183
228
  this.log.clear();
184
229
  this.#lastSyncCount = 0;
185
230
  }
186
231
 
187
- // Compaction — array shrunk.
188
- if (normalizedMessages.length < this.#lastSyncCount) {
232
+ // Compaction — array shrunk. Seeded forks preserve the inherited prefix
233
+ // and append child-local deltas, so a shorter child message array is not a
234
+ // compaction signal while a seed prefix is active.
235
+ if (messagesToSync.length < this.#lastSyncCount) {
236
+ if (this.#seededPrefixCount > 0) {
237
+ throw new Error("AppendOnlyContextManager.syncMessages() cannot compact a seeded fork without reset");
238
+ }
189
239
  this.log.clear();
190
240
  this.#lastSyncCount = 0;
191
241
  }
192
242
 
193
- const newMsgs = normalizedMessages.slice(this.#lastSyncCount);
243
+ const newMsgs = messagesToSync.slice(this.#lastSyncCount);
194
244
  for (const msg of newMsgs) {
195
245
  this.log.append(msg);
196
246
  }
197
247
 
198
- this.#lastSyncCount = normalizedMessages.length;
199
- this.#syncedDigest = this.#computeDigest(normalizedMessages);
248
+ this.#lastSyncCount = messagesToSync.length;
249
+ this.#syncedDigest = this.#computeDigest(messagesToSync);
250
+ }
251
+
252
+ seedNormalizedMessages(messages: readonly Message[], options?: { reset?: boolean }): void {
253
+ if (this.log.length > 0 && options?.reset !== true) {
254
+ throw new Error("AppendOnlyContextManager.seedNormalizedMessages() cannot seed a non-empty log without reset");
255
+ }
256
+ const clonedMessages = cloneJson([...messages]);
257
+ this.log.clear();
258
+ this.log.extend(clonedMessages);
259
+ this.#lastSyncCount = clonedMessages.length;
260
+ this.#syncedDigest = this.#computeDigest(clonedMessages);
261
+ this.#seededPrefixCount = clonedMessages.length;
200
262
  }
201
263
 
202
264
  /** Reset prefix + log for a model/provider switch while mode stays active. */
@@ -205,6 +267,7 @@ export class AppendOnlyContextManager {
205
267
  this.log.clear();
206
268
  this.#lastSyncCount = 0;
207
269
  this.#syncedDigest = 0;
270
+ this.#seededPrefixCount = 0;
208
271
  }
209
272
 
210
273
  /** Reset the sync cursor AND clear the log. */
@@ -212,6 +275,7 @@ export class AppendOnlyContextManager {
212
275
  this.log.clear();
213
276
  this.#lastSyncCount = 0;
214
277
  this.#syncedDigest = 0;
278
+ this.#seededPrefixCount = 0;
215
279
  }
216
280
 
217
281
  appendMessage(message: any): void {
@@ -231,6 +295,7 @@ export class AppendOnlyContextManager {
231
295
  this.log.clear();
232
296
  this.#lastSyncCount = 0;
233
297
  this.#syncedDigest = 0;
298
+ this.#seededPrefixCount = 0;
234
299
  this.prefix.build(context, options);
235
300
  }
236
301
 
@@ -276,6 +341,16 @@ function takeSnapshot(context: AgentContext, options: BuildOptions): StablePrefi
276
341
  };
277
342
  }
278
343
 
344
+ function normalizeImportedTools(tools: readonly Tool[], options: BuildOptions): Tool[] {
345
+ const clonedTools = cloneJson(tools);
346
+ const normalizedTools = normalizeTools(clonedTools as AgentContext["tools"], options.intentTracing) ?? [];
347
+ return cloneJson(normalizedTools);
348
+ }
349
+
350
+ function cloneJson<T>(value: T): T {
351
+ return JSON.parse(JSON.stringify(value)) as T;
352
+ }
353
+
279
354
  function computeFingerprint(systemPrompt: string[], tools: Tool[], options: BuildOptions): string {
280
355
  const payload = JSON.stringify({
281
356
  s: systemPrompt,
package/src/types.ts CHANGED
@@ -43,6 +43,12 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
43
43
  * Used by providers that support session-based caching (e.g., OpenAI code provider).
44
44
  */
45
45
  sessionId?: string;
46
+ /**
47
+ * Optional provider-facing cache/session affinity identifier. When set, this
48
+ * is forwarded to providers as StreamOptions.sessionId while `sessionId`
49
+ * remains the logical agent conversation id for telemetry/metadata.
50
+ */
51
+ providerSessionId?: string;
46
52
 
47
53
  /**
48
54
  * Optional resolver called per LLM request to produce request metadata.