@gajae-code/agent-core 0.16.1 → 0.16.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,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.16.4] - 2026-09-05
6
+
7
+ - Provider calls now carry the agent-owned opaque provider conversation identity separately from generic session/cache affinity, including compaction, handoff, turn-prefix summary, and branch-summary maintenance calls. This lets provider-specific conversation headers remain stable without treating prompt-derived gateway cache keys as conversation authority (#5295).
8
+
9
+ - Failed cooperative context maintenance now terminalizes the active run with its concrete maintenance error instead of advertising a continuation over unchanged context. Only committed prune, compaction, or promotion outcomes remain resumable.
10
+
11
+ ## [0.16.3] - 2026-09-04
12
+
13
+ ## [0.16.2] - 2026-09-04
14
+
5
15
  ## [0.16.1] - 2026-09-03
6
16
 
7
17
  ### Changed
@@ -62,6 +62,8 @@ export interface GenerateBranchSummaryOptions {
62
62
  * reuses the live turn's provider/WebSocket session.
63
63
  */
64
64
  sessionId?: string;
65
+ /** Opaque provider conversation identity; never derived from branch-summary content. */
66
+ providerSessionId?: string;
65
67
  /** Shared provider state map so the branch summary call reuses session-scoped transport/session caches. */
66
68
  providerSessionState?: Map<string, ProviderSessionState>;
67
69
  /** Hint that websocket transport should be preferred when supported by the provider implementation. */
@@ -209,6 +209,8 @@ export interface SummaryOptions {
209
209
  * `providerSessionId ?? sessionId` the agent loop sends for normal turns).
210
210
  */
211
211
  sessionId?: string;
212
+ /** Opaque provider conversation identity; never derived from compaction content. */
213
+ providerSessionId?: string;
212
214
  /** Shared provider state map so maintenance calls reuse session-scoped transport/session caches. */
213
215
  providerSessionState?: Map<string, ProviderSessionState>;
214
216
  /** Hint that websocket transport should be preferred when supported by the provider implementation. */
@@ -262,6 +264,8 @@ export interface HandoffOptions {
262
264
  * reuses the live turn's provider/WebSocket session.
263
265
  */
264
266
  sessionId?: string;
267
+ /** Opaque provider conversation identity; never derived from handoff content. */
268
+ providerSessionId?: string;
265
269
  /** Shared provider state map so the handoff call reuses session-scoped transport/session caches. */
266
270
  providerSessionState?: Map<string, ProviderSessionState>;
267
271
  /** Hint that websocket transport should be preferred when supported by the provider implementation. */
@@ -195,14 +195,16 @@ export type ManagedAttemptOutcomeHandler = (outcome: ManagedAttemptOutcome) => M
195
195
  * Outcome of a cooperative mid-run context-maintenance checkpoint (see
196
196
  * {@link AgentLoopConfig.maintainContext}). Any value other than "not-needed"
197
197
  * means the checkpoint mutated (or attempted to mutate) durable context, so the
198
- * loop ends the current run without the lossy `agent_end` finalization and the
199
- * maintenance owner resumes the run on the rewritten context.
198
+ * loop ends the current run without lossy finalization. Committed maintenance
199
+ * resumes on rewritten context; failed or aborted maintenance terminalizes.
200
200
  */
201
201
  export type MidRunMaintenanceOutcome = "not-needed" | "pruned" | "compacted" | "promoted" | "failed" | "aborted";
202
202
  export interface ContextMaintenanceResult {
203
203
  outcome: MidRunMaintenanceOutcome;
204
204
  releaseCurrentContext?: boolean;
205
+ errorMessage?: string;
205
206
  }
207
+ export declare function isContinuingMidRunMaintenanceOutcome(outcome: unknown): boolean;
206
208
  /**
207
209
  * Configuration for the agent loop.
208
210
  */
@@ -407,9 +409,9 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
407
409
  * non-optional loop signal, and `awaitEventDrain(invocationSignal)` waits for
408
410
  * prior event consumer bodies with loop and invocation cancellation composed.
409
411
  * Any outcome other than "not-needed" ends the current run with
410
- * `agent_end.stopReason === "maintenance"` (NOT the lossy pause / completed
411
- * finalization); the callback's continuation owner resumes the run on the
412
- * rewritten context.
412
+ * `agent_end.stopReason === "maintenance"`. Successful maintenance resumes
413
+ * on rewritten context; `failed` and `aborted` are terminal and never resend
414
+ * the unchanged context.
413
415
  */
414
416
  maintainContext?: (context: AgentContext, lifecycle: {
415
417
  signal: AbortSignal;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/agent-core",
4
- "version": "0.16.1",
4
+ "version": "0.16.4",
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 and Gajae Code Contributors",
@@ -32,9 +32,9 @@
32
32
  "fmt": "biome format --write ."
33
33
  },
34
34
  "dependencies": {
35
- "@gajae-code/ai": "0.16.1",
36
- "@gajae-code/natives": "0.16.1",
37
- "@gajae-code/utils": "0.16.1",
35
+ "@gajae-code/ai": "0.16.4",
36
+ "@gajae-code/natives": "0.16.4",
37
+ "@gajae-code/utils": "0.16.4",
38
38
  "@opentelemetry/api": "^1.9.0"
39
39
  },
40
40
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -89,6 +89,7 @@ import {
89
89
  type AgentTool,
90
90
  type AgentToolContext,
91
91
  type AgentToolResult,
92
+ isContinuingMidRunMaintenanceOutcome,
92
93
  type ManagedAttemptOutcome,
93
94
  type StandaloneRunOwnership,
94
95
  type StreamFn,
@@ -1260,13 +1261,12 @@ function publishAgentEnd(
1260
1261
  event: Extract<AgentEvent, { type: "agent_end" }>,
1261
1262
  scope?: AttemptScope,
1262
1263
  ): void {
1263
- // Aborted maintenance yields no continuation, so it is terminal for standalone
1264
- // ownership and resource sealing. The event itself keeps its `maintenance`
1265
- // stopReason so AgentSession can still report the aborted maintenance
1266
- // settlement to its consumers.
1264
+ // Only maintenance that committed a context mutation can continue. Failed and
1265
+ // aborted maintenance are terminal so the unchanged context is never resent.
1267
1266
  const publishedEvent = scope ? { ...event, scope } : event;
1268
1267
  const maintenanceContinues =
1269
- publishedEvent.stopReason === "maintenance" && publishedEvent.maintenanceOutcome !== "aborted";
1268
+ publishedEvent.stopReason === "maintenance" &&
1269
+ isContinuingMidRunMaintenanceOutcome(publishedEvent.maintenanceOutcome);
1270
1270
  stream.push(publishedEvent);
1271
1271
  const standalone = config.standaloneRunOwnership
1272
1272
  ? standaloneOwnershipStates.get(config.standaloneRunOwnership)
@@ -3739,6 +3739,16 @@ async function runLoopBody(
3739
3739
  }
3740
3740
 
3741
3741
  if (outcome !== "not-needed") {
3742
+ if (outcome === "failed") {
3743
+ const errorMessage =
3744
+ maintenance.errorMessage ??
3745
+ "Context maintenance failed before the next model request; the unchanged context was not resubmitted.";
3746
+ const message = managedFailureMessage(new Error(errorMessage), config);
3747
+ stream.push({ type: "message_start", message, scope: attemptScope });
3748
+ stream.push({ type: "message_end", message, scope: attemptScope });
3749
+ currentContext.messages.push(message);
3750
+ newMessages.push(message);
3751
+ }
3742
3752
  publishAgentEnd(
3743
3753
  stream,
3744
3754
  config,
@@ -4639,6 +4649,7 @@ async function streamAssistantResponse(
4639
4649
  authCredentialType,
4640
4650
  metadata: resolvedMetadata,
4641
4651
  sessionId: config.providerSessionId ?? config.sessionId,
4652
+ providerSessionId: config.providerSessionId,
4642
4653
  toolChoice: effectiveToolChoice,
4643
4654
  reasoning: effectiveReasoning,
4644
4655
  temperature: effectiveTemperature,
package/src/agent.ts CHANGED
@@ -54,7 +54,7 @@ import type {
54
54
  StreamFn,
55
55
  ToolCallContext,
56
56
  } from "./types";
57
- import { setAgentTerminalOwnerContext } from "./types";
57
+ import { isContinuingMidRunMaintenanceOutcome, setAgentTerminalOwnerContext } from "./types";
58
58
 
59
59
  /**
60
60
  * Closed runtime allowlist of failure-classifier codes. The public diagnostic
@@ -118,7 +118,7 @@ function sanitizeAgentFailure(error: unknown, runtimeClassifiedCode?: string): {
118
118
  }
119
119
 
120
120
  /** Only runtime-authenticated built-in constructors may contribute a name. */
121
- const TRUSTED_ERROR_CONSTRUCTORS = new Map<Function, string>([
121
+ const TRUSTED_ERROR_CONSTRUCTORS = new Map<object, string>([
122
122
  [Error, "Error"],
123
123
  [TypeError, "TypeError"],
124
124
  [RangeError, "RangeError"],
@@ -2147,12 +2147,15 @@ export class Agent {
2147
2147
  }
2148
2148
  this.#state.isStreaming = false;
2149
2149
  this.#state.streamMessage = null;
2150
- // A maintenance checkpoint is only non-terminal while a continuation will
2151
- // follow. An aborted maintenance yields none, and because the loop runs with
2150
+ // A maintenance checkpoint is only non-terminal while a committed rewrite
2151
+ // will continue. Failed or aborted maintenance yields none. Because the loop runs with
2152
2152
  // `resourceSealOwner: "caller"` it deliberately leaves sealing to us, so
2153
2153
  // treating it as a checkpoint here would leave the run open forever and make
2154
2154
  // every cancel report `run_not_sealed`.
2155
- if (event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted") {
2155
+ if (
2156
+ event.stopReason === "maintenance" &&
2157
+ isContinuingMidRunMaintenanceOutcome(event.maintenanceOutcome)
2158
+ ) {
2156
2159
  this.#managedLogicalRunOwner ??= managedLogicalRunOwner ?? runId;
2157
2160
  maintenanceInterrupted = true;
2158
2161
  this.#emit(event);
@@ -91,6 +91,8 @@ export interface GenerateBranchSummaryOptions {
91
91
  * reuses the live turn's provider/WebSocket session.
92
92
  */
93
93
  sessionId?: string;
94
+ /** Opaque provider conversation identity; never derived from branch-summary content. */
95
+ providerSessionId?: string;
94
96
  /** Shared provider state map so the branch summary call reuses session-scoped transport/session caches. */
95
97
  providerSessionState?: Map<string, ProviderSessionState>;
96
98
  /** Hint that websocket transport should be preferred when supported by the provider implementation. */
@@ -291,6 +293,7 @@ export async function generateBranchSummary(
291
293
  reserveTokens = 16384,
292
294
  metadata,
293
295
  sessionId,
296
+ providerSessionId,
294
297
  providerSessionState,
295
298
  preferWebsockets,
296
299
  } = options;
@@ -326,7 +329,16 @@ export async function generateBranchSummary(
326
329
  const response = await instrumentedCompleteSimple(
327
330
  model,
328
331
  { systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
329
- { apiKey, signal, maxTokens: 2048, metadata, sessionId, providerSessionState, preferWebsockets },
332
+ {
333
+ apiKey,
334
+ signal,
335
+ maxTokens: 2048,
336
+ metadata,
337
+ sessionId,
338
+ providerSessionId,
339
+ providerSessionState,
340
+ preferWebsockets,
341
+ },
330
342
  { telemetry: options.telemetry, oneshotKind: "branch_summary" },
331
343
  );
332
344
 
@@ -882,6 +882,8 @@ export interface SummaryOptions {
882
882
  * `providerSessionId ?? sessionId` the agent loop sends for normal turns).
883
883
  */
884
884
  sessionId?: string;
885
+ /** Opaque provider conversation identity; never derived from compaction content. */
886
+ providerSessionId?: string;
885
887
  /** Shared provider state map so maintenance calls reuse session-scoped transport/session caches. */
886
888
  providerSessionState?: Map<string, ProviderSessionState>;
887
889
  /** Hint that websocket transport should be preferred when supported by the provider implementation. */
@@ -1025,6 +1027,7 @@ export async function generateSummary(
1025
1027
  initiatorOverride: options?.initiatorOverride,
1026
1028
  metadata: options?.metadata,
1027
1029
  sessionId: options?.sessionId,
1030
+ providerSessionId: options?.providerSessionId,
1028
1031
  providerSessionState: options?.providerSessionState,
1029
1032
  preferWebsockets: options?.preferWebsockets,
1030
1033
  },
@@ -1073,6 +1076,8 @@ export interface HandoffOptions {
1073
1076
  * reuses the live turn's provider/WebSocket session.
1074
1077
  */
1075
1078
  sessionId?: string;
1079
+ /** Opaque provider conversation identity; never derived from handoff content. */
1080
+ providerSessionId?: string;
1076
1081
  /** Shared provider state map so the handoff call reuses session-scoped transport/session caches. */
1077
1082
  providerSessionState?: Map<string, ProviderSessionState>;
1078
1083
  /** Hint that websocket transport should be preferred when supported by the provider implementation. */
@@ -1120,6 +1125,7 @@ export async function generateHandoff(
1120
1125
  initiatorOverride: options.initiatorOverride,
1121
1126
  metadata: options.metadata,
1122
1127
  sessionId: options.sessionId,
1128
+ providerSessionId: options.providerSessionId,
1123
1129
  providerSessionState: options.providerSessionState,
1124
1130
  preferWebsockets: options.preferWebsockets,
1125
1131
  },
@@ -1378,6 +1384,7 @@ export async function compact(
1378
1384
  convertToLlm: options?.convertToLlm,
1379
1385
  telemetry: options?.telemetry,
1380
1386
  sessionId: options?.sessionId,
1387
+ providerSessionId: options?.providerSessionId,
1381
1388
  providerSessionState: options?.providerSessionState,
1382
1389
  preferWebsockets: options?.preferWebsockets,
1383
1390
  remoteCompactionFallbackHealth: options?.remoteCompactionFallbackHealth,
@@ -1552,6 +1559,7 @@ async function generateTurnPrefixSummary(
1552
1559
  initiatorOverride: options?.initiatorOverride,
1553
1560
  metadata: options?.metadata,
1554
1561
  sessionId: options?.sessionId,
1562
+ providerSessionId: options?.providerSessionId,
1555
1563
  providerSessionState: options?.providerSessionState,
1556
1564
  preferWebsockets: options?.preferWebsockets,
1557
1565
  },
package/src/types.ts CHANGED
@@ -214,14 +214,19 @@ export type ManagedAttemptOutcomeHandler = (
214
214
  * Outcome of a cooperative mid-run context-maintenance checkpoint (see
215
215
  * {@link AgentLoopConfig.maintainContext}). Any value other than "not-needed"
216
216
  * means the checkpoint mutated (or attempted to mutate) durable context, so the
217
- * loop ends the current run without the lossy `agent_end` finalization and the
218
- * maintenance owner resumes the run on the rewritten context.
217
+ * loop ends the current run without lossy finalization. Committed maintenance
218
+ * resumes on rewritten context; failed or aborted maintenance terminalizes.
219
219
  */
220
220
  export type MidRunMaintenanceOutcome = "not-needed" | "pruned" | "compacted" | "promoted" | "failed" | "aborted";
221
221
 
222
222
  export interface ContextMaintenanceResult {
223
223
  outcome: MidRunMaintenanceOutcome;
224
224
  releaseCurrentContext?: boolean;
225
+ errorMessage?: string;
226
+ }
227
+
228
+ export function isContinuingMidRunMaintenanceOutcome(outcome: unknown): boolean {
229
+ return outcome === "pruned" || outcome === "compacted" || outcome === "promoted";
225
230
  }
226
231
 
227
232
  /**
@@ -438,9 +443,9 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
438
443
  * non-optional loop signal, and `awaitEventDrain(invocationSignal)` waits for
439
444
  * prior event consumer bodies with loop and invocation cancellation composed.
440
445
  * Any outcome other than "not-needed" ends the current run with
441
- * `agent_end.stopReason === "maintenance"` (NOT the lossy pause / completed
442
- * finalization); the callback's continuation owner resumes the run on the
443
- * rewritten context.
446
+ * `agent_end.stopReason === "maintenance"`. Successful maintenance resumes
447
+ * on rewritten context; `failed` and `aborted` are terminal and never resend
448
+ * the unchanged context.
444
449
  */
445
450
  maintainContext?: (
446
451
  context: AgentContext,