@gajae-code/agent-core 0.16.3 → 0.16.6

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,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.16.6] - 2026-09-07
6
+
7
+ ## [0.16.5] - 2026-09-07
8
+
9
+ - Compaction summary, turn-prefix summary, and handoff generation now clamp their reasoning effort to the model they are about to call instead of hard-coding `high`. A reasoning-capable model on a transport without reasoning control (the registry strips `thinking` when `openai-codex` or `anthropic` is routed through a non-audited proxy `baseUrl`) rejected the raw effort inside the provider mapper with "Model <provider>/<id> does not support thinking"; since the compaction fallback chain then reaches for the same-provider largest-context model, every candidate died on that throw and auto-compaction reported only the last one. The agent turn already clamps through `clampThinkingLevelForModel`; the maintenance calls were the one path still sending an unclamped effort.
10
+
11
+ ## [0.16.4] - 2026-09-05
12
+
13
+ - 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).
14
+
15
+ - 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.
16
+
5
17
  ## [0.16.3] - 2026-09-04
6
18
 
7
19
  ## [0.16.2] - 2026-09-04
@@ -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.3",
4
+ "version": "0.16.6",
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.3",
36
- "@gajae-code/natives": "0.16.3",
37
- "@gajae-code/utils": "0.16.3",
35
+ "@gajae-code/ai": "0.16.6",
36
+ "@gajae-code/natives": "0.16.6",
37
+ "@gajae-code/utils": "0.16.6",
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
 
@@ -8,6 +8,7 @@
8
8
  import * as os from "node:os";
9
9
  import {
10
10
  type AssistantMessage,
11
+ clampThinkingLevelForModel,
11
12
  Effort,
12
13
  type Message,
13
14
  type MessageAttribution,
@@ -882,6 +883,8 @@ export interface SummaryOptions {
882
883
  * `providerSessionId ?? sessionId` the agent loop sends for normal turns).
883
884
  */
884
885
  sessionId?: string;
886
+ /** Opaque provider conversation identity; never derived from compaction content. */
887
+ providerSessionId?: string;
885
888
  /** Shared provider state map so maintenance calls reuse session-scoped transport/session caches. */
886
889
  providerSessionState?: Map<string, ProviderSessionState>;
887
890
  /** Hint that websocket transport should be preferred when supported by the provider implementation. */
@@ -1021,10 +1024,11 @@ export async function generateSummary(
1021
1024
  maxTokens,
1022
1025
  signal,
1023
1026
  apiKey,
1024
- reasoning: Effort.High,
1027
+ reasoning: maintenanceReasoning(model),
1025
1028
  initiatorOverride: options?.initiatorOverride,
1026
1029
  metadata: options?.metadata,
1027
1030
  sessionId: options?.sessionId,
1031
+ providerSessionId: options?.providerSessionId,
1028
1032
  providerSessionState: options?.providerSessionState,
1029
1033
  preferWebsockets: options?.preferWebsockets,
1030
1034
  },
@@ -1073,6 +1077,8 @@ export interface HandoffOptions {
1073
1077
  * reuses the live turn's provider/WebSocket session.
1074
1078
  */
1075
1079
  sessionId?: string;
1080
+ /** Opaque provider conversation identity; never derived from handoff content. */
1081
+ providerSessionId?: string;
1076
1082
  /** Shared provider state map so the handoff call reuses session-scoped transport/session caches. */
1077
1083
  providerSessionState?: Map<string, ProviderSessionState>;
1078
1084
  /** Hint that websocket transport should be preferred when supported by the provider implementation. */
@@ -1115,11 +1121,12 @@ export async function generateHandoff(
1115
1121
  {
1116
1122
  apiKey,
1117
1123
  signal,
1118
- reasoning: Effort.High,
1124
+ reasoning: maintenanceReasoning(model),
1119
1125
  toolChoice: "none",
1120
1126
  initiatorOverride: options.initiatorOverride,
1121
1127
  metadata: options.metadata,
1122
1128
  sessionId: options.sessionId,
1129
+ providerSessionId: options.providerSessionId,
1123
1130
  providerSessionState: options.providerSessionState,
1124
1131
  preferWebsockets: options.preferWebsockets,
1125
1132
  },
@@ -1340,6 +1347,22 @@ export function prepareCompaction(
1340
1347
 
1341
1348
  const TURN_PREFIX_SUMMARIZATION_PROMPT = prompt.render(compactionTurnPrefixPrompt);
1342
1349
 
1350
+ /**
1351
+ * Reasoning effort for a maintenance one-shot call (summary, turn-prefix
1352
+ * summary, handoff), sized against the model that will actually run it.
1353
+ *
1354
+ * These calls want `high`, but they must never *demand* it: the fallback
1355
+ * chain hands them whatever same-provider model has the most context, and a
1356
+ * reasoning-capable model on a transport without reasoning control (the
1357
+ * registry strips `thinking` for a proxied `openai-codex` baseUrl) rejects any
1358
+ * explicit effort inside the provider mapper. The agent turn already clamps
1359
+ * through this helper; the maintenance calls must not be the one path that
1360
+ * still sends a raw effort.
1361
+ */
1362
+ function maintenanceReasoning(model: Model): Effort | undefined {
1363
+ return clampThinkingLevelForModel(model, Effort.High);
1364
+ }
1365
+
1343
1366
  /**
1344
1367
  * Generate summaries for compaction using prepared data.
1345
1368
  * Returns CompactionResult - SessionManager adds id/parentId when saving.
@@ -1378,6 +1401,7 @@ export async function compact(
1378
1401
  convertToLlm: options?.convertToLlm,
1379
1402
  telemetry: options?.telemetry,
1380
1403
  sessionId: options?.sessionId,
1404
+ providerSessionId: options?.providerSessionId,
1381
1405
  providerSessionState: options?.providerSessionState,
1382
1406
  preferWebsockets: options?.preferWebsockets,
1383
1407
  remoteCompactionFallbackHealth: options?.remoteCompactionFallbackHealth,
@@ -1548,10 +1572,11 @@ async function generateTurnPrefixSummary(
1548
1572
  maxTokens,
1549
1573
  signal,
1550
1574
  apiKey,
1551
- reasoning: Effort.High,
1575
+ reasoning: maintenanceReasoning(model),
1552
1576
  initiatorOverride: options?.initiatorOverride,
1553
1577
  metadata: options?.metadata,
1554
1578
  sessionId: options?.sessionId,
1579
+ providerSessionId: options?.providerSessionId,
1555
1580
  providerSessionState: options?.providerSessionState,
1556
1581
  preferWebsockets: options?.preferWebsockets,
1557
1582
  },
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,