@gajae-code/agent-core 0.10.1 → 0.11.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,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.10.2] - 2026-07-14
6
+
7
+ ### Fixed
8
+
9
+ - Extended the gpt-5.6 `Request blocked (code=invalid_prompt)` fix to the compaction paths that bypass the streaming transport. Remote OpenAI compaction (`/responses/compact`, `compaction.remoteEnabled` default on — the "remote compact task" in openai/codex#32028) built its native `input` from reasoning signatures, verbatim history items, and message/tool text without neutralizing leaked Harmony control-token markers (e.g. `<|channel|>analysis`), so gpt-5.6 rejected the compaction request and, on retry, could escalate to account-level blocking. `requestOpenAiRemoteCompaction` now neutralizes reserved control tokens across the whole outgoing `input`, and the generic `requestRemoteCompaction` prompt/systemPrompt are neutralized too. Local summarization was already covered by the streaming-transport request-boundary fix.
10
+
11
+ ### Changed
12
+
13
+ - `AgentLoopConfig.maintainContext` now receives a required cancellation-aware lifecycle (`signal`, `awaitEventDrain(invocationSignal)`). Agent loops compose the run and maintenance-invocation signals and pass that single signal to EventStream's FIFO consumer-drain barrier, so cancellation removes the pending drain at its owner instead of racing an orphaned wait.
14
+
5
15
  ## [0.10.0] - 2026-07-12
6
16
 
7
17
  ### Fixed
@@ -5,11 +5,19 @@
5
5
  import { type Context, EventStream } from "@gajae-code/ai";
6
6
  import { type AgentRunCoverage, type AgentRunSummary } from "./run-collector";
7
7
  import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, StreamFn } from "./types";
8
+ /** Sentinel returned by the abort race in `streamAssistantResponse`. */
9
+ /**
10
+ * Defensive caps for a provisional managed attempt. These are intentionally
11
+ * well above ordinary streamed responses; they only bound memory when an
12
+ * upstream emits an unbounded event stream before the attempt can commit.
13
+ */
14
+ export declare const MANAGED_ATTEMPT_MAX_STAGED_EVENTS = 10000;
15
+ export declare const MANAGED_ATTEMPT_MAX_STAGED_BYTES: number;
8
16
  /**
9
17
  * Start an agent loop with a new prompt message.
10
18
  * The prompt is added to the context and events are emitted for it.
11
19
  */
12
- export declare function agentLoop(prompts: AgentMessage[], context: AgentContext, config: AgentLoopConfig, signal?: AbortSignal, streamFn?: StreamFn): EventStream<AgentEvent, AgentMessage[]>;
20
+ export declare function agentLoop(prompts: AgentMessage[], context: AgentContext, config: AgentLoopConfig, signal?: AbortSignal, streamFn?: StreamFn, emitManagedAgentStart?: boolean): EventStream<AgentEvent, AgentMessage[]>;
13
21
  /**
14
22
  * Continue an agent loop from the current context without adding a new message.
15
23
  * Used for retries - context already has user message or tool results.
@@ -18,7 +26,7 @@ export declare function agentLoop(prompts: AgentMessage[], context: AgentContext
18
26
  * via `convertToLlm`. If it doesn't, the LLM provider will reject the request.
19
27
  * This cannot be validated here since `convertToLlm` is only called once per turn.
20
28
  */
21
- export declare function agentLoopContinue(context: AgentContext, config: AgentLoopConfig, signal?: AbortSignal, streamFn?: StreamFn): EventStream<AgentEvent, AgentMessage[]>;
29
+ export declare function agentLoopContinue(context: AgentContext, config: AgentLoopConfig, signal?: AbortSignal, streamFn?: StreamFn, emitManagedAgentStart?: boolean): EventStream<AgentEvent, AgentMessage[]>;
22
30
  /**
23
31
  * Detailed-result handle returned by {@link agentLoopDetailed}. Adds the
24
32
  * run-level telemetry/coverage rollup to the existing `AgentMessage[]`
@@ -4,13 +4,16 @@
4
4
  import { type AssistantMessage, type AssistantMessageEvent, type CursorExecHandlers, type CursorToolResultHandler, type Effort, type ImageContent, type Message, type Model, type ProviderSessionState, type ServiceTier, type SimpleStreamOptions, type ThinkingBudgets, type ToolChoice } from "@gajae-code/ai";
5
5
  import type { AppendOnlyContextManager } from "./append-only-context";
6
6
  import type { HarmonyAuditEvent } from "./harmony-leak";
7
- import type { AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, StreamFn, ToolCallContext } from "./types";
7
+ import type { AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, ManagedLogicalRunId, RunTerminalRequest, StreamFn, ToolCallContext } from "./types";
8
8
  /**
9
9
  * Whether persisted history ends at a point where a new model turn can resume.
10
10
  * Assistant-ended histories require an in-memory queued message and are handled
11
11
  * separately by `Agent.continue()`.
12
12
  */
