@gajae-code/agent-core 0.15.5 → 0.16.0

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,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.16.0] - 2026-09-02
6
+
7
+ ## [0.15.6] - 2026-08-30
8
+
5
9
  ## [0.15.5] - 2026-08-29
6
10
 
7
11
  ## [0.15.4] - 2026-08-29
@@ -5,7 +5,7 @@ import { type AssistantMessage, type AssistantMessageEvent, type CursorExecHandl
5
5
  import type { AppendOnlyContextManager } from "./append-only-context";
6
6
  import type { AttemptRunHandle, AttemptScope } from "./attempt-scope";
7
7
  import type { HarmonyAuditEvent } from "./harmony-leak";
8
- import type { AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, ManagedLogicalRunId, RunCancellationDomainBridge, RunResourceLedger, RunTerminalRequest, StreamFn, ToolCallContext } from "./types";
8
+ import type { AgentEvent, AgentLoopConfig, AgentMessage, AgentMetadataResolverContext, AgentState, AgentTool, AgentToolContext, ManagedLogicalRunId, RunCancellationDomainBridge, RunResourceLedger, RunTerminalRequest, StreamFn, ToolCallContext } from "./types";
9
9
  /**
10
10
  * Whether persisted history ends at a point where a new model turn can resume.
11
11
  * Assistant-ended histories require an in-memory queued message and are handled
@@ -271,7 +271,7 @@ export declare class Agent {
271
271
  * only included for `"anthropic"` requests). Falls back to the static
272
272
  * {@link metadata} value when no resolver is set.
273
273
  */
274
- metadataForProvider(provider: string): Record<string, unknown> | undefined;
274
+ metadataForProvider(provider: string, model?: Model, transport?: AgentMetadataResolverContext["transport"]): Record<string, unknown> | undefined;
275
275
  /**
276
276
  * Install a function that resolves request metadata at call time. The
277
277
  * resolver receives the target provider string and can gate provider-specific
@@ -280,7 +280,7 @@ export declare class Agent {
280
280
  * credential. Pass `undefined` to clear and revert to the static
281
281
  * {@link metadata} value.
282
282
  */
283
- setMetadataResolver(resolver: ((provider: string) => Record<string, unknown> | undefined) | undefined): void;
283
+ setMetadataResolver(resolver: ((context: AgentMetadataResolverContext) => Record<string, unknown> | undefined) | undefined): void;
284
284
  /**
285
285
  * Read the active OpenTelemetry configuration. Returns `undefined` when
286
286
  * instrumentation is disabled. Callers spawning child runs (e.g. subagent
@@ -358,6 +358,18 @@ export declare class Agent {
358
358
  setProvisionalAssistantMessageEventInterceptor(fn: ((message: AssistantMessage, event: AssistantMessageEvent) => void) | undefined): void;
359
359
  setOnBeforeYield(fn: (() => Promise<void> | void) | undefined): void;
360
360
  setShouldPause(fn: AgentLoopConfig["shouldPause"] | undefined): void;
361
+ /** The currently installed cooperative pause checkpoint, if any. */
362
+ get shouldPause(): AgentLoopConfig["shouldPause"] | undefined;
363
+ /**
364
+ * Fence old-turn steering admission.
365
+ *
366
+ * The loop polls steering UPSTREAM of its pause checkpoint (and again on the
367
+ * immediate-interrupt path), so a cooperative stop alone cannot prevent one
368
+ * more old-turn model call once a steering message has already been dequeued.
369
+ * While the fence returns true the poll yields no messages AND does not
370
+ * dequeue, so the queue survives intact for the next turn.
371
+ */
372
+ setSteeringAdmissionFence(fn: (() => boolean) | undefined): void;
361
373
  setMaintainContext(fn: AgentLoopConfig["maintainContext"] | undefined): void;
362
374
  /**
363
375
  * Publish an event produced OUTSIDE the agent loop (a provider that executed the tool
@@ -6,6 +6,20 @@ import type { AgentRunCoverage, AgentRunSummary } from "./run-collector";
6
6
  import type { AgentTelemetryConfig } from "./telemetry";
7
7
  /** Stream function - can return sync or Promise for async config lookup */
8
8
  export type StreamFn = (...args: Parameters<typeof streamSimple>) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
9
+ /**
10
+ * Request context supplied to provider-aware metadata resolvers.
11
+ *
12
+ * The model is the exact model selected for the concrete request (including
13
+ * fallback and ephemeral requests), while `transport` distinguishes the
14
+ * built-in stream path from a caller-supplied stream function. Metadata that
15
+ * carries provider identity must use both values to fail closed when routing
16
+ * is not the canonical provider path.
17
+ */
18
+ export interface AgentMetadataResolverContext {
19
+ provider: string;
20
+ model?: Model;
21
+ transport?: "default" | "custom";
22
+ }
9
23
  /** Stable identifier for a managed logical run, shared by all of its retry attempts. */
10
24
  export type ManagedLogicalRunId = number;
11
25
  /** A resource owned by a prompt run until its promise settles. */
@@ -233,7 +247,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
233
247
  * current when `AgentLoopConfig` was first constructed). Overrides the static
