@gajae-code/agent-core 0.17.1 → 0.17.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.17.4] - 2026-09-23
6
+
7
+ ## [0.17.3] - 2026-09-22
8
+
9
+ ### Performance
10
+
11
+ - Bound stream abort-race retention to the current read and reuse history hashes when checking append-only context rewrites.
12
+
13
+ ## [0.17.2] - 2026-09-18
14
+
5
15
  ## [0.17.1] - 2026-09-17
6
16
 
7
17
  ## [0.17.0] - 2026-09-17
@@ -234,6 +234,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
234
234
  * next tool/turn boundary either way.
235
235
  */
236
236
  toolInterruptPolicy?: "abort_tools" | "finish_tools";
237
+ /** Test-only diagnostic for bounding pending per-read abort-race reactions. */
238
+ onAbortRaceReactionChange?: (delta: 1 | -1) => void;
237
239
  /**
238
240
  * Optional session identifier forwarded to LLM providers.
239
241
  * Used by providers that support session-based caching (e.g., OpenAI code provider).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/agent-core",
4
- "version": "0.17.1",
4
+ "version": "0.17.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.17.1",
36
- "@gajae-code/natives": "0.17.1",
37
- "@gajae-code/utils": "0.17.1",
35
+ "@gajae-code/ai": "0.17.4",
36
+ "@gajae-code/natives": "0.17.4",
37
+ "@gajae-code/utils": "0.17.4",
38
38
  "@opentelemetry/api": "^1.9.0"
39
39
  },
40
40
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -2030,6 +2030,8 @@ function losslessDetachedClone<T>(value: T): T {
2030
2030
  "kind",
2031
2031
  "status",
2032
2032
  "code",
2033
+ "http2RstCode",
2034
+ "nativeErrorCode",
2033
2035
  "providerCode",
2034
2036
  "openaiErrorCode",
2035
2037
  "anthropicErrorType",
@@ -4808,10 +4810,9 @@ async function streamAssistantResponse(
4808
4810
  return getResponseResult();
4809
4811
  };
4810
4812
 
4811
- // Set up a single abort race: register the abort listener once for the whole
4812
- // stream and reuse the same race promise for every iterator.next() instead of
4813
- // allocating Promise.withResolvers and add/removeEventListener per event.
4814
- let abortRacePromise: Promise<typeof ABORTED> | undefined;
4813
+ // Keep one listener, but race a fresh promise per read so pending abort
4814
+ // reactions do not retain every event until the request ends.
4815
+ let settleReadAbort: (() => void) | undefined;
4815
4816
  let detachAbortListener: (() => void) | undefined;
4816
4817
  if (requestSignal) {
4817
4818
  if (requestSignal.aborted) {
@@ -4827,18 +4828,32 @@ async function streamAssistantResponse(
4827
4828
  await finishChat(aborted);
4828
4829
  return aborted;
4829
4830
  }
4830
- const { promise, resolve } = Promise.withResolvers<typeof ABORTED>();
4831
- const onAbort = () => resolve(ABORTED);
4831
+ const onAbort = () => settleReadAbort?.();
4832
4832
  requestSignal.addEventListener("abort", onAbort, { once: true });
4833
- abortRacePromise = promise;
4834
4833
  detachAbortListener = () => requestSignal.removeEventListener("abort", onAbort);
4835
4834
  }
4836
4835
 
4837
4836
  try {
4838
4837
  while (true) {
4839
4838
  let next: IteratorResult<AssistantMessageEvent>;
4840
- if (abortRacePromise) {
4841
- const result = await Promise.race([responseIterator.next(), abortRacePromise]);
4839
+ if (requestSignal) {
4840
+ const { promise, resolve } = Promise.withResolvers<typeof ABORTED>();
4841
+ let settled = false;
4842
+ const settleAbort = (): void => {
4843
+ if (settled) return;
4844
+ settled = true;
4845
+ resolve(ABORTED);
4846
+ config.onAbortRaceReactionChange?.(-1);
4847
+ };
4848
+ config.onAbortRaceReactionChange?.(1);
4849
+ settleReadAbort = settleAbort;
4850
+ let result: IteratorResult<AssistantMessageEvent> | typeof ABORTED;
4851
+ try {
4852
+ result = requestSignal.aborted ? ABORTED : await Promise.race([responseIterator.next(), promise]);
4853
+ } finally {
4854
+ settleAbort();
4855
+ settleReadAbort = undefined;
4856
+ }
4842
4857
  if (result === ABORTED) {
4843
4858
  closeIterator();
4844
4859
  const aborted = emitAbortedAssistantMessage(
@@ -272,18 +272,19 @@ export class AppendOnlyContextManager {
272
272
  seededPrefixLength > 0 && !includesSeedPrefix
273
273
  ? [...this.log.entries().slice(0, seededPrefixLength), ...normalizedMessages]
274
274
  : normalizedMessages;
275
+ const hashes = this.#hashRange(messagesToSync, 0, messagesToSync.length);
275
276
 
276
277
  // Detect in-place rewrites of already-synced messages via per-message content
277
278
  // hashes (no retained full serialized-history string; F5).
278
279
  if (
279
280
  this.#lastSyncCount > 0 &&
280
281
  this.#lastSyncCount <= messagesToSync.length &&
281
- this.#prefixChanged(messagesToSync, this.#lastSyncCount)
282
+ this.#prefixChanged(hashes, this.#lastSyncCount)
282
283
  ) {
283
284
  if (this.#seededPrefixCount > 0) {
284
285
  // F9: a seeded fork whose inherited prefix changed (e.g. after compaction)
285
286
  // rebases onto the new provider context instead of throwing.
286
- this.#rebaseToBaseline(messagesToSync, seededPrefixLength);
287
+ this.#rebaseToBaseline(messagesToSync, hashes, seededPrefixLength);
287
288
  return;
288
289
  }
289
290
  this.log.clear();
@@ -296,7 +297,7 @@ export class AppendOnlyContextManager {
296
297
  // while a seed prefix is active; a genuine seeded compaction rebases (F9).
297
298
  if (messagesToSync.length < this.#lastSyncCount) {
298
299
  if (this.#seededPrefixCount > 0) {
299
- this.#rebaseToBaseline(messagesToSync, seededPrefixLength);
300
+ this.#rebaseToBaseline(messagesToSync, hashes, seededPrefixLength);
300
301
  return;
301
302
  }
302
303
  this.log.clear();
@@ -310,7 +311,7 @@ export class AppendOnlyContextManager {
310
311
  }
311
312
 
312
313
  this.#lastSyncCount = messagesToSync.length;
313
- this.#syncedHashes = this.#hashRange(messagesToSync, 0, messagesToSync.length);
314
+ this.#syncedHashes = hashes;
314
315
  }
315
316
 
316
317
  seedNormalizedMessages(messages: readonly Message[], options?: { reset?: boolean }): void {
@@ -393,21 +394,21 @@ export class AppendOnlyContextManager {
393
394
  }
394
395
 
395
396
  /** True when any of the first `count` already-synced messages changed content (in-place rewrite). */
