@gajae-code/agent-core 0.12.5 → 0.12.7

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,7 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.12.7] - 2026-07-31
6
+
7
+ ## [0.12.6] - 2026-07-31
8
+
5
9
  ## [0.12.5] - 2026-07-30
10
+ ### Fixed
11
+
12
+ - Proxy streams now fail closed when a `toolcall_end` event references missing or non-tool-call content instead of silently dropping the protocol violation and accepting a later terminal event.
6
13
 
7
14
  ## [0.12.4] - 2026-07-30
8
15
 
@@ -3,6 +3,7 @@
3
3
  * Transforms to Message[] only at the LLM call boundary.
4
4
  */
5
5
  import { type Context, EventStream } from "@gajae-code/ai";
6
+ import type { AttemptScope } from "./attempt-scope";
6
7
  import { type AgentRunCoverage, type AgentRunSummary } from "./run-collector";
7
8
  import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, StreamFn } from "./types";
8
9
  /** Sentinel returned by the abort race in `streamAssistantResponse`. */
@@ -17,7 +18,7 @@ export declare const MANAGED_ATTEMPT_MAX_STAGED_BYTES: number;
17
18
  * Start an agent loop with a new prompt message.
18
19
  * The prompt is added to the context and events are emitted for it.
19
20
  */
20
- export declare function agentLoop(prompts: AgentMessage[], context: AgentContext, config: AgentLoopConfig, signal?: AbortSignal, streamFn?: StreamFn, emitManagedAgentStart?: boolean): EventStream<AgentEvent, AgentMessage[]>;
21
+ export declare function agentLoop(prompts: AgentMessage[], context: AgentContext, config: AgentLoopConfig, signal?: AbortSignal, streamFn?: StreamFn, emitAgentStart?: boolean, initialScope?: AttemptScope): EventStream<AgentEvent, AgentMessage[]>;
21
22
  /**
22
23
  * Continue an agent loop from the current context without adding a new message.
23
24
  * Used for retries - context already has user message or tool results.
@@ -26,7 +27,7 @@ export declare function agentLoop(prompts: AgentMessage[], context: AgentContext
26
27
  * via `convertToLlm`. If it doesn't, the LLM provider will reject the request.
27
28
  * This cannot be validated here since `convertToLlm` is only called once per turn.
28
29
  */
29
- export declare function agentLoopContinue(context: AgentContext, config: AgentLoopConfig, signal?: AbortSignal, streamFn?: StreamFn, emitManagedAgentStart?: boolean): EventStream<AgentEvent, AgentMessage[]>;
30
+ export declare function agentLoopContinue(context: AgentContext, config: AgentLoopConfig, signal?: AbortSignal, streamFn?: StreamFn, emitAgentStart?: boolean, initialScope?: AttemptScope): EventStream<AgentEvent, AgentMessage[]>;
30
31
  /**
31
32
  * Hard work budget for one degraded snapshot: every visited node AND every
32
33
  * enumerated own key is debited against this budget before it is processed
@@ -3,8 +3,9 @@
3
3
  */
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
+ import type { AttemptScope } from "./attempt-scope";
6
7
  import type { HarmonyAuditEvent } from "./harmony-leak";
7
- import type { AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, ManagedLogicalRunId, RunResourceLedger, RunTerminalRequest, StreamFn, ToolCallContext } from "./types";
8
+ import type { AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, ManagedLogicalRunId, RunCancellationDomainBridge, RunResourceLedger, RunTerminalRequest, StreamFn, ToolCallContext } from "./types";
8
9
  /**
9
10
  * Whether persisted history ends at a point where a new model turn can resume.
10
11
  * Assistant-ended histories require an in-memory queued message and are handled
@@ -28,7 +29,7 @@ export interface AgentOptions {
28
29
  * Optional transform applied to context before convertToLlm.
29
30
  * Use for context pruning, injecting external context, etc.
30
31
  */
31
- transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
32
+ transformContext?: (messages: AgentMessage[], signal?: AbortSignal, scope?: AttemptScope) => Promise<AgentMessage[]>;
32
33
  /**
33
34
  * Steering mode: "all" = send all steering messages at once, "one-at-a-time" = one per turn
34
35
  */