13
13
  export declare function canContinuePersistedHistory(messages: readonly AgentMessage[]): boolean;
14
+ export declare class ManagedCursorInvariantError extends Error {
15
+ constructor(message?: string);
16
+ }
14
17
  export declare class AgentBusyError extends Error {
15
18
  constructor(message?: string);
16
19
  }
@@ -170,6 +173,16 @@ export interface AgentOptions {
170
173
  }
171
174
  export interface AgentPromptOptions {
172
175
  toolChoice?: ToolChoice;
176
+ /** Disable transport replay; fallback accounting is owned by the caller. */
177
+ fallbackManaged?: boolean;
178
+ /** Called synchronously after this invocation claims the agent run, before asynchronous provider work. */
179
+ onRunAccepted?: () => void;
180
+ /** Called once immediately before every managed upstream request. */
181
+ nextFallbackAttempt?: AgentLoopConfig["nextFallbackAttempt"];
182
+ /** Called after a managed upstream request is accepted and committed. */
183
+ onManagedAttemptAccepted?: AgentLoopConfig["onManagedAttemptAccepted"];
184
+ /** Receives a discarded managed attempt without exposing assistant lifecycle events. */
185
+ onManagedAttemptOutcome?: AgentLoopConfig["onManagedAttemptOutcome"];
173
186
  }
