@gajae-code/agent-core 0.5.3 → 0.5.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.5.4] - 2026-06-17
6
+
7
+ ### Fixed
8
+
9
+ - Maintenance one-shot LLM calls now preserve active provider session state and the configured WebSocket transport preference. `SummaryOptions`, `HandoffOptions`, and `GenerateBranchSummaryOptions` accept `sessionId`, `providerSessionState`, and `preferWebsockets`, and `generateSummary`, `generateShortSummary`, `generateTurnPrefixSummary`, `generateHandoff`, `generateBranchSummary`, and `compact()` forward them through to `completeSimple` — previously these fields were dropped, so Codex/OpenAI-compatible compaction summaries, handoff generation, and branch summaries fell back to HTTP/SSE and lost `session_id` affinity even with `providers.openaiWebsockets: "on"`. Split-turn compaction now runs its history and turn-prefix summaries sequentially when they share a single provider WebSocket session, avoiding `websocket request already in progress`; non-WebSocket sessions still run them in parallel. `Agent` exposes a `preferWebsockets` getter so callers can forward the live transport preference (#736).
10
+
5
11
  ## [0.5.3] - 2026-06-16
6
12
 
7
13
  ### Fixed
@@ -193,6 +193,12 @@ export declare class Agent {
193
193
  set sessionId(value: string | undefined);
194
194
  get providerSessionId(): string | undefined;
195
195
  set providerSessionId(value: string | undefined);
196
+ /**
197
+ * Whether websocket transport is preferred when the provider implementation
198
+ * supports it. Read by maintenance one-shot calls (compaction, handoff,
199
+ * branch summary) so they forward the same transport preference as live turns.
200
+ */
201
+ get preferWebsockets(): boolean | undefined;
196
202
  /**
197
203
  * Static metadata forwarded to every API request when no resolver is installed
198
204
  * (e.g. `metadata.user_id` for Anthropic session attribution). Setting this
@@ -4,7 +4,7 @@
4
4
  * When navigating to a different point in the session tree, this generates
5
5
  * a summary of the branch being left so context isn't lost.
6
6
  */
7
- import type { Model } from "@gajae-code/ai";
7
+ import type { Model, ProviderSessionState } from "@gajae-code/ai";
8
8
  import { type AgentTelemetry } from "../telemetry";
9
9
  import type { AgentMessage } from "../types";
10
10
  import type { ReadonlySessionManager, SessionEntry } from "./entries";
@@ -57,6 +57,15 @@ export interface GenerateBranchSummaryOptions {
57
57
  * wrapped in an OTEL chat span tagged with `pi.gen_ai.oneshot.kind = "branch_summary"`.
58
58
  */
59
59
  telemetry?: AgentTelemetry;
60
+ /**
61
+ * Provider session affinity id forwarded to the branch summary LLM call so it
62
+ * reuses the live turn's provider/WebSocket session.
63
+ */
64
+ sessionId?: string;
65
+ /** Shared provider state map so the branch summary call reuses session-scoped transport/session caches. */
66
+ providerSessionState?: Map<string, ProviderSessionState>;
67
+ /** Hint that websocket transport should be preferred when supported by the provider implementation. */
68
+ preferWebsockets?: boolean;
60
69
  }
61
70
  /**
62
71
  * Collect entries that should be summarized when navigating from one position to another.
@@ -4,7 +4,7 @@
4
4
  * Pure functions for compaction logic. The session manager handles I/O,
5
5
  * and after compaction the session is reloaded.
6
6
  */
7
- import { type MessageAttribution, type Model, type Usage } from "@gajae-code/ai";
7
+ import { type MessageAttribution, type Model, type ProviderSessionState, type Usage } from "@gajae-code/ai";
8
8
  import { type AgentTelemetry } from "../telemetry";
9
9
  import type { AgentMessage, AgentTool } from "../types";
10
10
  import type { SessionEntry } from "./entries";
@@ -182,6 +182,16 @@ export interface SummaryOptions {
182
182
  */
183
183
  telemetry?: AgentTelemetry;
184
184
  authCredentialType?: "api_key" | "oauth";
185
+ /**
186
+ * Provider session affinity id forwarded to the maintenance LLM call so it
187
+ * reuses the live turn's provider/WebSocket session (matches the
188
+ * `providerSessionId ?? sessionId` the agent loop sends for normal turns).
189
+ */
190
+ sessionId?: string;
191
+ /** Shared provider state map so maintenance calls reuse session-scoped transport/session caches. */
192
+ providerSessionState?: Map<string, ProviderSessionState>;
193
+ /** Hint that websocket transport should be preferred when supported by the provider implementation. */
194
+ preferWebsockets?: boolean;
185
195
  }
186
196
  export declare function generateSummary(currentMessages: AgentMessage[], model: Model, reserveTokens: number, apiKey: string, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, options?: SummaryOptions): Promise<string>;
187
197
  export interface HandoffOptions {
@@ -199,6 +209,15 @@ export interface HandoffOptions {
199
209
  */
200
210
  telemetry?: AgentTelemetry;
201
211
  authCredentialType?: "api_key" | "oauth";
212
+ /**
213
+ * Provider session affinity id forwarded to the handoff LLM call so it
214
+ * reuses the live turn's provider/WebSocket session.
215
+ */
216
+ sessionId?: string;
217
+ /** Shared provider state map so the handoff call reuses session-scoped transport/session caches. */
218
+ providerSessionState?: Map<string, ProviderSessionState>;
219
+ /** Hint that websocket transport should be preferred when supported by the provider implementation. */
220
+ preferWebsockets?: boolean;
202
221
  }
203
222
  export declare function renderHandoffPrompt(customInstructions?: string): string;
204
223
  export declare function generateHandoff(messages: AgentMessage[], model: Model, apiKey: string, options: HandoffOptions, signal?: AbortSignal): Promise<string>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/agent-core",
4
- "version": "0.5.3",
4
+ "version": "0.5.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.5.3",
39
- "@gajae-code/natives": "0.5.3",
40
- "@gajae-code/utils": "0.5.3",
38
+ "@gajae-code/ai": "0.5.4",
39
+ "@gajae-code/natives": "0.5.4",
40
+ "@gajae-code/utils": "0.5.4",
41
41
  "@opentelemetry/api": "^1.9.0"
42
42
  },
43
43
  "devDependencies": {
package/src/agent.ts CHANGED
@@ -408,6 +408,15 @@ export class Agent {
408
408
  this.#providerSessionId = value;
409
409
  }
410
410
 
411
+ /**
412
+ * Whether websocket transport is preferred when the provider implementation
413
+ * supports it. Read by maintenance one-shot calls (compaction, handoff,
414
+ * branch summary) so they forward the same transport preference as live turns.
415
+ */
416
+ get preferWebsockets(): boolean | undefined {
417
+ return this.#preferWebsockets;
418
+ }
419
+
411
420
  /**
412
421
  * Static metadata forwarded to every API request when no resolver is installed
413
422
  * (e.g. `metadata.user_id` for Anthropic session attribution). Setting this
@@ -5,7 +5,7 @@
5
5
  * a summary of the branch being left so context isn't lost.
6
6
  */
7
7
 
8
- import type { Model } from "@gajae-code/ai";
8
+ import type { Model, ProviderSessionState } from "@gajae-code/ai";
9
9
  import { prompt } from "@gajae-code/utils";
10
10
  import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
11
11
  import type { AgentMessage } from "../types";
@@ -86,6 +86,15 @@ export interface GenerateBranchSummaryOptions {
86
86
  * wrapped in an OTEL chat span tagged with `pi.gen_ai.oneshot.kind = "branch_summary"`.
87
87
  */
88
88
  telemetry?: AgentTelemetry;
89
+ /**
90
+ * Provider session affinity id forwarded to the branch summary LLM call so it
91
+ * reuses the live turn's provider/WebSocket session.
92
+ */
93
+ sessionId?: string;
94
+ /** Shared provider state map so the branch summary call reuses session-scoped transport/session caches. */
95
+ providerSessionState?: Map<string, ProviderSessionState>;
96
+ /** Hint that websocket transport should be preferred when supported by the provider implementation. */
97
+ preferWebsockets?: boolean;
89
98
  }
90
99
 
91
100
  // ============================================================================
@@ -274,7 +283,17 @@ export async function generateBranchSummary(
274
283
  entries: SessionEntry[],
275
284
  options: GenerateBranchSummaryOptions,
276
285
  ): Promise<BranchSummaryResult> {
277
- const { model, apiKey, signal, customInstructions, reserveTokens = 16384, metadata } = options;
286
+ const {
287
+ model,
288
+ apiKey,
289
+ signal,
290
+ customInstructions,
291
+ reserveTokens = 16384,
292
+ metadata,
293
+ sessionId,
294
+ providerSessionState,
295
+ preferWebsockets,
296
+ } = options;
278
297
 
279
298
  // Token budget = context window minus reserved space for prompt + response
280
299
  const contextWindow = model.contextWindow || 128000;
@@ -307,7 +326,7 @@ export async function generateBranchSummary(
307
326
  const response = await instrumentedCompleteSimple(
308
327
  model,
309
328
  { systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
310
- { apiKey, signal, maxTokens: 2048, metadata },
329
+ { apiKey, signal, maxTokens: 2048, metadata, sessionId, providerSessionState, preferWebsockets },
311
330
  { telemetry: options.telemetry, oneshotKind: "branch_summary" },
312
331
  );
313
332
 
@@ -12,6 +12,7 @@ import {
12
12
  type Message,
13
13
  type MessageAttribution,
14
14
  type Model,
15
+ type ProviderSessionState,
15
16
  type Usage,
16
17
  } from "@gajae-code/ai";
17
18
  import { isCompiledBinary, logger, prompt } from "@gajae-code/utils";
@@ -735,6 +736,16 @@ export interface SummaryOptions {
735
736
  */
736
737
  telemetry?: AgentTelemetry;
737
738
  authCredentialType?: "api_key" | "oauth";
739
+ /**
740
+ * Provider session affinity id forwarded to the maintenance LLM call so it
741
+ * reuses the live turn's provider/WebSocket session (matches the
742
+ * `providerSessionId ?? sessionId` the agent loop sends for normal turns).
743
+ */
744
+ sessionId?: string;
745
+ /** Shared provider state map so maintenance calls reuse session-scoped transport/session caches. */
746
+ providerSessionState?: Map<string, ProviderSessionState>;
747
+ /** Hint that websocket transport should be preferred when supported by the provider implementation. */
748
+ preferWebsockets?: boolean;
738
749
  }
739
750
 
740
751
  export async function generateSummary(
@@ -801,6 +812,9 @@ export async function generateSummary(
801
812
  reasoning: Effort.High,
802
813
  initiatorOverride: options?.initiatorOverride,
803
814
  metadata: options?.metadata,
815
+ sessionId: options?.sessionId,
816
+ providerSessionState: options?.providerSessionState,
817
+ preferWebsockets: options?.preferWebsockets,
804
818
  },
805
819
  { telemetry: options?.telemetry, oneshotKind: "compaction_summary" },
806
820
  );
@@ -836,6 +850,15 @@ export interface HandoffOptions {
836
850
  */
837
851
  telemetry?: AgentTelemetry;
838
852
  authCredentialType?: "api_key" | "oauth";
853
+ /**
854
+ * Provider session affinity id forwarded to the handoff LLM call so it
855
+ * reuses the live turn's provider/WebSocket session.
856
+ */
857
+ sessionId?: string;
858
+ /** Shared provider state map so the handoff call reuses session-scoped transport/session caches. */
859
+ providerSessionState?: Map<string, ProviderSessionState>;
860
+ /** Hint that websocket transport should be preferred when supported by the provider implementation. */
861
+ preferWebsockets?: boolean;
839
862
  }
840
863
 
841
864
  export function renderHandoffPrompt(customInstructions?: string): string {
@@ -877,6 +900,9 @@ export async function generateHandoff(
877
900
  toolChoice: "none",
878
901
  initiatorOverride: options.initiatorOverride,
879
902
  metadata: options.metadata,
903
+ sessionId: options.sessionId,
904
+ providerSessionState: options.providerSessionState,
905
+ preferWebsockets: options.preferWebsockets,
880
906
  },
881
907
  { telemetry: options.telemetry, oneshotKind: "handoff" },
882
908
  );
@@ -936,6 +962,9 @@ async function generateShortSummary(
936
962
  reasoning: Effort.High,
937
963
  initiatorOverride: options?.initiatorOverride,
938
964
  metadata: options?.metadata,
965
+ sessionId: options?.sessionId,
966
+ providerSessionState: options?.providerSessionState,
967
+ preferWebsockets: options?.preferWebsockets,
939
968
  },
940
969
  { telemetry: options?.telemetry, oneshotKind: "compaction_short_summary" },
941
970
  );
@@ -1120,6 +1149,9 @@ export async function compact(
1120
1149
  metadata: options?.metadata,
1121
1150
  convertToLlm: options?.convertToLlm,
1122
1151
  telemetry: options?.telemetry,
1152
+ sessionId: options?.sessionId,
1153
+ providerSessionState: options?.providerSessionState,
1154
+ preferWebsockets: options?.preferWebsockets,
1123
1155
  };
1124
1156
 
1125
1157
  let preserveData = withOpenAiRemoteCompactionPreserveData(previousPreserveData, undefined);
@@ -1159,9 +1191,22 @@ export async function compact(
1159
1191
  // Generate summaries (can be parallel if both needed) and merge into one
1160
1192
  let summary: string;
1161
1193
 
1194
+ // A single active Codex WebSocket session cannot service two concurrent
1195
+ // requests ("websocket request already in progress"). When the maintenance
1196
+ // calls use the Codex Responses provider, share one provider session, and
1197
+ // websocket transport is not explicitly disabled, run the split-turn history
1198
+ // and turn-prefix summaries sequentially. This covers websocket activation
1199
+ // from config/env/model defaults too: the provider can select websockets even
1200
+ // when `preferWebsockets` is undefined, while non-Codex providers keep the
1201
+ // previous parallel behavior.
1202
+ const summariesMayShareWebSocketSession = Boolean(
1203
+ model.api === "openai-codex-responses" &&
1204
+ summaryOptions.providerSessionState &&
1205
+ summaryOptions.preferWebsockets !== false,
1206
+ );
1207
+
1162
1208
  if (isSplitTurn && turnPrefixMessages.length > 0) {
1163
- // Generate both summaries in parallel
1164
- const [historyResult, turnPrefixResult] = await Promise.all([
1209
+ const runHistorySummary = () =>
1165
1210
  messagesToSummarize.length > 0
1166
1211
  ? generateSummary(
1167
1212
  messagesToSummarize,
@@ -1173,9 +1218,19 @@ export async function compact(
1173
1218
  previousSummary,
1174
1219
  summaryOptions,
1175
1220
  )
1176
- : Promise.resolve("No prior history."),
1177
- generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, signal, summaryOptions),
1178
- ]);
1221
+ : Promise.resolve("No prior history.");
1222
+ const runTurnPrefixSummary = () =>
1223
+ generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, signal, summaryOptions);
1224
+
1225
+ let historyResult: string;
1226
+ let turnPrefixResult: string;
1227
+ if (summariesMayShareWebSocketSession) {
1228
+ // Sequential: avoids concurrent requests on the same provider session.
1229
+ historyResult = await runHistorySummary();
1230
+ turnPrefixResult = await runTurnPrefixSummary();
1231
+ } else {
1232
+ [historyResult, turnPrefixResult] = await Promise.all([runHistorySummary(), runTurnPrefixSummary()]);
1233
+ }
1179
1234
  // Merge into single summary
1180
1235
  summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
1181
1236
  } else if (messagesToSummarize.length > 0) {
@@ -1211,6 +1266,9 @@ export async function compact(
1211
1266
  initiatorOverride: summaryOptions.initiatorOverride,
1212
1267
  metadata: summaryOptions.metadata,
1213
1268
  telemetry: summaryOptions.telemetry,
1269
+ sessionId: summaryOptions.sessionId,
1270
+ providerSessionState: summaryOptions.providerSessionState,
1271
+ preferWebsockets: summaryOptions.preferWebsockets,
1214
1272
  },
1215
1273
  );
1216
1274
 
@@ -1266,6 +1324,9 @@ async function generateTurnPrefixSummary(
1266
1324
  reasoning: Effort.High,
1267
1325
  initiatorOverride: options?.initiatorOverride,
1268
1326
  metadata: options?.metadata,
1327
+ sessionId: options?.sessionId,
1328
+ providerSessionState: options?.providerSessionState,
1329
+ preferWebsockets: options?.preferWebsockets,
1269
1330
  },
1270
1331
  { telemetry: options?.telemetry, oneshotKind: "compaction_turn_prefix" },
1271
1332
  );