234
248
  * `metadata` field when present.
235
249
  */
236
- metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
250
+ metadataResolver?: (context: AgentMetadataResolverContext) => Record<string, unknown> | undefined;
237
251
  /**
238
252
  * Converts AgentMessage[] to LLM-compatible Message[] before each LLM call.
239
253
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/agent-core",
4
- "version": "0.15.5",
4
+ "version": "0.16.0",
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.15.5",
36
- "@gajae-code/natives": "0.15.5",
37
- "@gajae-code/utils": "0.15.5",
35
+ "@gajae-code/ai": "0.16.0",
36
+ "@gajae-code/natives": "0.16.0",
37
+ "@gajae-code/utils": "0.16.0",
38
38
  "@opentelemetry/api": "^1.9.0"
39
39
  },
40
40
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -4523,7 +4523,13 @@ async function streamAssistantResponse(
4523
4523
  // reflects the credential actually used, not the snapshot from AgentLoopConfig construction.
4524
4524
  const authCredentialType = config.getAuthCredentialType?.(config.model.provider);
4525
4525
 
4526
- const resolvedMetadata = config.metadataResolver ? config.metadataResolver(config.model.provider) : config.metadata;
4526
+ const resolvedMetadata = config.metadataResolver
4527
+ ? config.metadataResolver({
4528
+ provider: config.model.provider,
4529
+ model: config.model,
4530
+ transport: streamFunction === streamSimple ? "default" : "custom",
4531
+ })
4532
+ : config.metadata;
4527
4533
 
4528
4534
  // Synthetic recovery requests choose their tool mode explicitly below and
4529
4535
  // must never consume a queued dynamic choice intended for an ordinary turn.
package/src/agent.ts CHANGED
@@ -38,6 +38,7 @@ import type {
38
38
  AgentEvent,
39
39
  AgentLoopConfig,
40
40
  AgentMessage,
41
+ AgentMetadataResolverContext,
41
42
  AgentState,
42
43
  AgentTool,
43
44
  AgentToolContext,
@@ -116,6 +117,27 @@ function sanitizeAgentFailure(error: unknown, runtimeClassifiedCode?: string): {
116
117
  return { code, message: "Agent run failed." };
117
118
  }
118
119
 
120
+ /** Only runtime-authenticated built-in constructors may contribute a name. */
121
+ const TRUSTED_ERROR_CONSTRUCTORS = new Map<Function, string>([
122
+ [Error, "Error"],
123
+ [TypeError, "TypeError"],
124
+ [RangeError, "RangeError"],
125
+ [SyntaxError, "SyntaxError"],
126
+ [ReferenceError, "ReferenceError"],
127
+ [URIError, "URIError"],
128
+ [EvalError, "EvalError"],
129
+ [AggregateError, "AggregateError"],
130
+ ]);
131
+
132
+ function safeErrorName(error: unknown): string | undefined {
133
+ try {
134
+ if (!(error instanceof Error)) return undefined;
135
+ return TRUSTED_ERROR_CONSTRUCTORS.get(error.constructor);
136
+ } catch {
137
+ return undefined;
138
+ }
139
+ }
140
+
119
141
  /** Guarded HTTP-status extraction for untrusted provider errors: a throwing
120
142
  * getter must never escape the failure handler and suppress terminalization
121
143
  * (exact-head review P1). */