@@ -177,8 +178,11 @@ export interface AgentPromptOptions {
177
178
  toolChoice?: ToolChoice;
178
179
  /** Disable transport replay; fallback accounting is owned by the caller. */
179
180
  fallbackManaged?: boolean;
181
+ /** Continue a cooperative maintenance checkpoint under its existing logical run and cancellation domain. */
182
+ maintenanceContinuation?: boolean;
180
183
  /** Called synchronously after this invocation claims the agent run, before asynchronous provider work. */
181
- onRunAccepted?: () => void;
184
+ /** Receives the immutable run handle as the first callback argument. */
185
+ onRunAccepted?: (...args: any[]) => void;
182
186
  /** Called once immediately before every managed upstream request. */
183
187
  nextFallbackAttempt?: AgentLoopConfig["nextFallbackAttempt"];
184
188
  /** Called after a managed upstream request is accepted and committed. */
@@ -194,6 +198,14 @@ export declare class Agent {
194
198
  #private;
195
199
  get intentTracing(): boolean;
196
200
  readonly resourceLedger: RunResourceLedger;
201
+ bindRunCancellationDomainBridge(bridge: RunCancellationDomainBridge, agentSessionClaimKey?: object): void;
202
+ /** Mint a side-attempt scope and its authority unregister function. */
203
+ mintSideAttemptScope(): {
204
+ scope: AttemptScope;
205
+ dispose: () => void;
206
+ };
207
+ /** Return the Agent-owned attempt scope authority for session record injection. */
208
+ getAttemptScopeAuthority(): import("./attempt-scope").AttemptScopeAuthority;
197
209
  streamFn: StreamFn;
198
210
  getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
199
211
  getAuthCredentialType?: (provider: string) => "api_key" | "oauth" | undefined;
@@ -416,7 +428,7 @@ export declare class Agent {
416
428
  * did not drain. The abandoned provider/tool stream may still settle later, so
417
429
  * #runLoop guards every state mutation with a run id.
418
430
  */
419
- forceAbort(reason?: string): boolean;
431
+ forceAbort(reason?: string, logicalRunId?: ManagedLogicalRunId | number): boolean;
420
432
  waitForIdle(): Promise<void>;
421
433
  /** The active per-attempt run identifier. */
422
434
  get activeRunId(): number | undefined;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Per-attempt scope identity for request-scoped execution attribution.
3
+ *
4
+ * An AttemptScope is an immutable, frozen value allocated before every
5
+ * observable lifecycle emission for a single provider/agent attempt.
6
+ * It carries a stable `attemptId`, a monotonic `generation` (per-lineage),
7
+ * and a `lineage` discriminator that distinguishes the main attempt from
8
+ * concurrent side attempts (IRC background, ephemeral/btw turns).
9
+ *
10
+ * The `attemptId` + `generation` + `lineage` form the comparable identity.
11
+ * AttemptScope is structurally assignable to AttemptScopeRef in
12
+ * `packages/ai` so it can be carried through `SimpleStreamOptions` and
13
+ * provider hook signatures without a reverse dependency.
14
+ */
15
+ export type AttemptLineage = "main" | `side:${string}`;
16
+ export interface AttemptScope {
17
+ readonly attemptId: string;
18
+ readonly generation: number;
19
+ readonly lineage: AttemptLineage;
20
+ }
21
+ export declare function attemptScopesEqual(a: AttemptScope, b: AttemptScope): boolean;
22
+ /**
23
+ * Per-lineage currentness authority. Main and side attempts have separate
24
+ * instances so a side attempt never invalidates the main scope, and
25
+ * `forceAbort` advances only the main lineage.
26
+ */
27
+ export interface LineageCurrentness {
28
+ readonly lineage: AttemptLineage;
29
+ /** True iff no successor scope with a greater generation was allocated in this lineage. */
30
+ isCurrent(scope: AttemptScope): boolean;
31
+ /** Allocate the next generation for the given attempt identity in this lineage. */
32
+ advance(attemptId: string): number;
33
+ /** Allocate the next generation in this lineage. */
34
+ /** Current generation value for this lineage. */
35
+ readonly current: number;
36
+ }
37
+ export declare function createLineageCurrentness(lineage: AttemptLineage): LineageCurrentness;
38
+ /**
39
+ * Agent-owned authority over all attempt lineages. Owns the main lineage;
40
+ * side lineages are registered/removed with bounded lifecycle.
41
+ *
42
+ * This is the SINGLE source of currentness truth injected into
43
+ * AttemptRecordStore (packages/coding-agent). Every store operation
44
+ * calls `authority.isCurrent(scope)` and fails closed when the authority
45
+ * is missing or the scope is superseded.
46
+ */
47
+ export interface AttemptScopeAuthority {
48
+ /** Register a side-lineage authority. Returns an unregister function. */
49
+ registerSide(lineage: AttemptLineage, auth: LineageCurrentness): () => void;
50
+ /** True iff the scope's lineage is known and its generation is current. */
51
+ isCurrent(scope: AttemptScope): boolean;
52
+ /** Advance the main lineage (called by forceAbort). Returns the new generation. */
53
+ advanceMain(): number;
54
+ /** Mint the next main-lineage scope. */
55
+ mintMain(): AttemptScope;
56
+ /**
57
+ * Atomically register a fresh side lineage, mint a side scope, and return
58
+ * both the scope and a dispose function. The authority knows the lineage
59
+ * BEFORE the scope is returned, so `isCurrent` succeeds immediately.
60
+ */
61
+ mintSide(): {
62
+ scope: AttemptScope;
63
+ dispose: () => void;
64
+ };
65
+ }
66
+ export interface AttemptMinter {
67
+ mint(lineage: AttemptLineage): AttemptScope;
68
+ }
69
+ export declare function createAttemptMinter(): AttemptMinter;
70
+ /**
71
+ * Create the Agent-owned authority. Owns the main lineage and a bounded
72
+ * (LRU-capped) map of side lineages. Only RETIRED side authorities are
73
+ * eligible for LRU eviction; a live side attempt is never silently
74
+ * invalidated by a newer side registration.
75
+ */
76
+ export declare function createAttemptScopeAuthority(): AttemptScopeAuthority;
77
+ /**
78
+ * Immutable per-run attempt handle, carried through terminal/finalizer paths.
79
+ * Keyed by logicalRunId in the Agent's `#runHandles` map.
80
+ */
81
+ export interface AttemptRunHandle {
82
+ readonly logicalRunId: number | import("./types.js").ManagedLogicalRunId;
83
+ readonly scope: AttemptScope;
84
+ }
@@ -1,5 +1,6 @@
1
1
  import type { AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream, Effort, ImageContent, Message, Model, SimpleStreamOptions, Static, streamSimple, TextContent, Tool, ToolChoice, ToolResultMessage, TransportFailureFacts, TSchema } from "@gajae-code/ai";
2
2
  import type { AppendOnlyContextManager } from "./append-only-context";
3
+ import type { AttemptMinter, AttemptRunHandle, AttemptScope } from "./attempt-scope";
3
4
  import type { HarmonyAuditEvent } from "./harmony-leak";
4
5
  import type { AgentRunCoverage, AgentRunSummary } from "./run-collector";
5
6
  import type { AgentTelemetryConfig } from "./telemetry";
@@ -15,15 +16,94 @@ export interface RunResourceEntry {
15
16
  label: string;
16
17
  registeredAt: number;
17
18
  }
19
+ export type RunSettlementReason = "unknown_run" | "run_not_sealed" | "resources_pending" | "quarantined";
18
20
  export type RunSettlementProof = {
19
21
  status: "settled";
20
22
  } | {
21
23
  status: "unfenced";
24
+ reason: RunSettlementReason;
22
25
  pending: RunResourceEntry[];
23
26
  };
27
+ export interface RunCancellationDomain {
28
+ readonly resourceRunId: string;
29
+ readonly signal: AbortSignal;
30
+ }
31
+ export interface RunCancellationDomainBridge {
32
+ open(resourceRunId: string): {
33
+ ok: true;
34
+ domain: RunCancellationDomain;
35
+ created: boolean;
36
+ } | {
37
+ ok: false;
38
+ reason: "duplicate_identity" | "quarantined";
39
+ };
40
+ lookup(resourceRunId: string): RunCancellationDomain | undefined;
41
+ abort(resourceRunId: string, reason?: unknown): {
42
+ ok: true;
43
+ newlyAborted: boolean;
44
+ } | {
45
+ ok: false;
46
+ reason: "unknown_run" | "quarantined";
47
+ };
48
+ release(resourceRunId: string, disposition: "settled" | "quarantined"): void;
49
+ }
50
+ export type ReserveProducerResult = {
51
+ ok: true;
52
+ lease: RunResourceProducerLease;
53
+ } | {
54
+ ok: false;
55
+ reason: "unknown_run" | "sealed" | "quarantined" | "domain_mismatch";
56
+ };
57
+ export type ClaimProducerResult = {
58
+ ok: true;
59
+ lease: RunResourceProducerLease;
60
+ } | {
61
+ ok: false;
62
+ reason: "already_claimed" | "handle_mismatch" | "domain_mismatch" | "closed" | "quarantined";
63
+ };
64
+ export type ForkProducerResult = {
65
+ ok: true;
66
+ lease: RunResourceProducerLease;
67
+ } | {
68
+ ok: false;
69
+ reason: "parent_closed" | "quarantined" | "domain_mismatch";
70
+ };
71
+ export interface RunResourceProducerLease {
72
+ readonly resourceRunId: string;
73
+ readonly domain: RunCancellationDomain;
74
+ readonly signal: AbortSignal;
75
+ track(kind: RunResourceKind, label: string, settled: PromiseLike<unknown>): boolean;
76
+ fork(expectedDomain: RunCancellationDomain, kind: RunResourceKind, label: string): ForkProducerResult;
77
+ closeDiscovery(): void;
78
+ }
79
+ export interface AgentTerminalOwnerContext {
80
+ readonly resourceRunId: string;
81
+ readonly domain: RunCancellationDomain;
82
+ }
83
+ export declare function setAgentTerminalOwnerContext(event: object, context: AgentTerminalOwnerContext): void;
84
+ export declare function getAgentTerminalOwnerContext(event: object): AgentTerminalOwnerContext | undefined;
85
+ export interface StandaloneRunOwnership {
86
+ readonly resourceRunId: string;
87
+ readonly domain: RunCancellationDomain;
88
+ claimContinuation(): {
89
+ ok: true;
90
+ ownership: StandaloneRunOwnership;
91
+ } | {
92
+ ok: false;
93
+ reason: "already_claimed" | "terminal" | "quarantined";
94
+ };
95
+ abandon(reason: "cancelled" | "error"): void;
96
+ }
24
97
  export interface RunResourceLedger {
98
+ /** Bind the bridge once, before any logical run may be opened. */
99
+ bindCancellationDomainBridge(bridge: RunCancellationDomainBridge): void;
100
+ /** Bind the unforgeable AgentSession claim key once, before terminal publication. */
101
+ bindAgentSessionClaimKey(key: object): void;
25
102
  /** Reserve a run handle before publishing its `agent_start` event. */
26
- open(resourceRunId: string): void;
103
+ open(resourceRunId: string): RunCancellationDomain | undefined;
104
+ lookupDomain(resourceRunId: string): RunCancellationDomain | undefined;
105
+ reserveProducer(resourceRunId: string, expectedDomain: RunCancellationDomain | undefined, kind: RunResourceKind, label: string): ReserveProducerResult;
106
+ claimProducer(resourceRunId: string, expectedDomain: RunCancellationDomain | undefined, ownerKey: object): ClaimProducerResult;
27
107
  track(resourceRunId: string, kind: RunResourceKind, label: string, settled: PromiseLike<unknown>): void;
28
108
  pending(resourceRunId: string): RunResourceEntry[];
29
109
  /** Seal a run after terminal event publication; only sealed empty runs settle. */
@@ -54,6 +134,10 @@ export interface ManagedAttemptContinuationOwnership {
54
134
  /** Stable managed logical-run id; use for all terminal completion requests. */
55
135
  readonly logicalRunId: ManagedLogicalRunId;
56
136
  readonly generation: number;
137
+ readonly domain: RunCancellationDomain;
138
+ readonly lease: RunResourceProducerLease;
139
+ /** Immutable per-attempt handle used by terminalizers and continuations. */
140
+ readonly handle: AttemptRunHandle;
57
141
  isCurrent(): boolean;
58
142
  }
59
143
  /** Runs after a discarded attempt is idle, only while its ownership token remains current. */
@@ -77,12 +161,15 @@ export type ManagedAttemptOutcome = {
77
161
  /** Exact provider transport facts, including retry headers, for fallback policy. */
78
162
  transportFailure?: TransportFailureFacts;
79
163
  };
164
+ scope?: AttemptScope;
80
165
  } | {
81
166
  type: "context_overflow_discarded";
82
167
  message: AssistantMessage;
168
+ scope?: AttemptScope;
83
169
  } | {
84
170
  type: "run_terminal";
85
171
  reason: "cancelled" | "error" | "exhausted";
172
+ scope?: AttemptScope;
86
173
  };
87
174
  export type ManagedAttemptOutcomeHandler = (outcome: ManagedAttemptOutcome) => ManagedAttemptDecision | Promise<ManagedAttemptDecision>;
88
175
  /**
@@ -108,6 +195,10 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
108
195
  onManagedAttemptAccepted?: () => void | Promise<void>;
109
196
  /** Receives a managed invocation outcome without publishing provisional lifecycle events. */
110
197
  onManagedAttemptOutcome?: ManagedAttemptOutcomeHandler;
198
+ /** Per-attempt scope allocator for direct loop callers. */
199
+ attemptMinter?: AttemptMinter;
200
+ /** Scope allocated by the owning Agent for the first attempt in this loop. */
201
+ initialScope?: AttemptScope;
111
202
  /**
112
203
  * When to interrupt tool execution for steering messages.
113
204
  * - "immediate" = check after each tool call (default)
@@ -175,7 +266,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
175
266
  * }
176
267
  * ```
177
268
  */
178
- transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
269
+ transformContext?: (messages: AgentMessage[], signal?: AbortSignal, scope?: AttemptScope) => Promise<AgentMessage[]>;
179
270
  /**
180
271
  * Resolves an API key dynamically for each LLM call.
181
272
  *
@@ -336,6 +427,12 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
336
427
  resourceLedger?: RunResourceLedger;
337
428
  /** Stable resource ownership identifier for this prompt run. */
338
429
  resourceRunId?: string;
430
+ /** Immutable logical cancellation domain bound by the resource ledger. */
431
+ resourceCancellationDomain?: RunCancellationDomain;
432
+ /** Agent passes caller ownership; direct loop callers retain loop-owned sealing. */
433
+ resourceSealOwner?: "caller" | "loop";
434
+ /** Opaque ownership required to resume a standalone maintenance lifecycle. */
435
+ standaloneRunOwnership?: StandaloneRunOwnership;
339
436
  }
340
437
  /**
341
438
  * Batch/sequencing metadata for the tool call currently being processed.
@@ -465,6 +562,8 @@ export interface RenderResultOptions {
465
562
  * Apps can extend via declaration merging.
466
563
  */
467
564
  export interface AgentToolContext {
565
+ /** Per-attempt scope used to attribute tool lifecycle and extension delivery. */
566
+ attemptScope?: AttemptScope;
468
567
  }
469
568
  export type AgentToolExecFn<TParameters extends TSchema = TSchema, TDetails = any, TTheme = unknown> = (this: AgentTool<TParameters, TDetails, TTheme>, toolCallId: string, params: Static<TParameters>, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>, context?: AgentToolContext) => Promise<AgentToolResult<TDetails, TParameters>>;
470
569
  export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any, TTheme = unknown> extends Tool<TParameters> {
@@ -513,6 +612,7 @@ export interface AgentContext {
513
612
  */
514
613
  export type AgentEvent = {
515
614
  type: "agent_start";
615
+ scope?: AttemptScope;
516
616
  } | {
517
617
  type: "agent_end";
518
618
  messages: AgentMessage[];
@@ -523,38 +623,47 @@ export type AgentEvent = {
523
623
  /** Present iff `AgentTelemetryConfig` was supplied on this run. */
524
624
  telemetry?: AgentRunSummary;
525
625
  coverage?: AgentRunCoverage;
626
+ scope?: AttemptScope;
526
627
  } | {
527
628
  type: "turn_start";
629
+ scope?: AttemptScope;
528
630
  } | {
529
631
  type: "turn_end";
530
632
  message: AgentMessage;
531
633
  toolResults: ToolResultMessage[];
634
+ scope?: AttemptScope;
532
635
  } | {
533
636
  type: "message_start";
534
637
  message: AgentMessage;
638
+ scope?: AttemptScope;
535
639
  } | {
536
640
  type: "message_update";
537
641
  message: AgentMessage;
538
642
  assistantMessageEvent: AssistantMessageEvent;
643
+ scope?: AttemptScope;
539
644
  } | {
540
645
  type: "message_end";
541
646
  message: AgentMessage;
647
+ scope?: AttemptScope;
542
648
  } | {
543
649
  type: "tool_execution_start";
544
650
  toolCallId: string;
545
651
  toolName: string;
546
652
  args: any;
547
653
  intent?: string;
654
+ scope?: AttemptScope;
548
655
  } | {
549
656
  type: "tool_execution_update";
550
657
  toolCallId: string;
551
658
  toolName: string;
552
659
  args: any;
553
660
  partialResult: any;
661
+ scope?: AttemptScope;
554
662
  } | {
555
663
  type: "tool_execution_end";
556
664
  toolCallId: string;
557
665
  toolName: string;
558
666
  result: any;
559
667
  isError?: boolean;
668
+ scope?: AttemptScope;
560
669
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/agent-core",
4
- "version": "0.12.5",
4
+ "version": "0.12.7",
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.12.5",
36
- "@gajae-code/natives": "0.12.5",
37
- "@gajae-code/utils": "0.12.5",
35
+ "@gajae-code/ai": "0.12.7",
36
+ "@gajae-code/natives": "0.12.7",
37
+ "@gajae-code/utils": "0.12.7",
38
38
  "@opentelemetry/api": "^1.9.0"
39
39
  },
40
40
  "devDependencies": {