@gajae-code/agent-core 0.2.1 → 0.2.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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.2] - 2026-05-31
6
+
7
+ ### Changed
8
+
9
+ - Refreshed agent-core package metadata for the GJC 0.2.2 release.
10
+
5
11
  ## [0.2.1] - 2026-05-30
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
  */
@@ -157,6 +159,7 @@ export interface AgentPromptOptions {
157
159
  }
158
160
  export declare class Agent {
159
161
  #private;
162
+ get intentTracing(): boolean;
160
163
  streamFn: StreamFn;
161
164
  getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
162
165
  getAuthCredentialType?: (provider: string) => "api_key" | "oauth" | undefined;
@@ -180,6 +183,8 @@ export declare class Agent {
180
183
  * Call this when switching sessions (new session, branch, resume).
181
184
  */
182
185
  set sessionId(value: string | undefined);
186
+ get providerSessionId(): string | undefined;
187
+ set providerSessionId(value: string | undefined);
183
188
  /**
184
189
  * Static metadata forwarded to every API request when no resolver is installed
185
190
  * (e.g. `metadata.user_id` for Anthropic session attribution). Setting this
@@ -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.1",
4
+ "version": "0.2.3",
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.1",
39
- "@gajae-code/natives": "0.2.1",
40
- "@gajae-code/utils": "0.2.1",
38
+ "@gajae-code/ai": "0.2.3",
39
+ "@gajae-code/natives": "0.2.3",
40
+ "@gajae-code/utils": "0.2.3",
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,
@@ -864,30 +865,28 @@ function emitAbortedAssistantMessage(
864
865
  stream: EventStream<AgentEvent, AgentMessage[]>,
865
866
  ): AssistantMessage {
866
867
  const errorMessage = "Request was aborted";
867
- const abortedMessage: AssistantMessage = partialMessage
868
- ? { ...partialMessage, stopReason: "aborted", errorMessage }
869
- : {
870
- role: "assistant",
871
- content: [],
872
- api: config.model.api,
873
- provider: config.model.provider,
874
- model: config.model.id,
875
- usage: {
876
- input: 0,
877
- output: 0,
878
- cacheRead: 0,
879
- cacheWrite: 0,
880
- totalTokens: 0,
881
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
882
- },
883
- stopReason: "aborted",
884
- errorMessage,
885
- timestamp: Date.now(),
886
- };
868
+ const now = Date.now();
869
+ const abortedMessage: AssistantMessage = {
870
+ role: "assistant",
871
+ content: partialMessage ? structuredClone(partialMessage.content) : [],
872
+ api: config.model.api,
873
+ provider: config.model.provider,
874
+ model: config.model.id,
875
+ usage: {
876
+ input: 0,
877
+ output: 0,
878
+ cacheRead: 0,
879
+ cacheWrite: 0,
880
+ totalTokens: 0,
881
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
882
+ },
883
+ stopReason: "aborted",
884
+ errorMessage,
885
+ timestamp: now,
886
+ };
887
887
  if (addedPartial) {
888
- context.messages[context.messages.length - 1] = abortedMessage;
888
+ context.messages.pop();
889
889
  } else {
890
- context.messages.push(abortedMessage);
891
890
  stream.push({ type: "message_start", message: { ...abortedMessage } });
892
891
  }
893
892
  stream.push({ type: "message_end", message: abortedMessage });
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
  */
@@ -269,6 +271,7 @@ export class Agent {
269
271
  #followUpMode: "all" | "one-at-a-time";
270
272
  #interruptMode: "immediate" | "wait";
271
273
  #sessionId?: string;
274
+ #providerSessionId?: string;
272
275
  #metadata?: Record<string, unknown>;
273
276
  #metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
274
277
  #providerSessionState?: Map<string, ProviderSessionState>;
@@ -303,6 +306,10 @@ export class Agent {
303
306
  #telemetry?: AgentLoopConfig["telemetry"];
304
307
  #appendOnlyContext?: AppendOnlyContextManager;
305
308
 
309
+ get intentTracing(): boolean {
310
+ return this.#intentTracing;
311
+ }
312
+
306
313
  /** Buffered Cursor tool results with text length at time of call (for correct ordering) */
307
314
  #cursorToolResultBuffer: CursorToolResultEntry[] = [];
308
315
 
@@ -329,6 +336,7 @@ export class Agent {
329
336
  this.#interruptMode = opts.interruptMode || "immediate";
330
337
  this.streamFn = opts.streamFn || streamSimple;
331
338
  this.#sessionId = opts.sessionId;
339
+ this.#providerSessionId = opts.providerSessionId;
332
340
  this.#providerSessionState = opts.providerSessionState;
333
341
  this.#thinkingBudgets = opts.thinkingBudgets;
334
342
  this.#temperature = opts.temperature;
@@ -376,6 +384,14 @@ export class Agent {
376
384
  this.#sessionId = value;
377
385
  }
378
386
 
387
+ get providerSessionId(): string | undefined {
388
+ return this.#providerSessionId;
389
+ }
390
+
391
+ set providerSessionId(value: string | undefined) {
392
+ this.#providerSessionId = value;
393
+ }
394
+
379
395
  /**
380
396
  * Static metadata forwarded to every API request when no resolver is installed
381
397
  * (e.g. `metadata.user_id` for Anthropic session attribution). Setting this
@@ -1070,6 +1086,7 @@ export class Agent {
1070
1086
  hideThinkingSummary: this.#hideThinkingSummary,
1071
1087
  interruptMode: this.#interruptMode,
1072
1088
  sessionId: this.#sessionId,
1089
+ providerSessionId: this.#providerSessionId,
1073
1090
  metadata: this.#metadataResolver ? undefined : this.#metadata,
1074
1091
  metadataResolver: this.#metadataResolver,
1075
1092
  providerSessionState: this.#providerSessionState,
@@ -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.