174
187
  export declare class Agent {
175
188
  #private;
@@ -307,10 +320,11 @@ export declare class Agent {
307
320
  setAssistantMessageEventInterceptor(fn: ((message: AssistantMessage, event: AssistantMessageEvent) => void) | undefined): void;
308
321
  setOnBeforeYield(fn: (() => Promise<void> | void) | undefined): void;
309
322
  setShouldPause(fn: AgentLoopConfig["shouldPause"] | undefined): void;
323
+ setMaintainContext(fn: AgentLoopConfig["maintainContext"] | undefined): void;
310
324
  emitExternalEvent(event: AgentEvent): void;
311
325
  createExternalEventEmitterForCurrentRun(): ((event: AgentEvent) => void) | undefined;
312
326
  setSystemPrompt(v: string[]): void;
313
- setModel(m: Model): void;
327
+ setModel(m: Model | undefined): void;
314
328
  setThinkingLevel(l: Effort | undefined): void;
315
329
  setSteeringMode(mode: "all" | "one-at-a-time"): void;
316
330
  getSteeringMode(): "all" | "one-at-a-time";
@@ -391,6 +405,24 @@ export declare class Agent {
391
405
  */
392
406
  forceAbort(reason?: string): boolean;
393
407
  waitForIdle(): Promise<void>;
408
+ /** The active per-attempt run identifier. */
409
+ get activeRunId(): number | undefined;
410
+ /**
411
+ * Stable identifier for the active managed logical run, shared by every retry
412
+ * attempt. Pass this value to requestRunTerminal(); never retain activeRunId
413
+ * for managed terminal completion.
414
+ */
415
+ get currentManagedLogicalRunId(): ManagedLogicalRunId | undefined;
416
+ /**
417
+ * Request terminal completion through the single logical-run keyed finalizer.
418
+ *
419
+ * For managed runs, logicalRunId must be currentManagedLogicalRunId from any
420
+ * attempt in the retry chain. Non-managed runs use their activeRunId. Terminal
421
+ * requests with messages emit a committed message_start/message_end lifecycle
422
+ * for each diagnostic before agent_end. Requests without messages (such as
423
+ * cancellation) emit only agent_end.
424
+ */
425
+ requestRunTerminal(logicalRunId: ManagedLogicalRunId, request: RunTerminalRequest): boolean;
394
426
  reset(): void;
395
427
  /** Send a prompt with an AgentMessage */
396
428
  prompt(message: AgentMessage | AgentMessage[], options?: AgentPromptOptions): Promise<void>;
@@ -399,5 +431,5 @@ export declare class Agent {
399
431
  /**
400
432
  * Continue from current context (used for retries and resuming queued messages).
401
433
  */
402
- continue(): Promise<void>;
434
+ continue(options?: AgentPromptOptions): Promise<void>;
403
435
  }
@@ -100,9 +100,19 @@ export interface ModeChangeEntry extends SessionEntryBase {
100
100
  /** Optional mode-specific data (e.g. plan file path) */
101
101
  data?: Record<string, unknown>;
102
102
  }
103
+ export interface ConfiguredModelChainEntry extends SessionEntryBase {
104
+ type: "configured_model_chain";
105
+ role: string;
106
+ entries: readonly string[];
107
+ origin: string;
108
+ identity?: string;
109
+ explicitHead: boolean;
110
+ /** Whether this entry removes the configured chain for its role. */
111
+ cleared?: boolean;
112
+ }
103
113
  export interface CustomCompactionSessionEntries {
104
114
  }
105
- export type SessionEntry = SessionMessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | ServiceTierChangeEntry | CompactionEntry | BranchSummaryEntry | CustomEntry | CustomMessageEntry | LabelEntry | TtsrInjectionEntry | MCPToolSelectionEntry | SessionInitEntry | ModeChangeEntry | CustomCompactionSessionEntries[keyof CustomCompactionSessionEntries];
115
+ export type SessionEntry = SessionMessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | ServiceTierChangeEntry | CompactionEntry | BranchSummaryEntry | CustomEntry | CustomMessageEntry | LabelEntry | TtsrInjectionEntry | MCPToolSelectionEntry | SessionInitEntry | ModeChangeEntry | ConfiguredModelChainEntry | CustomCompactionSessionEntries[keyof CustomCompactionSessionEntries];
106
116
  export interface ReadonlySessionManager {
107
117
  getBranch(leafId?: string | null): SessionEntry[];
108
118
  getEntry(id: string): SessionEntry | undefined;
@@ -5,11 +5,76 @@ import type { AgentRunCoverage, AgentRunSummary } from "./run-collector";
5
5
  import type { AgentTelemetryConfig } from "./telemetry";
6
6
  /** Stream function - can return sync or Promise for async config lookup */
7
7
  export type StreamFn = (...args: Parameters<typeof streamSimple>) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
8
+ /** Stable identifier for a managed logical run, shared by all of its retry attempts. */
9
+ export type ManagedLogicalRunId = number;
10
+ /** Terminal completion requested for a logical run. */
11
+ export interface RunTerminalRequest {
12
+ stopReason: "cancelled" | "error" | "exhausted";
13
+ messages?: AgentMessage[];
14
+ }
15
+ /**
16
+ * Ownership token supplied when Agent invokes a retry continuation.
17
+ *
18
+ * A continuation MUST verify `isCurrent()` immediately before starting a
19
+ * follow-up invocation and abandon the retry when it returns false. The token
20
+ * becomes invalid when its originating run is force-aborted or superseded.
21
+ * Coding-agent retry continuations must accept this argument and must not call
22
+ * `agent.continue()` after ownership has been lost.
23
+ */
24
+ export interface ManagedAttemptContinuationOwnership {
25
+ /** Per-attempt run-loop id; use only for attempt-local ownership checks. */
26
+ readonly runId: number;
27
+ /** Stable managed logical-run id; use for all terminal completion requests. */
28
+ readonly logicalRunId: ManagedLogicalRunId;
29
+ readonly generation: number;
30
+ isCurrent(): boolean;
31
+ }
32
+ /** Runs after a discarded attempt is idle, only while its ownership token remains current. */
33
+ export type ManagedAttemptContinuation = (ownership: ManagedAttemptContinuationOwnership) => void | Promise<void>;
34
+ /** Decision returned by managed fallback policy for one provisional attempt. */
35
+ export type ManagedAttemptDecision = {
36
+ type: "retry";
37
+ continuation: ManagedAttemptContinuation;
38
+ } | {
39
+ type: "terminal";
40
+ terminal: RunTerminalRequest;
41
+ };
42
+ /** Structured result for one managed upstream invocation. */
43
+ export type ManagedAttemptOutcome = {
44
+ type: "retryable_discarded";
45
+ failure: {
46
+ message: AssistantMessage;
47
+ /** Exact provider transport facts, including retry headers, for fallback policy. */
48
+ transportFailure?: import("@gajae-code/ai").TransportFailureFacts;
49
+ };
50
+ } | {
51
+ type: "run_terminal";
52
+ reason: "cancelled" | "error" | "exhausted";
53
+ };
54
+ export type ManagedAttemptOutcomeHandler = (outcome: ManagedAttemptOutcome) => ManagedAttemptDecision | Promise<ManagedAttemptDecision>;
55
+ /**
56
+ * Outcome of a cooperative mid-run context-maintenance checkpoint (see
57
+ * {@link AgentLoopConfig.maintainContext}). Any value other than "not-needed"
58
+ * means the checkpoint mutated (or attempted to mutate) durable context, so the
59
+ * loop ends the current run without the lossy `agent_end` finalization and the
60
+ * maintenance owner resumes the run on the rewritten context.
61
+ */
62
+ export type MidRunMaintenanceOutcome = "not-needed" | "pruned" | "compacted" | "promoted" | "failed" | "aborted";
8
63
  /**
9
64
  * Configuration for the agent loop.
10
65
  */
11
66
  export interface AgentLoopConfig extends SimpleStreamOptions {
12
67
  model: Model;
68
+ /**
69
+ * Supplies a fresh opaque token at each concrete managed transport invocation.
70
+ * The callback runs at the stream boundary so controller accounting matches
71
+ * upstream request count, including multi-step tool turns.
72
+ */
73
+ nextFallbackAttempt?: (model: Model) => SimpleStreamOptions["fallbackAttempt"];
74
+ /** Called after a managed upstream request is accepted and committed. */
75
+ onManagedAttemptAccepted?: () => void | Promise<void>;
76
+ /** Receives a managed invocation outcome without publishing provisional lifecycle events. */
77
+ onManagedAttemptOutcome?: ManagedAttemptOutcomeHandler;
13
78
  /**
14
79
  * When to interrupt tool execution for steering messages.
15
80
  * - "immediate" = check after each tool call (default)
@@ -129,6 +194,29 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
129
194
  * Use this when tool availability or the system prompt can change mid-turn.
130
195
  */
131
196
  syncContextBeforeModelCall?: (context: AgentContext) => void | Promise<void>;
197
+ /**
198
+ * Cooperative mid-run context-maintenance checkpoint.
199
+ *
200
+ * Invoked at the top of every loop iteration AFTER pending tool-result /
201
+ * steering messages have been materialized into durable context and BEFORE
202
+ * {@link syncContextBeforeModelCall} and the model call. This is the only
203
+ * boundary where the full unsent context (tool results + dequeued steering)
204
+ * is already durable, so a long uninterrupted tool loop can be bounded here
205
+ * before it grows past the provider window.
206
+ *
207
+ * The callback owns the maintenance decision (prune / compact / promote) and
208
+ * receives the minimal cancellation-aware lifecycle: `signal` is the
209
+ * non-optional loop signal, and `awaitEventDrain(invocationSignal)` waits for
210
+ * prior event consumer bodies with loop and invocation cancellation composed.
211
+ * Any outcome other than "not-needed" ends the current run with
212
+ * `agent_end.stopReason === "maintenance"` (NOT the lossy pause / completed
213
+ * finalization); the callback's continuation owner resumes the run on the
214
+ * rewritten context.
215
+ */
216
+ maintainContext?: (context: AgentContext, lifecycle: {
217
+ signal: AbortSignal;
218
+ awaitEventDrain: (invocationSignal: AbortSignal) => Promise<void>;
219
+ }) => Promise<MidRunMaintenanceOutcome> | MidRunMaintenanceOutcome;
132
220
  /**
133
221
  * Optional transform applied to tool call arguments before execution.
134
222
  * Use for deobfuscating secrets or rewriting arguments.
@@ -308,7 +396,7 @@ export type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessag
308
396
  */
309
397
  export interface AgentState {
310
398
  systemPrompt: string[];
311
- model: Model;
399
+ model: Model | undefined;
312
400
  thinkingLevel?: Effort;
313
401
  tools: AgentTool<any>[];
314
402
  messages: AgentMessage[];
@@ -388,8 +476,10 @@ export type AgentEvent = {
388
476
  } | {
389
477
  type: "agent_end";
390
478
  messages: AgentMessage[];
391
- /** Indicates whether the loop ended normally or suspended at a pause checkpoint. */
392
- stopReason?: "completed" | "paused";
479
+ /** Indicates whether the loop ended normally, suspended, cancelled, or entered maintenance. */
480
+ stopReason?: "completed" | "paused" | "cancelled" | "maintenance";
481
+ /** Present iff `stopReason === "maintenance"`; the maintenance outcome. */
482
+ maintenanceOutcome?: MidRunMaintenanceOutcome;
393
483
  /** Present iff `AgentTelemetryConfig` was supplied on this run. */
394
484
  telemetry?: AgentRunSummary;
395
485
  coverage?: AgentRunCoverage;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/agent-core",
4
- "version": "0.10.1",
4
+ "version": "0.11.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.10.1",
36
- "@gajae-code/natives": "0.10.1",
37
- "@gajae-code/utils": "0.10.1",
35
+ "@gajae-code/ai": "0.11.0",
36
+ "@gajae-code/natives": "0.11.0",
37
+ "@gajae-code/utils": "0.11.0",
38
38
  "@opentelemetry/api": "^1.9.0"
39
39
  },
40
40
  "devDependencies": {