396
- #prefixChanged(messages: readonly unknown[], count: number): boolean {
397
+ #prefixChanged(hashes: readonly (number | bigint)[], count: number): boolean {
397
398
  if (count > this.#syncedHashes.length) return false;
398
399
  for (let i = 0; i < count; i++) {
399
- if (this.#hashMessage(messages[i]) !== this.#syncedHashes[i]) return true;
400
+ if (hashes[i] !== this.#syncedHashes[i]) return true;
400
401
  }
401
402
  return false;
402
403
  }
403
404
 
404
405
  /** F9: reset the log to a new provider-visible baseline after seeded compaction/rebase. */
405
- #rebaseToBaseline(messages: readonly unknown[], seededPrefixCount = 0): void {
406
+ #rebaseToBaseline(messages: readonly unknown[], hashes: (number | bigint)[], seededPrefixCount: number): void {
406
407
  this.log.clear();
407
408
  this.log.extend(messages.map(message => cloneJson(message)));
408
409
  this.#lastSyncCount = messages.length;
409
410
  this.#seededPrefixCount = seededPrefixCount;
410
- this.#syncedHashes = this.#hashRange(messages, 0, messages.length);
411
+ this.#syncedHashes = hashes;
411
412
  }
412
413
  }
413
414
 
@@ -1365,8 +1365,7 @@ const TURN_PREFIX_SUMMARIZATION_PROMPT = prompt.render(compactionTurnPrefixPromp
1365
1365
  * Reasoning effort for a maintenance one-shot call (summary, turn-prefix
1366
1366
  * summary, handoff), sized against the model that will actually run it.
1367
1367
  *
1368
- * These calls want `high`, but they must never *demand* it: the fallback
1369
- * chain hands them whatever same-provider model has the most context, and a
1368
+ * These calls want `high`, but they must never *demand* it: a
1370
1369
  * reasoning-capable model on a transport without reasoning control (the
1371
1370
  * registry strips `thinking` for a proxied `openai-codex` baseUrl) rejects any
1372
1371
  * explicit effort inside the provider mapper. The agent turn already clamps
package/src/telemetry.ts CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  type ToolChoice,
38
38
  type Usage,
39
39
  } from "@gajae-code/ai";
40
+ import { isDesignedError } from "@gajae-code/utils/error-classification";
40
41
  import { recordHandledError } from "@gajae-code/utils/postmortem";
41
42
  import {
42
43
  type Attributes,
@@ -1878,7 +1879,7 @@ export function finishExecuteToolSpan(
1878
1879
  ? options.errorObject.name || "Error"
1879
1880
  : STATUS_ERROR_TYPE[status];
1880
1881
  }
1881
- if (status === "error" && options.errorObject instanceof Error) {
1882
+ if (status === "error" && options.errorObject instanceof Error && !isDesignedError(options.errorObject)) {
1882
1883
  try {
1883
1884
  recordHandledError(`Tool ${options.toolName}`, options.errorObject);
1884
1885
  } catch {}
package/src/types.ts CHANGED
@@ -260,6 +260,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
260
260
  * next tool/turn boundary either way.
261
261
  */
262
262
  toolInterruptPolicy?: "abort_tools" | "finish_tools";
263
+ /** Test-only diagnostic for bounding pending per-read abort-race reactions. */
264
+ onAbortRaceReactionChange?: (delta: 1 | -1) => void;
263
265
 
264
266
  /**
265
267
  * Optional session identifier forwarded to LLM providers.