@gajae-code/agent-core 0.11.11 → 0.12.1

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/src/agent.ts CHANGED
@@ -25,6 +25,7 @@ import { agentLoop, agentLoopContinue } from "./agent-loop";
25
25
  import type { AppendOnlyContextManager } from "./append-only-context";
26
26
  import type { HarmonyAuditEvent } from "./harmony-leak";
27
27
  import { assertImagePlaceholdersHavePayload } from "./image-placeholder-guard";
28
+ import { createRunResourceLedger } from "./run-resource-ledger";
28
29
  import type {
29
30
  AgentContext,
30
31
  AgentEvent,
@@ -38,6 +39,7 @@ import type {
38
39
  ManagedAttemptDecision,
39
40
  ManagedAttemptOutcome,
40
41
  ManagedLogicalRunId,
42
+ RunResourceLedger,
41
43
  RunTerminalRequest,
42
44
  StreamFn,
43
45
  ToolCallContext,
@@ -235,6 +237,8 @@ export interface AgentOptions {
235
237
  requestMaxRetries?: number;
236
238
  /** Provider stream replay retry budget. Counts retries, not the initial attempt. */
237
239
  streamMaxRetries?: number;
240
+ /** Explicit first-event stream watchdog override in milliseconds. Set to 0 to disable. */
241
+ streamFirstEventTimeoutMs?: number;
238
242
 
239
243
  /**
240
244
  * Provides tool execution context, resolved per tool call.
@@ -354,6 +358,7 @@ export class Agent {
354
358
  #maxRetryDelayMs?: number;
355
359
  #requestMaxRetries?: number;
356
360
  #streamMaxRetries?: number;
361
+ #streamFirstEventTimeoutMs?: number;
357
362
  #getToolContext?: (toolCall?: ToolCallContext) => AgentToolContext | undefined;
358
363
  #cursorExecHandlers?: CursorExecHandlers;
359
364
  #cursorOnToolResult?: CursorToolResultHandler;
@@ -361,6 +366,7 @@ export class Agent {
361
366
  #resolveRunningPrompt?: () => void;
362
367
  #runSequence = 0;
363
368
  #activeRunId?: number;
369
+ #activeResourceRunId?: string;
364
370
  #continuationGeneration = 0;
365
371
  #activeFallbackManaged = false;
366
372
  #kimiApiFormat?: "openai" | "anthropic";
@@ -388,6 +394,7 @@ export class Agent {
388
394
  #cursorToolResultBuffer: CursorToolResultEntry[] = [];
389
395
  #terminalizedLogicalRunIds = new Set<ManagedLogicalRunId>();
390
396
  #managedLogicalRunOwner?: ManagedLogicalRunId;
397
+ readonly resourceLedger: RunResourceLedger = createRunResourceLedger();
391
398
 
392
399
  streamFn: StreamFn;
393
400
  getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
@@ -426,6 +433,7 @@ export class Agent {
426
433
  this.#maxRetryDelayMs = opts.maxRetryDelayMs;
427
434
  this.#requestMaxRetries = opts.requestMaxRetries;
428
435
  this.#streamMaxRetries = opts.streamMaxRetries;
436
+ this.#streamFirstEventTimeoutMs = opts.streamFirstEventTimeoutMs;
429
437
  this.getApiKey = opts.getApiKey;
430
438
  this.getAuthCredentialType = opts.getAuthCredentialType;
431
439
  this.#onPayload = opts.onPayload;
@@ -671,6 +679,14 @@ export class Agent {
671
679
  this.#streamMaxRetries = value;
672
680
  }
673
681
 
682
+ get streamFirstEventTimeoutMs(): number | undefined {
683
+ return this.#streamFirstEventTimeoutMs;
684
+ }
685
+
686
+ set streamFirstEventTimeoutMs(value: number | undefined) {
687
+ this.#streamFirstEventTimeoutMs = value;
688
+ }
689
+
674
690
  get state(): AgentState {
675
691
  return this.#state;
676
692
  }
@@ -1152,12 +1168,14 @@ export class Agent {
1152
1168
  this.#state.pendingToolCalls = new Set<string>();
1153
1169
  this.#abortController = undefined;
1154
1170
  this.#cursorToolResultBuffer = [];
1171
+ this.resourceLedger.quarantine(this.#activeResourceRunId ?? String(managedLogicalRunId ?? runId));
1155
1172
  this.#managedLogicalRunOwner = undefined;
1156
1173
 
1157
1174
  const resolve = this.#resolveRunningPrompt;
1158
1175
  this.#runningPrompt = undefined;
1159
1176
  this.#resolveRunningPrompt = undefined;
1160
1177
  this.#activeRunId = undefined;
1178
+ this.#activeResourceRunId = undefined;
1161
1179
  resolve?.();
1162
1180
  if (this.#activeFallbackManaged) {
1163
1181
  this.requestRunTerminal(managedLogicalRunId ?? runId, { stopReason: "cancelled" });
@@ -1175,6 +1193,10 @@ export class Agent {
1175
1193
  get activeRunId(): number | undefined {
1176
1194
  return this.#activeRunId;
1177
1195
  }
1196
+ /** Stable resource ownership identifier for the active prompt run. */
1197
+ get activeResourceRunId(): string | undefined {
1198
+ return this.#activeResourceRunId;
1199
+ }
1178
1200
 
1179
1201
  /**
1180
1202
  * Stable identifier for the active managed logical run, shared by every retry
@@ -1196,6 +1218,9 @@ export class Agent {
1196
1218
  */
1197
1219
  requestRunTerminal(logicalRunId: ManagedLogicalRunId, request: RunTerminalRequest): boolean {
1198
1220
  if (this.#terminalizedLogicalRunIds.has(logicalRunId)) return false;
1221
+ if (this.#managedLogicalRunOwner === logicalRunId) {
1222
+ this.#managedLogicalRunOwner = undefined;
1223
+ }
1199
1224
  this.#finalizeRun(
1200
1225
  logicalRunId,
1201
1226
  {
@@ -1339,11 +1364,13 @@ export class Agent {
1339
1364
  this.#state.isStreaming = true;
1340
1365
  this.#state.streamMessage = null;
1341
1366
  this.#state.error = undefined;
1342
- options?.onRunAccepted?.();
1343
1367
 
1344
1368
  const fallbackManaged = options?.fallbackManaged === true;
1345
1369
  const managedLogicalRunOwner = fallbackManaged ? (this.#managedLogicalRunOwner ?? runId) : undefined;
1346
1370
  const startsManagedLogicalRun = fallbackManaged && this.#managedLogicalRunOwner === undefined;
1371
+ this.#activeResourceRunId = String(managedLogicalRunOwner ?? runId);
1372
+ this.resourceLedger.open(this.#activeResourceRunId);
1373
+ options?.onRunAccepted?.();
1347
1374
  if (startsManagedLogicalRun) {
1348
1375
  this.#managedLogicalRunOwner = managedLogicalRunOwner;
1349
1376
  this.#emit({ type: "agent_start" });
@@ -1355,6 +1382,7 @@ export class Agent {
1355
1382
  this.#state.isStreaming = false;
1356
1383
  this.#abortController = undefined;
1357
1384
  this.#activeRunId = undefined;
1385
+ this.#activeResourceRunId = undefined;
1358
1386
  this.#runningPrompt = undefined;
1359
1387
  this.#resolveRunningPrompt = undefined;
1360
1388
  resolve();
@@ -1428,6 +1456,7 @@ export class Agent {
1428
1456
  maxRetryDelayMs: this.#maxRetryDelayMs,
1429
1457
  requestMaxRetries: this.#requestMaxRetries,
1430
1458
  streamMaxRetries: this.#streamMaxRetries,
1459
+ streamFirstEventTimeoutMs: this.#streamFirstEventTimeoutMs,
1431
1460
  ...(fallbackManaged
1432
1461
  ? {
1433
1462
  fallbackManaged: true,
@@ -1451,6 +1480,8 @@ export class Agent {
1451
1480
  onResponse: this.#onResponse,
1452
1481
  onSseEvent: this.#onSseEvent,
1453
1482
  signal: abortController.signal,
1483
+ resourceLedger: this.resourceLedger,
1484
+ resourceRunId: this.#activeResourceRunId,
1454
1485
  getApiKey: this.getApiKey,
1455
1486
  getAuthCredentialType: this.getAuthCredentialType,
1456
1487
  getToolContext: this.#getToolContext,
@@ -1697,6 +1728,7 @@ export class Agent {
1697
1728
  this.#state.pendingToolCalls = new Set<string>();
1698
1729
  this.#abortController = undefined;
1699
1730
  this.#activeRunId = undefined;
1731
+ this.#activeResourceRunId = undefined;
1700
1732
  this.#activeFallbackManaged = false;
1701
1733
  this.#resolveRunningPrompt?.();
1702
1734
  this.#runningPrompt = undefined;
@@ -1755,8 +1787,13 @@ export class Agent {
1755
1787
  if (this.#terminalizedLogicalRunIds.size > 256) {
1756
1788
  this.#terminalizedLogicalRunIds.delete(this.#terminalizedLogicalRunIds.values().next().value!);
1757
1789
  }
1758
- beforeEvent?.();
1759
- if (event) this.#emit(event);
1790
+ try {
1791
+ beforeEvent?.();
1792
+ if (event) this.#emit(event);
1793
+ } finally {
1794
+ // Publish terminal lifecycle synchronously before sealing the stable handle.
1795
+ this.resourceLedger.seal(String(logicalRunId));
1796
+ }
1760
1797
  }
1761
1798
 
1762
1799
  #getAssistantTextLength(message: AgentMessage | null): number {
@@ -28,7 +28,7 @@ import {
28
28
  neutralizeResponsesInputControlTokens,
29
29
  normalizeResponsesToolCallId,
30
30
  } from "@gajae-code/ai/utils";
31
- import { $env, logger } from "@gajae-code/utils";
31
+ import { $credentialEnv, logger } from "@gajae-code/utils";
32
32
 
33
33
  const OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1";
34
34
 
@@ -81,7 +81,8 @@ function resolveOpenAiCompactEndpoint(model: Model, authCredentialType?: "api_ke
81
81
  return resolveOpenAiCodexCompactEndpoint(model.baseUrl);
82
82
  }
83
83
 
84
- const envBaseUrl = $env.OPENAI_BASE_URL?.trim();
84
+ // Trusted sources only: the compaction endpoint carries the OpenAI credential.
85
+ const envBaseUrl = $credentialEnv("OPENAI_BASE_URL");
85
86
  const configuredBaseUrl = model.baseUrl?.trim();
86
87
  const rawBase =
87
88
  authCredentialType === "oauth"
@@ -94,6 +95,11 @@ function resolveOpenAiCompactEndpoint(model: Model, authCredentialType?: "api_ke
94
95
  return `${normalizedBase}/v1/responses/compact`;
95
96
  }
96
97
 
98
+ /** Test seam: the compaction endpoint as resolved from trusted env. */
99
+ export function resolveOpenAiCompactEndpointForTest(model: Model, authCredentialType?: "api_key" | "oauth"): string {
100
+ return resolveOpenAiCompactEndpoint(model, authCredentialType);
101
+ }
102
+
97
103
  function resolveOpenAiCodexCompactEndpoint(baseUrl: string | undefined): string {
98
104
  const rawBase = baseUrl && baseUrl.length > 0 ? baseUrl : CODEX_BASE_URL;
99
105
  const normalizedBase = rawBase.endsWith("/") ? rawBase.slice(0, -1) : rawBase;