@gajae-code/agent-core 0.13.3 → 0.14.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 +21 -0
- package/dist/types/agent-loop.d.ts +2 -1
- package/dist/types/agent.d.ts +36 -5
- package/dist/types/index.d.ts +1 -0
- package/dist/types/tool-dispatch-identity.d.ts +27 -0
- package/dist/types/types.d.ts +11 -0
- package/package.json +4 -4
- package/src/agent-loop.ts +805 -161
- package/src/agent.ts +116 -18
- package/src/index.ts +2 -0
- package/src/proxy.ts +3 -1
- package/src/tool-dispatch-identity.ts +87 -0
- package/src/types.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.14.0] - 2026-08-17
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Non-managed lossless response staging now commits its buffered lifecycle and switches to ordinary pass-through publication when the provisional event cap is reached, instead of turning a large reasoning-only response into a fatal `local_snapshot_failure`. Managed fallback attempts keep the strict bounded-buffer rejection required for atomic retry and provider-fallback isolation.
|
|
10
|
+
- Managed snapshot machinery no longer fails runs on benign payload-class or readable-proxy roots: an assistant message or stream event whose fields live on prototype getters (which `structuredClone` drops — it copies only own enumerable properties) or behind a proxy whose gets are readable is repaired through the existing guarded-read path instead of throwing a deterministic `shell.role`/`event.unknownType`/`event.snapshot` local snapshot failure. The run-loop message_update replay also builds its event through the managed event snapshot instead of a naive `{ ...event }` spread, which silently dropped prototype-carried fields before the snapshot boundary could see them. Hostile shapes (throwing get traps, sentinel-marked degraded content, malformed non-string event types) keep their named fail-fast diagnostics with no retry authority.
|
|
11
|
+
|
|
12
|
+
- Managed fallback now validates and byte-measures the detached event snapshot rather than trusting the live payload's JSON result. Custom payload classes whose prototype `toJSON()` hides bigint state are sanitized after `structuredClone` removes that serializer, so every accepted snapshot stays detached, JSON-serializable, and bounded; residual typed `local_snapshot_failure` diagnostics remain outside provider fallback authority and surface without deterministic retry amplification.
|
|
13
|
+
- Managed fallback buffer overflows now retain a typed `local_buffer_overflow` error kind on the terminal assistant message, so session retry policy surfaces them immediately without provider-fallback attribution instead of admitting them to the bounded `unknown` retry class.
|
|
14
|
+
- Managed local-failure diagnostics: `ManagedAttemptSnapshotError` and `ManagedAttemptBufferOverflowError` now carry a stable `stage` discriminator naming the exact rejecting site (`shell.role`, `shell.content`, `event.snapshot`, `event.contentIndex`, `event.delta`, `event.content`, `event.toolcall`, `event.done.reason`, `event.error.reason`, `event.unknownType`, `staging.losslessSnapshot`, `staging.measure`, `staging.sanitize`, `staging.overflow`, `overflow.preMeasure`, `overflow.staged`), and the run-loop failure boundary emits ONE bounded shape-only `logger.warn` per stream invocation (stage, error kind, model, provider, snapshot mode, staged event count/bytes, and content block count for the content stage). The diagnostic is gated on the module-private local error identities and its stage is whitelisted against the closed vocabulary, so neither a foreign error that self-labels a local failure kind nor an in-module regression can route arbitrary text into the log; it never records raw text, thinking, tool arguments, or any provider payload, and the user-facing message string is unchanged so session-side classification keeps matching. Previously all 14 rejecting sites shared one static message, leaving no way to identify which provider shape a normalizer must be taught to accept.
|
|
15
|
+
- A turn whose tool arguments arrive flagged `escapedNonAsciiArguments` is now resampled instead of being reported as a tool failure: the defective assistant turn is dropped from history and the request is re-issued, up to twice per turn, before the terminal per-call rejection takes over. Hand-spelled `\uXXXX` arguments decode into valid-looking but silently wrong text (observed as garbled Hangul in `ask` prompts) and no post-parse repair can recover them, but the defect is a wire-format accident that resampling clears - surfacing it as a tool error instead burned the whole turn and fed the literal escape syntax back into the context the model samples from next. Scoped to the non-managed session path, matching the existing `invalid_prompt` and reasoning-content repairs; managed fallback keeps owning its own retry policy.
|
|
16
|
+
- Visible-text Harmony leak retries now close the already-published assistant lifecycle with an empty aborted terminal stripped of raw provider payload before contaminated history is removed and a replacement request begins, preventing both orphaned streaming updates and leaked control text in durable history or replay.
|
|
17
|
+
- Unmanaged escaped-non-ASCII resampling now stages a detached, provider-metadata-preserving assistant lifecycle until validation, publishes live safety updates before dispatch, and defers terminal `message_end` publication until subscriber-triggered cancellation is resolved so persisted assistant state and aborted tool-result pairing cannot disagree.
|
|
18
|
+
- The agent loop still rejects a tool call flagged `escapedNonAsciiArguments` before execution once the resample budget is spent, with a retryable error telling the model to re-issue the call writing non-ASCII characters literally.
|
|
19
|
+
- Managed fallback attempt snapshots no longer fail the whole run on benign provider shape variations: an assistant message whose `content` is a bare string or is missing now degrades to an empty content array, and staged assistant events with out-of-vocabulary `done`/`error` reasons or an unknown string `type` degrade to schema-valid values instead of throwing a non-retryable `ManagedAttemptSnapshotError`. This matches the closed `StopReason` vocabulary already normalized elsewhere in the shell. Object-shaped and other exotic non-array `content` stays fail-closed under the named `shell.content` diagnostic, as does sanitizer-sentinel string content (`[unserializable]`/`[accessor]`/`[truncated]`/`[Circular]`, which mark a non-cloneable original value such as a proxy-wrapped content array rather than provider string variance — degrading those would silently drop real content behind a successful empty turn), and hostile inputs keep failing fast with no retry authority: a live proxy root, a throwing `get`/`getOwnPropertyDescriptor` trap, and a non-string event `type` all remain local snapshot failures.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Reassignable `onFollowUpConsumed` hook on `Agent`: invoked with the follow-up messages the loop dequeues for the next turn, so consumers can attach per-turn state (e.g. a fresh owned-completion lineage) at actual resume admission.
|
|
24
|
+
- `AgentPromptOptions.onRunAccepted` now receives a typed acceptance payload containing `consumedQueuedMessages`, allowing consumers to bind ownership and other per-message state only to the queued messages actually claimed by the accepted run.
|
|
5
25
|
## [0.13.3] - 2026-08-15
|
|
6
26
|
|
|
7
27
|
### Fixed
|
|
@@ -37,6 +57,7 @@
|
|
|
37
57
|
### Fixed
|
|
38
58
|
|
|
39
59
|
- An aborted run whose tool ignores its `AbortSignal` now terminates on its own (#3894). `Promise.allSettled` waited on the unresolved call forever, so the turn only ended when the session's force-abort budget expired; the loop now emits a synthetic aborted result for the outstanding calls and `waitForIdle` settles immediately. Session dispose consequently reaches idle through the cooperative path instead of force-invalidating the run.
|
|
60
|
+
|
|
40
61
|
### Changed
|
|
41
62
|
|
|
42
63
|
- Telemetry configured with `spans: false` now skips span and attribute construction while preserving usage and cost hooks.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Agent loop that works with AgentMessage throughout.
|
|
3
3
|
* Transforms to Message[] only at the LLM call boundary.
|
|
4
4
|
*/
|
|
5
|
-
import { type Context, EventStream } from "@gajae-code/ai";
|
|
5
|
+
import { type AssistantMessage, type AssistantMessageEvent, type Context, EventStream } from "@gajae-code/ai";
|
|
6
6
|
import type { AttemptScope } from "./attempt-scope";
|
|
7
7
|
import { type AgentRunCoverage, type AgentRunSummary } from "./run-collector";
|
|
8
8
|
import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, StreamFn } from "./types";
|
|
@@ -72,6 +72,7 @@ export declare const MANAGED_SNAPSHOT_MAX_NODES = 100000;
|
|
|
72
72
|
* callers use the default budget via {@link managedAttemptSnapshot}.
|
|
73
73
|
*/
|
|
74
74
|
export declare function sanitizedDetachedClone<T>(value: T, maxNodes?: number): T;
|
|
75
|
+
export declare function managedAssistantEventSnapshot(event: AssistantMessageEvent, message: AssistantMessage): AssistantMessageEvent;
|
|
75
76
|
/**
|
|
76
77
|
* Detailed-result handle returned by {@link agentLoopDetailed}. Adds the
|
|
77
78
|
* run-level telemetry/coverage rollup to the existing `AgentMessage[]`
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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
|
+
import type { AttemptRunHandle, AttemptScope } from "./attempt-scope";
|
|
7
7
|
import type { HarmonyAuditEvent } from "./harmony-leak";
|
|
8
8
|
import type { AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, ManagedLogicalRunId, RunCancellationDomainBridge, RunResourceLedger, RunTerminalRequest, StreamFn, ToolCallContext } from "./types";
|
|
9
9
|
/**
|
|
@@ -162,6 +162,8 @@ export interface AgentOptions {
|
|
|
162
162
|
* message are emitted. See {@link AgentLoopConfig.afterToolCall} for full semantics.
|
|
163
163
|
*/
|
|
164
164
|
afterToolCall?: AgentLoopConfig["afterToolCall"];
|
|
165
|
+
/** Invoked with the follow-up messages dequeued for the next turn (reassignable). */
|
|
166
|
+
onFollowUpConsumed?: AgentLoopConfig["onFollowUpConsumed"];
|
|
165
167
|
/**
|
|
166
168
|
* Opt-in OpenTelemetry instrumentation. Passing `{}` enables the loop's
|
|
167
169
|
* GenAI-semantic-convention spans using the global tracer provider. See
|
|
@@ -181,8 +183,9 @@ export interface AgentPromptOptions {
|
|
|
181
183
|
/** Continue a cooperative maintenance checkpoint under its existing logical run and cancellation domain. */
|
|
182
184
|
maintenanceContinuation?: boolean;
|
|
183
185
|
/** Called synchronously after this invocation claims the agent run, before asynchronous provider work. */
|
|
184
|
-
|
|
185
|
-
|
|
186
|
+
onRunAccepted?: (handle: AttemptRunHandle, acceptance: {
|
|
187
|
+
consumedQueuedMessages: readonly AgentMessage[];
|
|
188
|
+
}) => void;
|
|
186
189
|
/** Called once immediately before every managed upstream request. */
|
|
187
190
|
nextFallbackAttempt?: AgentLoopConfig["nextFallbackAttempt"];
|
|
188
191
|
/** Called after a managed upstream request is accepted and committed. */
|
|
@@ -224,6 +227,8 @@ export declare class Agent {
|
|
|
224
227
|
* message emission. Reassign at any time to swap the implementation.
|
|
225
228
|
*/
|
|
226
229
|
afterToolCall?: AgentLoopConfig["afterToolCall"];
|
|
230
|
+
/** Invoked with the follow-up messages dequeued for the next turn. Reassign at any time. */
|
|
231
|
+
onFollowUpConsumed?: AgentLoopConfig["onFollowUpConsumed"];
|
|
227
232
|
constructor(opts?: AgentOptions);
|
|
228
233
|
/**
|
|
229
234
|
* Get the current session ID used for provider caching.
|
|
@@ -344,9 +349,22 @@ export declare class Agent {
|
|
|
344
349
|
setProviderResponseInterceptor(fn: SimpleStreamOptions["onResponse"] | undefined): void;
|
|
345
350
|
setRawSseEventInterceptor(fn: SimpleStreamOptions["onSseEvent"] | undefined): void;
|
|
346
351
|
setAssistantMessageEventInterceptor(fn: ((message: AssistantMessage, event: AssistantMessageEvent) => void) | undefined): void;
|
|
352
|
+
setProvisionalAssistantMessageEventInterceptor(fn: ((message: AssistantMessage, event: AssistantMessageEvent) => void) | undefined): void;
|
|
347
353
|
setOnBeforeYield(fn: (() => Promise<void> | void) | undefined): void;
|
|
348
354
|
setShouldPause(fn: AgentLoopConfig["shouldPause"] | undefined): void;
|
|
349
355
|
setMaintainContext(fn: AgentLoopConfig["maintainContext"] | undefined): void;
|
|
356
|
+
/**
|
|
357
|
+
* Publish an event produced OUTSIDE the agent loop (a provider that executed the tool
|
|
358
|
+
* itself, a host bridge, a replay).
|
|
359
|
+
*
|
|
360
|
+
* Identity is the PRODUCER's to prove: whoever dispatched the call binds the tool object
|
|
361
|
+
* it actually ran (see `bindDispatchedToolIdentity`) before handing the event here, and
|
|
362
|
+
* that binding is never touched from this side. Re-resolving `event.toolName` against the
|
|
363
|
+
* mutable current tool list would let a mid-run `setTools`, MCP reload, or plain name
|
|
364
|
+
* collision overwrite a proven object with one that never ran — and would invent an
|
|
365
|
+
* identity for replays and host bridges that never executed an AgentTool at all. An
|
|
366
|
+
* unbound external event stays unbound; unproven provenance is `custom`.
|
|
367
|
+
*/
|
|
350
368
|
emitExternalEvent(event: AgentEvent): void;
|
|
351
369
|
createExternalEventEmitterForCurrentRun(): ((event: AgentEvent) => void) | undefined;
|
|
352
370
|
setSystemPrompt(v: string[]): void;
|
|
@@ -425,8 +443,21 @@ export declare class Agent {
|
|
|
425
443
|
popLastFollowUp(): AgentMessage | undefined;
|
|
426
444
|
removeFollowUpAt(index: number): AgentMessage | undefined;
|
|
427
445
|
moveFollowUp(fromIndex: number, toIndex: number): boolean;
|
|
428
|
-
/**
|
|
429
|
-
|
|
446
|
+
/**
|
|
447
|
+
* Remove ALL queued STEERING messages without touching the follow-up queue.
|
|
448
|
+
* Used by the terminal-abort path to purge steering queued for the aborted
|
|
449
|
+
* turn (the loop may exit on the abort signal without polling it); the
|
|
450
|
+
* follow-up queue is preserved because it may carry owned-completion
|
|
451
|
+
* resumes that must still deliver.
|
|
452
|
+
*/
|
|
453
|
+
clearSteeringMessages(): void;
|
|
454
|
+
/**
|
|
455
|
+
* Remove queued steering/follow-up messages matching `predicate`, preserving
|
|
456
|
+
* order of the rest. `scope` restricts the removal to one queue — the
|
|
457
|
+
* terminal-abort steering purge must not wipe the follow-up queue, which
|
|
458
|
+
* the owned-completion resume policy preserves.
|
|
459
|
+
*/
|
|
460
|
+
removeQueuedMessages(predicate: (message: AgentMessage) => boolean, scope?: "both" | "steering" | "followUp"): {
|
|
430
461
|
steering: number;
|
|
431
462
|
followUp: number;
|
|
432
463
|
total: number;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record, at the producer boundary, the tool object this event was emitted for.
|
|
3
|
+
*
|
|
4
|
+
* A call with no resolved tool (an unknown name the loop is about to reject, or a call
|
|
5
|
+
* aborted before dispatch) binds nothing: there is no object, so there is nothing to prove.
|
|
6
|
+
*/
|
|
7
|
+
export declare function bindDispatchedToolIdentity(event: object, tool: object | undefined): void;
|
|
8
|
+
/** The tool object this event was actually dispatched to, if its producer bound one. */
|
|
9
|
+
export declare function dispatchedToolIdentity(event: object): object | undefined;
|
|
10
|
+
/** Mark an event as pairing-only: emitted for a call the loop never dispatched. */
|
|
11
|
+
export declare function markNonDispatchedToolEvent(event: object): void;
|
|
12
|
+
/** Whether this exact event was synthesized for a call that never ran. */
|
|
13
|
+
export declare function isNonDispatchedToolEvent(event: object): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Active tool a call name dispatches to. Tools emitted via OpenAI's custom-tool path
|
|
16
|
+
* (e.g. `apply_patch` on GPT-5) come back under their wire-level name, which may differ
|
|
17
|
+
* from the harness-internal `name`. Match on either, preferring `name` for determinism if
|
|
18
|
+
* both somehow collide.
|
|
19
|
+
*
|
|
20
|
+
* This is the single dispatch-matching rule: execution, external-event identity binding,
|
|
21
|
+
* and every "is this tool callable" check must agree, or a label can describe a tool the
|
|
22
|
+
* call would never have reached.
|
|
23
|
+
*/
|
|
24
|
+
export declare function activeToolForCallName<T extends {
|
|
25
|
+
name: string;
|
|
26
|
+
customWireName?: string;
|
|
27
|
+
}>(tools: ReadonlyArray<T> | undefined, callName: string): T | undefined;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -301,6 +301,13 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
301
301
|
* continues with another turn.
|
|
302
302
|
*/
|
|
303
303
|
getFollowUpMessages?: () => Promise<AgentMessage[]>;
|
|
304
|
+
/**
|
|
305
|
+
* Invoked with the follow-up messages the loop dequeues for the next turn
|
|
306
|
+
* (right after {@link getFollowUpMessages}). The consumer may use this to
|
|
307
|
+
* attach per-turn state (e.g. a fresh owned-completion lineage) at actual
|
|
308
|
+
* resume admission rather than when the message was merely queued.
|
|
309
|
+
*/
|
|
310
|
+
onFollowUpConsumed?: (messages: AgentMessage[]) => void;
|
|
304
311
|
/**
|
|
305
312
|
* Supplies one bounded synthetic recovery instruction before the loop would
|
|
306
313
|
* otherwise yield. Unlike a follow-up, it is sent only to the provider and
|
|
@@ -381,6 +388,10 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
381
388
|
* Callers may abort synchronously to stop consuming buffered provider events.
|
|
382
389
|
*/
|
|
383
390
|
onAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => void;
|
|
391
|
+
/** Observe unmanaged provisional assistant deltas before public publication. */
|
|
392
|
+
onProvisionalAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => void;
|
|
393
|
+
/** True when the host consumes provisional assistant events for live safety checks. */
|
|
394
|
+
hasProvisionalAssistantMessageEventConsumer?: boolean;
|
|
384
395
|
/** Called for non-content tool-choice incapability stream events. */
|
|
385
396
|
onToolChoiceIncapability?: (event: Extract<AssistantMessageEvent, {
|
|
386
397
|
type: "toolChoiceIncapability";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/agent-core",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.14.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.
|
|
36
|
-
"@gajae-code/natives": "0.
|
|
37
|
-
"@gajae-code/utils": "0.
|
|
35
|
+
"@gajae-code/ai": "0.14.0",
|
|
36
|
+
"@gajae-code/natives": "0.14.0",
|
|
37
|
+
"@gajae-code/utils": "0.14.0",
|
|
38
38
|
"@opentelemetry/api": "^1.9.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|