@@ -466,7 +488,7 @@ export class Agent {
466
488
  #sessionId?: string;
467
489
  #providerSessionId?: string;
468
490
  #metadata?: Record<string, unknown>;
469
- #metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
491
+ #metadataResolver?: (context: AgentMetadataResolverContext) => Record<string, unknown> | undefined;
470
492
  #providerSessionState?: Map<string, ProviderSessionState>;
471
493
  #thinkingBudgets?: ThinkingBudgets;
472
494
  #temperature?: number;
@@ -505,6 +527,8 @@ export class Agent {
505
527
  #onHarmonyLeak?: (event: HarmonyAuditEvent) => void | Promise<void>;
506
528
  #onBeforeYield?: () => Promise<void> | void;
507
529
  #shouldPause?: AgentLoopConfig["shouldPause"];
530
+ /** While set and returning true, steering is neither admitted nor dequeued. */
531
+ #steeringAdmissionFence?: () => boolean;
508
532
  #maintainContext?: AgentLoopConfig["maintainContext"];
509
533
  #telemetry?: AgentLoopConfig["telemetry"];
510
534
  #appendOnlyContext?: AppendOnlyContextManager;
@@ -669,8 +693,12 @@ export class Agent {
669
693
  * only included for `"anthropic"` requests). Falls back to the static
670
694
  * {@link metadata} value when no resolver is set.
671
695
  */
672
- metadataForProvider(provider: string): Record<string, unknown> | undefined {
673
- if (this.#metadataResolver) return this.#metadataResolver(provider);
696
+ metadataForProvider(
697
+ provider: string,
698
+ model?: Model,
699
+ transport?: AgentMetadataResolverContext["transport"],
700
+ ): Record<string, unknown> | undefined {
701
+ if (this.#metadataResolver) return this.#metadataResolver({ provider, model, transport });
674
702
  return this.#metadata;
675
703
  }
676
704
 
@@ -682,7 +710,9 @@ export class Agent {
682
710
  * credential. Pass `undefined` to clear and revert to the static
683
711
  * {@link metadata} value.
684
712
  */
685
- setMetadataResolver(resolver: ((provider: string) => Record<string, unknown> | undefined) | undefined): void {
713
+ setMetadataResolver(
714
+ resolver: ((context: AgentMetadataResolverContext) => Record<string, unknown> | undefined) | undefined,
715
+ ): void {
686
716
  this.#metadataResolver = resolver;
687
717
  }
688
718
 
@@ -891,6 +921,24 @@ export class Agent {
891
921
  this.#shouldPause = fn;
892
922
  }
893
923
 
924
+ /** The currently installed cooperative pause checkpoint, if any. */
925
+ get shouldPause(): AgentLoopConfig["shouldPause"] | undefined {
926
+ return this.#shouldPause;
927
+ }
928
+
929
+ /**
930
+ * Fence old-turn steering admission.
931
+ *
932
+ * The loop polls steering UPSTREAM of its pause checkpoint (and again on the
933
+ * immediate-interrupt path), so a cooperative stop alone cannot prevent one
934
+ * more old-turn model call once a steering message has already been dequeued.
935
+ * While the fence returns true the poll yields no messages AND does not
936
+ * dequeue, so the queue survives intact for the next turn.
937
+ */
938
+ setSteeringAdmissionFence(fn: (() => boolean) | undefined): void {
939
+ this.#steeringAdmissionFence = fn;
940
+ }
941
+
894
942
  setMaintainContext(fn: AgentLoopConfig["maintainContext"] | undefined): void {
895
943
  this.#maintainContext = fn;
896
944
  }
@@ -1911,6 +1959,12 @@ export class Agent {
1911
1959
  skipInitialSteeringPoll = false;
1912
1960
  return [];
1913
1961
  }
1962
+ // Fenced: yield nothing and dequeue nothing, so a steer submitted while a
1963
+ // fold is being claimed is neither consumed by the run being wound down
1964
+ // nor lost.
1965
+ if (this.#steeringAdmissionFence?.() === true) {
1966
+ return [];
1967
+ }
1914
1968
  const queued = this.#dequeueSteeringMessages();
1915
1969
  if (this.#activeRunId !== runId) {
1916
1970
  this.#steeringQueue = [...queued, ...this.#steeringQueue];
@@ -2095,6 +2149,8 @@ export class Agent {
2095
2149
  const runtimeFailureCode = abortController.signal.aborted
2096
2150
  ? "aborted"
2097
2151
  : (managedLocalErrorDiagnostic(err)?.errorKind ?? providerCode);
2152
+ const sanitized = sanitizeAgentFailure(err, runtimeFailureCode);
2153
+ const errorName = safeErrorName(err);
2098
2154
 
2099
2155
  const errorMsg: AgentMessage = {
2100
2156
  role: "assistant",
@@ -2111,7 +2167,9 @@ export class Agent {
2111
2167
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
2112
2168
  },
2113
2169
  stopReason: abortController.signal.aborted ? "aborted" : "error",
2114
- errorMessage: sanitizeAgentFailure(err).message,
2170
+ errorMessage: sanitized.message,
2171
+ errorCode: sanitized.code,
2172
+ ...(errorName ? { errorName } : {}),
2115
2173
  errorStatus: safeErrorStatus(err),
2116
2174
  // Local-diagnostic authority (`errorKind` + structured
2117
2175
  // `bufferOverflow`) comes from ONE identity check: a foreign error
package/src/types.ts CHANGED
@@ -28,6 +28,21 @@ export type StreamFn = (
28
28
  ...args: Parameters<typeof streamSimple>
29
29
  ) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
30
30
 
31
+ /**
32
+ * Request context supplied to provider-aware metadata resolvers.
33
+ *
34
+ * The model is the exact model selected for the concrete request (including
35
+ * fallback and ephemeral requests), while `transport` distinguishes the
36
+ * built-in stream path from a caller-supplied stream function. Metadata that
37
+ * carries provider identity must use both values to fail closed when routing
38
+ * is not the canonical provider path.
39
+ */
40
+ export interface AgentMetadataResolverContext {
41
+ provider: string;
42
+ model?: Model;
43
+ transport?: "default" | "custom";
44
+ }
45
+
31
46
  /** Stable identifier for a managed logical run, shared by all of its retry attempts. */
32
47
  export type ManagedLogicalRunId = number;
33
48
  /** A resource owned by a prompt run until its promise settles. */
@@ -257,7 +272,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
257
272
  * current when `AgentLoopConfig` was first constructed). Overrides the static
258
273
  * `metadata` field when present.
259
274
  */
260
- metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
275
+ metadataResolver?: (context: AgentMetadataResolverContext) => Record<string, unknown> | undefined;
261
276
 
262
277
  /**
263
278
  * Converts AgentMessage[] to LLM-compatible Message[] before each LLM call.