@gajae-code/agent-core 0.12.0 → 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/CHANGELOG.md +7 -1
- package/dist/types/agent.d.ts +8 -1
- package/dist/types/compaction/pruning.d.ts +29 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/run-resource-ledger.d.ts +2 -0
- package/dist/types/types.d.ts +34 -0
- package/package.json +4 -4
- package/src/agent-loop.ts +128 -30
- package/src/agent.ts +37 -3
- package/src/compaction/pruning.ts +325 -131
- package/src/index.ts +1 -0
- package/src/run-resource-ledger.ts +209 -0
- package/src/types.ts +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [0.12.
|
|
5
|
+
## [0.12.1] - 2026-07-29
|
|
6
|
+
- Agent session configuration can carry an explicit first-event stream timeout while preserving provider defaults when the setting is absent.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- The `invalid_prompt` circuit breaker no longer replays the rejected turn on its repaired resend. The streaming path commits the failed assistant message to the context before the breaker runs, so the one repaired resend re-sent that errored turn as if the model had spoken it — re-triggering `Request blocked (code=invalid_prompt)` and leaving a second assistant tail that no continuation can resume from. The breaker now repairs and resends only the history that preceded the rejection.
|
|
11
|
+
- Compaction pruning now protects the newest two user/`bashExecution` turns, uses conservative read supersession, preserves bounded error-first diagnostics, and exposes reversible artifact-backed originals with exact savings accounting.
|
|
6
12
|
|
|
7
13
|
## [0.11.11] - 2026-07-26
|
|
8
14
|
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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, ManagedLogicalRunId, RunTerminalRequest, StreamFn, ToolCallContext } from "./types";
|
|
7
|
+
import type { AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, ManagedLogicalRunId, RunResourceLedger, 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
|
|
@@ -127,6 +127,8 @@ export interface AgentOptions {
|
|
|
127
127
|
requestMaxRetries?: number;
|
|
128
128
|
/** Provider stream replay retry budget. Counts retries, not the initial attempt. */
|
|
129
129
|
streamMaxRetries?: number;
|
|
130
|
+
/** Explicit first-event stream watchdog override in milliseconds. Set to 0 to disable. */
|
|
131
|
+
streamFirstEventTimeoutMs?: number;
|
|
130
132
|
/**
|
|
131
133
|
* Provides tool execution context, resolved per tool call.
|
|
132
134
|
* Use for late-bound UI or session state access.
|
|
@@ -191,6 +193,7 @@ export type AgentQueueSnapshot = {
|
|
|
191
193
|
export declare class Agent {
|
|
192
194
|
#private;
|
|
193
195
|
get intentTracing(): boolean;
|
|
196
|
+
readonly resourceLedger: RunResourceLedger;
|
|
194
197
|
streamFn: StreamFn;
|
|
195
198
|
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
|
196
199
|
getAuthCredentialType?: (provider: string) => "api_key" | "oauth" | undefined;
|
|
@@ -314,6 +317,8 @@ export declare class Agent {
|
|
|
314
317
|
set requestMaxRetries(value: number | undefined);
|
|
315
318
|
get streamMaxRetries(): number | undefined;
|
|
316
319
|
set streamMaxRetries(value: number | undefined);
|
|
320
|
+
get streamFirstEventTimeoutMs(): number | undefined;
|
|
321
|
+
set streamFirstEventTimeoutMs(value: number | undefined);
|
|
317
322
|
get state(): AgentState;
|
|
318
323
|
get contextRevision(): number;
|
|
319
324
|
get appendOnlyContext(): AppendOnlyContextManager | undefined;
|
|
@@ -415,6 +420,8 @@ export declare class Agent {
|
|
|
415
420
|
waitForIdle(): Promise<void>;
|
|
416
421
|
/** The active per-attempt run identifier. */
|
|
417
422
|
get activeRunId(): number | undefined;
|
|
423
|
+
/** Stable resource ownership identifier for the active prompt run. */
|
|
424
|
+
get activeResourceRunId(): string | undefined;
|
|
418
425
|
/**
|
|
419
426
|
* Stable identifier for the active managed logical run, shared by every retry
|
|
420
427
|
* attempt. Pass this value to requestRunTerminal(); never retain activeRunId
|
|
@@ -15,6 +15,8 @@ export interface PruneConfig {
|
|
|
15
15
|
minimumSavings: number;
|
|
16
16
|
/** Tool names that should never be pruned. */
|
|
17
17
|
protectedTools: string[];
|
|
18
|
+
/** Number of newest user turns whose tool outputs must remain intact. Defaults to 2. */
|
|
19
|
+
protectRecentTurns?: number;
|
|
18
20
|
/**
|
|
19
21
|
* Tools in `protectedTools` whose protection is waived once the result is
|
|
20
22
|
* superseded (a later result for the same target, or a later successful
|
|
@@ -24,9 +26,18 @@ export interface PruneConfig {
|
|
|
24
26
|
staleOverridableTools?: string[];
|
|
25
27
|
}
|
|
26
28
|
export declare const DEFAULT_PRUNE_CONFIG: PruneConfig;
|
|
29
|
+
export interface PrunedOriginal {
|
|
30
|
+
entryId: string;
|
|
31
|
+
toolName?: string;
|
|
32
|
+
originalText: string;
|
|
33
|
+
tokens: number;
|
|
34
|
+
/** Whether originalText captures all-text result content without omission. */
|
|
35
|
+
complete?: boolean;
|
|
36
|
+
}
|
|
27
37
|
export interface PruneResult {
|
|
28
38
|
prunedCount: number;
|
|
29
39
|
tokensSaved: number;
|
|
40
|
+
originals: PrunedOriginal[];
|
|
30
41
|
/**
|
|
31
42
|
* The mutated message entries. Callers whose entry source returns
|
|
32
43
|
* materialized copies (not live references) must write these back into
|
|
@@ -45,9 +56,11 @@ export interface AssistantArgumentPruneResult {
|
|
|
45
56
|
}
|
|
46
57
|
export declare function pruneAssistantToolArguments(entries: SessionEntry[], config?: PruneConfig): AssistantArgumentPruneResult;
|
|
47
58
|
/**
|
|
48
|
-
* Estimate the token savings {@link pruneToolOutputs} would
|
|
49
|
-
*
|
|
50
|
-
*
|
|
59
|
+
* Estimate the conservative final token savings {@link pruneToolOutputs} would
|
|
60
|
+
* achieve, without mutating entries or invoking the artifact-reference planner.
|
|
61
|
+
* When `artifactRefMaxChars` is present, the estimate budgets that full length
|
|
62
|
+
* for every complete candidate so the real artifact-backed prune cannot save
|
|
63
|
+
* less than the estimate.
|
|
51
64
|
*/
|
|
52
65
|
export declare function estimateToolOutputPruneSavings(entries: SessionEntry[], config?: PruneConfig, options?: PruneToolOutputsOptions): {
|
|
53
66
|
prunableCount: number;
|
|
@@ -69,5 +82,18 @@ export declare function shouldRunMaintenancePrune(args: {
|
|
|
69
82
|
export interface PruneToolOutputsOptions {
|
|
70
83
|
/** Lower the usual minimum only when the caller is already over its compaction threshold. */
|
|
71
84
|
relaxedMinimum?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Conservative maximum ASCII length of every planned artifact reference.
|
|
87
|
+
* Required when `artifactRef` is provided so estimation and final admission
|
|
88
|
+
* use the same worst-case notice size.
|
|
89
|
+
*/
|
|
90
|
+
artifactRefMaxChars?: number;
|
|
91
|
+
/**
|
|
92
|
+
* Plan a numeric `artifact://<id>` reference for a candidate's original
|
|
93
|
+
* text. The callback may reserve an in-memory identifier, but MUST NOT publish
|
|
94
|
+
* files or mutate session entries; publish only the originals returned by a
|
|
95
|
+
* successful {@link pruneToolOutputs} result.
|
|
96
|
+
*/
|
|
97
|
+
artifactRef?: (candidate: PrunedOriginal) => string | undefined;
|
|
72
98
|
}
|
|
73
99
|
export declare function pruneToolOutputs(entries: SessionEntry[], config?: PruneConfig, options?: PruneToolOutputsOptions): PruneResult;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from "./harmony-leak";
|
|
|
6
6
|
export * from "./image-placeholder-guard";
|
|
7
7
|
export * from "./proxy";
|
|
8
8
|
export * from "./run-collector";
|
|
9
|
+
export * from "./run-resource-ledger";
|
|
9
10
|
export * from "./telemetry";
|
|
10
11
|
export * from "./thinking";
|
|
11
12
|
export * from "./types";
|
package/dist/types/types.d.ts
CHANGED
|
@@ -7,6 +7,33 @@ import type { AgentTelemetryConfig } from "./telemetry";
|
|
|
7
7
|
export type StreamFn = (...args: Parameters<typeof streamSimple>) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
|
|
8
8
|
/** Stable identifier for a managed logical run, shared by all of its retry attempts. */
|
|
9
9
|
export type ManagedLogicalRunId = number;
|
|
10
|
+
/** A resource owned by a prompt run until its promise settles. */
|
|
11
|
+
export type RunResourceKind = "provider_factory" | "provider_iterator" | "tool" | "post_prompt";
|
|
12
|
+
export interface RunResourceEntry {
|
|
13
|
+
id: string;
|
|
14
|
+
kind: RunResourceKind;
|
|
15
|
+
label: string;
|
|
16
|
+
registeredAt: number;
|
|
17
|
+
}
|
|
18
|
+
export type RunSettlementProof = {
|
|
19
|
+
status: "settled";
|
|
20
|
+
} | {
|
|
21
|
+
status: "unfenced";
|
|
22
|
+
pending: RunResourceEntry[];
|
|
23
|
+
};
|
|
24
|
+
export interface RunResourceLedger {
|
|
25
|
+
/** Reserve a run handle before publishing its `agent_start` event. */
|
|
26
|
+
open(resourceRunId: string): void;
|
|
27
|
+
track(resourceRunId: string, kind: RunResourceKind, label: string, settled: PromiseLike<unknown>): void;
|
|
28
|
+
pending(resourceRunId: string): RunResourceEntry[];
|
|
29
|
+
/** Seal a run after terminal event publication; only sealed empty runs settle. */
|
|
30
|
+
seal(resourceRunId: string): void;
|
|
31
|
+
waitForSettlement(resourceRunId: string, options: {
|
|
32
|
+
graceMs: number;
|
|
33
|
+
}): Promise<RunSettlementProof>;
|
|
34
|
+
/** Terminally detach a run; its bounded tombstone remains unfenced forever. */
|
|
35
|
+
quarantine(resourceRunId: string): RunResourceEntry[];
|
|
36
|
+
}
|
|
10
37
|
/** Terminal completion requested for a logical run. */
|
|
11
38
|
export interface RunTerminalRequest {
|
|
12
39
|
stopReason: "cancelled" | "error" | "exhausted";
|
|
@@ -302,6 +329,13 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
302
329
|
* capture, cost estimator, agent identity).
|
|
303
330
|
*/
|
|
304
331
|
telemetry?: AgentTelemetryConfig;
|
|
332
|
+
/**
|
|
333
|
+
* Optional prompt-run resource ownership ledger. Provider and scheduler-level tool
|
|
334
|
+
* work is tracked until its owned lifecycle promise settles.
|
|
335
|
+
*/
|
|
336
|
+
resourceLedger?: RunResourceLedger;
|
|
337
|
+
/** Stable resource ownership identifier for this prompt run. */
|
|
338
|
+
resourceRunId?: string;
|
|
305
339
|
}
|
|
306
340
|
/**
|
|
307
341
|
* Batch/sequencing metadata for the tool call currently being processed.
|
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.
|
|
4
|
+
"version": "0.12.1",
|
|
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.
|
|
36
|
-
"@gajae-code/natives": "0.12.
|
|
37
|
-
"@gajae-code/utils": "0.12.
|
|
35
|
+
"@gajae-code/ai": "0.12.1",
|
|
36
|
+
"@gajae-code/natives": "0.12.1",
|
|
37
|
+
"@gajae-code/utils": "0.12.1",
|
|
38
38
|
"@opentelemetry/api": "^1.9.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -305,6 +305,7 @@ export function agentLoop(
|
|
|
305
305
|
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent, config.model)
|
|
306
306
|
: undefined;
|
|
307
307
|
const attemptStream = transaction ?? stream;
|
|
308
|
+
openResourceRun(config);
|
|
308
309
|
if (!config.fallbackManaged || emitManagedAgentStart) stream.push({ type: "agent_start" });
|
|
309
310
|
attemptStream.push({ type: "turn_start" });
|
|
310
311
|
for (const prompt of prompts) {
|
|
@@ -315,6 +316,7 @@ export function agentLoop(
|
|
|
315
316
|
try {
|
|
316
317
|
await runLoop(currentContext, newMessages, config, signal, stream, streamFn, transaction);
|
|
317
318
|
} catch (err) {
|
|
319
|
+
if (config.resourceLedger && config.resourceRunId) config.resourceLedger.seal(config.resourceRunId);
|
|
318
320
|
stream.fail(err);
|
|
319
321
|
}
|
|
320
322
|
})();
|
|
@@ -354,12 +356,14 @@ export function agentLoopContinue(
|
|
|
354
356
|
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent, config.model)
|
|
355
357
|
: undefined;
|
|
356
358
|
const attemptStream = transaction ?? stream;
|
|
359
|
+
openResourceRun(config);
|
|
357
360
|
if (!config.fallbackManaged || emitManagedAgentStart) stream.push({ type: "agent_start" });
|
|
358
361
|
attemptStream.push({ type: "turn_start" });
|
|
359
362
|
|
|
360
363
|
try {
|
|
361
364
|
await runLoop(currentContext, newMessages, config, signal, stream, streamFn, transaction);
|
|
362
365
|
} catch (err) {
|
|
366
|
+
if (config.resourceLedger && config.resourceRunId) config.resourceLedger.seal(config.resourceRunId);
|
|
363
367
|
stream.fail(err);
|
|
364
368
|
}
|
|
365
369
|
})();
|
|
@@ -374,6 +378,21 @@ function createAgentStream(): EventStream<AgentEvent, AgentMessage[]> {
|
|
|
374
378
|
);
|
|
375
379
|
}
|
|
376
380
|
|
|
381
|
+
function openResourceRun(config: AgentLoopConfig): void {
|
|
382
|
+
if (config.resourceLedger && config.resourceRunId) config.resourceLedger.open(config.resourceRunId);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function publishAgentEnd(
|
|
386
|
+
stream: EventStream<AgentEvent, AgentMessage[]>,
|
|
387
|
+
config: AgentLoopConfig,
|
|
388
|
+
event: Extract<AgentEvent, { type: "agent_end" }>,
|
|
389
|
+
): void {
|
|
390
|
+
stream.push(event);
|
|
391
|
+
if (event.stopReason !== "maintenance" && config.resourceLedger && config.resourceRunId) {
|
|
392
|
+
config.resourceLedger.seal(config.resourceRunId);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
377
396
|
/**
|
|
378
397
|
* Hard work budget for one degraded snapshot: every visited node AND every
|
|
379
398
|
* enumerated own key is debited against this budget before it is processed
|
|
@@ -1475,7 +1494,19 @@ async function runLoopBody(
|
|
|
1475
1494
|
isInvalidPromptError(message)
|
|
1476
1495
|
) {
|
|
1477
1496
|
invalidPromptRepairAttempted = true;
|
|
1478
|
-
|
|
1497
|
+
// The rejected turn was already committed to the context by the
|
|
1498
|
+
// streaming path. Repair (and resend) only the history that
|
|
1499
|
+
// preceded it: replaying an errored assistant turn re-poisons the
|
|
1500
|
+
// request and leaves a second assistant tail behind, which no
|
|
1501
|
+
// continuation can resume from.
|
|
1502
|
+
const rejectedIndex = currentContext.messages.length - 1;
|
|
1503
|
+
const rejectedCommitted =
|
|
1504
|
+
rejectedIndex >= 0 && currentContext.messages[rejectedIndex]?.role === "assistant";
|
|
1505
|
+
const retained = rejectedCommitted
|
|
1506
|
+
? currentContext.messages.slice(0, rejectedIndex)
|
|
1507
|
+
: currentContext.messages;
|
|
1508
|
+
if (repairInvalidPromptHistory(retained)) {
|
|
1509
|
+
if (rejectedCommitted) currentContext.messages.splice(rejectedIndex, 1);
|
|
1479
1510
|
continue;
|
|
1480
1511
|
}
|
|
1481
1512
|
}
|
|
@@ -1558,7 +1589,7 @@ async function runLoopBody(
|
|
|
1558
1589
|
});
|
|
1559
1590
|
}
|
|
1560
1591
|
stream.push({ type: "turn_end", message, toolResults });
|
|
1561
|
-
stream
|
|
1592
|
+
publishAgentEnd(stream, config, buildAgentEndEvent(newMessages, telemetry, stepCounter.count));
|
|
1562
1593
|
stream.end(newMessages);
|
|
1563
1594
|
return;
|
|
1564
1595
|
}
|
|
@@ -1638,7 +1669,7 @@ async function runLoopBody(
|
|
|
1638
1669
|
pendingMessages = (await config.getSteeringMessages?.()) || [];
|
|
1639
1670
|
if (pendingMessages.length > 0) continue;
|
|
1640
1671
|
if (config.shouldPause?.()) {
|
|
1641
|
-
stream
|
|
1672
|
+
publishAgentEnd(stream, config, buildAgentEndEvent(newMessages, telemetry, stepCounter.count, "paused"));
|
|
1642
1673
|
stream.end(newMessages);
|
|
1643
1674
|
return;
|
|
1644
1675
|
}
|
|
@@ -1659,7 +1690,7 @@ async function runLoopBody(
|
|
|
1659
1690
|
message.errorMessage = message.errorMessage
|
|
1660
1691
|
? `${message.errorMessage} | ${breakerMessage}`
|
|
1661
1692
|
: breakerMessage;
|
|
1662
|
-
stream
|
|
1693
|
+
publishAgentEnd(stream, config, buildAgentEndEvent(newMessages, telemetry, stepCounter.count));
|
|
1663
1694
|
stream.end(newMessages);
|
|
1664
1695
|
return;
|
|
1665
1696
|
}
|
|
@@ -1668,7 +1699,7 @@ async function runLoopBody(
|
|
|
1668
1699
|
// Agent would stop here. Check for follow-up messages.
|
|
1669
1700
|
await config.onBeforeYield?.();
|
|
1670
1701
|
if (config.shouldPause?.()) {
|
|
1671
|
-
stream
|
|
1702
|
+
publishAgentEnd(stream, config, buildAgentEndEvent(newMessages, telemetry, stepCounter.count, "paused"));
|
|
1672
1703
|
stream.end(newMessages);
|
|
1673
1704
|
return;
|
|
1674
1705
|
}
|
|
@@ -1683,7 +1714,7 @@ async function runLoopBody(
|
|
|
1683
1714
|
break;
|
|
1684
1715
|
}
|
|
1685
1716
|
|
|
1686
|
-
stream
|
|
1717
|
+
publishAgentEnd(stream, config, buildAgentEndEvent(newMessages, telemetry, stepCounter.count));
|
|
1687
1718
|
stream.end(newMessages);
|
|
1688
1719
|
}
|
|
1689
1720
|
|
|
@@ -1831,24 +1862,64 @@ async function streamAssistantResponse(
|
|
|
1831
1862
|
try {
|
|
1832
1863
|
return await runInActiveSpan(chatSpan, async () => {
|
|
1833
1864
|
const fallbackAttempt = config.fallbackManaged ? config.nextFallbackAttempt?.(config.model) : undefined;
|
|
1834
|
-
const
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1865
|
+
const responsePromise = Promise.resolve().then(() =>
|
|
1866
|
+
streamFunction(config.model, llmContext, {
|
|
1867
|
+
...config,
|
|
1868
|
+
fallbackAttempt,
|
|
1869
|
+
apiKey: resolvedApiKey,
|
|
1870
|
+
authCredentialType,
|
|
1871
|
+
metadata: resolvedMetadata,
|
|
1872
|
+
sessionId: config.providerSessionId ?? config.sessionId,
|
|
1873
|
+
toolChoice: effectiveToolChoice,
|
|
1874
|
+
reasoning: effectiveReasoning,
|
|
1875
|
+
temperature: effectiveTemperature,
|
|
1876
|
+
signal: requestSignal,
|
|
1877
|
+
onResponse: captureOnResponse,
|
|
1878
|
+
}),
|
|
1879
|
+
);
|
|
1880
|
+
const { promise: iteratorSettled, resolve: settleIterator } = Promise.withResolvers<void>();
|
|
1881
|
+
let responseResultPromise: Promise<AssistantMessage> | undefined;
|
|
1882
|
+
let responseForResult: { result(): Promise<AssistantMessage> } | undefined;
|
|
1883
|
+
const getResponseResult = (): Promise<AssistantMessage> =>
|
|
1884
|
+
(responseResultPromise ??= Promise.resolve().then(() => responseForResult!.result()));
|
|
1885
|
+
const providerLifecycle = responsePromise.then(async response => {
|
|
1886
|
+
responseForResult = response;
|
|
1887
|
+
await iteratorSettled;
|
|
1888
|
+
await Promise.allSettled([getResponseResult()]);
|
|
1846
1889
|
});
|
|
1890
|
+
if (config.resourceLedger && config.resourceRunId) {
|
|
1891
|
+
// One ownership spans factory creation, iterator close, and trailing result.
|
|
1892
|
+
config.resourceLedger.track(
|
|
1893
|
+
config.resourceRunId,
|
|
1894
|
+
"provider_factory",
|
|
1895
|
+
`${config.model.provider}/${config.model.id}`,
|
|
1896
|
+
providerLifecycle,
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
const response = await responsePromise;
|
|
1900
|
+
responseForResult = response;
|
|
1847
1901
|
|
|
1848
1902
|
let partialMessage: AssistantMessage | null = null;
|
|
1849
1903
|
let addedPartial = false;
|
|
1850
1904
|
|
|
1851
1905
|
const responseIterator = response[Symbol.asyncIterator]();
|
|
1906
|
+
let iteratorClosed = false;
|
|
1907
|
+
const closeIterator = (): void => {
|
|
1908
|
+
if (iteratorClosed) return;
|
|
1909
|
+
iteratorClosed = true;
|
|
1910
|
+
|
|
1911
|
+
void Promise.resolve()
|
|
1912
|
+
.then(() => responseIterator.return?.())
|
|
1913
|
+
.then(
|
|
1914
|
+
() => settleIterator(),
|
|
1915
|
+
() => settleIterator(),
|
|
1916
|
+
);
|
|
1917
|
+
};
|
|
1918
|
+
const finishResponse = async (): Promise<AssistantMessage> => {
|
|
1919
|
+
closeIterator();
|
|
1920
|
+
await iteratorSettled;
|
|
1921
|
+
return getResponseResult();
|
|
1922
|
+
};
|
|
1852
1923
|
|
|
1853
1924
|
// Set up a single abort race: register the abort listener once for the whole
|
|
1854
1925
|
// stream and reuse the same race promise for every iterator.next() instead of
|
|
@@ -1857,6 +1928,7 @@ async function streamAssistantResponse(
|
|
|
1857
1928
|
let detachAbortListener: (() => void) | undefined;
|
|
1858
1929
|
if (requestSignal) {
|
|
1859
1930
|
if (requestSignal.aborted) {
|
|
1931
|
+
closeIterator();
|
|
1860
1932
|
const aborted = emitAbortedAssistantMessage(partialMessage, addedPartial, context, config, stream);
|
|
1861
1933
|
await finishChat(aborted);
|
|
1862
1934
|
return aborted;
|
|
@@ -1874,7 +1946,7 @@ async function streamAssistantResponse(
|
|
|
1874
1946
|
if (abortRacePromise) {
|
|
1875
1947
|
const result = await Promise.race([responseIterator.next(), abortRacePromise]);
|
|
1876
1948
|
if (result === ABORTED) {
|
|
1877
|
-
|
|
1949
|
+
closeIterator();
|
|
1878
1950
|
const aborted = emitAbortedAssistantMessage(partialMessage, addedPartial, context, config, stream);
|
|
1879
1951
|
await finishChat(aborted);
|
|
1880
1952
|
return aborted;
|
|
@@ -1888,7 +1960,11 @@ async function streamAssistantResponse(
|
|
|
1888
1960
|
await finishChat(aborted);
|
|
1889
1961
|
return aborted;
|
|
1890
1962
|
}
|
|
1891
|
-
if (next.done)
|
|
1963
|
+
if (next.done) {
|
|
1964
|
+
iteratorClosed = true;
|
|
1965
|
+
settleIterator();
|
|
1966
|
+
break;
|
|
1967
|
+
}
|
|
1892
1968
|
|
|
1893
1969
|
const event = next.value;
|
|
1894
1970
|
|
|
@@ -1937,8 +2013,8 @@ async function streamAssistantResponse(
|
|
|
1937
2013
|
case "done":
|
|
1938
2014
|
case "error": {
|
|
1939
2015
|
const finalMessage = config.fallbackManaged
|
|
1940
|
-
? managedAssistantShell(await
|
|
1941
|
-
: await
|
|
2016
|
+
? managedAssistantShell(await finishResponse(), config.model)
|
|
2017
|
+
: await finishResponse();
|
|
1942
2018
|
if (addedPartial) {
|
|
1943
2019
|
context.messages[context.messages.length - 1] = finalMessage;
|
|
1944
2020
|
} else {
|
|
@@ -1955,11 +2031,12 @@ async function streamAssistantResponse(
|
|
|
1955
2031
|
}
|
|
1956
2032
|
} finally {
|
|
1957
2033
|
detachAbortListener?.();
|
|
2034
|
+
closeIterator();
|
|
1958
2035
|
}
|
|
1959
2036
|
|
|
1960
2037
|
const trailing = config.fallbackManaged
|
|
1961
|
-
? managedAssistantShell(await
|
|
1962
|
-
: await
|
|
2038
|
+
? managedAssistantShell(await finishResponse(), config.model)
|
|
2039
|
+
: await finishResponse();
|
|
1963
2040
|
await finishChat(trailing);
|
|
1964
2041
|
return trailing;
|
|
1965
2042
|
});
|
|
@@ -2142,10 +2219,8 @@ async function executeToolCalls(
|
|
|
2142
2219
|
const runTool = async (record: (typeof records)[number], index: number): Promise<void> => {
|
|
2143
2220
|
if (interruptState.triggered) {
|
|
2144
2221
|
// Skip both span emission and the collector orphan record here. The
|
|
2145
|
-
//
|
|
2146
|
-
//
|
|
2147
|
-
// `recordSkippedTool` and `emitToolResult` once per record, so any
|
|
2148
|
-
// work we did here would double-count.
|
|
2222
|
+
// scheduler-task finalizer emits the skipped result and collector record;
|
|
2223
|
+
// the tail sweep below remains a defensive fallback for unexpected throws.
|
|
2149
2224
|
record.skipped = true;
|
|
2150
2225
|
return;
|
|
2151
2226
|
}
|
|
@@ -2261,7 +2336,7 @@ async function executeToolCalls(
|
|
|
2261
2336
|
toolCalls: toolCallInfos,
|
|
2262
2337
|
})
|
|
2263
2338
|
: undefined;
|
|
2264
|
-
const
|
|
2339
|
+
const execution = tool.execute(
|
|
2265
2340
|
toolCall.id,
|
|
2266
2341
|
transformToolCallArguments ? transformToolCallArguments(effectiveArgs, toolCall.name) : effectiveArgs,
|
|
2267
2342
|
tool.nonAbortable ? undefined : toolSignal,
|
|
@@ -2276,6 +2351,7 @@ async function executeToolCalls(
|
|
|
2276
2351
|
},
|
|
2277
2352
|
toolContext,
|
|
2278
2353
|
);
|
|
2354
|
+
const rawResult = await execution;
|
|
2279
2355
|
const coerced = coerceToolResult(rawResult);
|
|
2280
2356
|
result = coerced.result;
|
|
2281
2357
|
if (coerced.malformed || result.isError) isError = true;
|
|
@@ -2359,8 +2435,30 @@ async function executeToolCalls(
|
|
|
2359
2435
|
const record = records[index];
|
|
2360
2436
|
const concurrency = record.tool?.concurrency ?? "shared";
|
|
2361
2437
|
const start = concurrency === "exclusive" ? Promise.all([lastExclusive, ...sharedTasks]) : lastExclusive;
|
|
2362
|
-
const task = start
|
|
2438
|
+
const task = start
|
|
2439
|
+
.then(() => runTool(record, index))
|
|
2440
|
+
.finally(() => {
|
|
2441
|
+
// Scheduler ownership includes dependency waits and the fallback skip
|
|
2442
|
+
// emission, not only tool.execute().
|
|
2443
|
+
if (!record.toolResultMessage) {
|
|
2444
|
+
record.skipped = true;
|
|
2445
|
+
recordSkippedTool(telemetry, {
|
|
2446
|
+
toolCallId: record.toolCall.id,
|
|
2447
|
+
toolName: record.toolCall.name,
|
|
2448
|
+
status: "skipped",
|
|
2449
|
+
});
|
|
2450
|
+
emitToolResult(record, createSkippedToolResult(), true);
|
|
2451
|
+
}
|
|
2452
|
+
});
|
|
2363
2453
|
tasks.push(task);
|
|
2454
|
+
if (config.resourceLedger && config.resourceRunId) {
|
|
2455
|
+
config.resourceLedger.track(
|
|
2456
|
+
config.resourceRunId,
|
|
2457
|
+
"tool",
|
|
2458
|
+
`${record.toolCall.name}:${record.toolCall.id}`,
|
|
2459
|
+
task,
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2364
2462
|
if (concurrency === "exclusive") {
|
|
2365
2463
|
lastExclusive = task;
|
|
2366
2464
|
sharedTasks = [];
|
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
|
|
@@ -1342,11 +1364,13 @@ export class Agent {
|
|
|
1342
1364
|
this.#state.isStreaming = true;
|
|
1343
1365
|
this.#state.streamMessage = null;
|
|
1344
1366
|
this.#state.error = undefined;
|
|
1345
|
-
options?.onRunAccepted?.();
|
|
1346
1367
|
|
|
1347
1368
|
const fallbackManaged = options?.fallbackManaged === true;
|
|
1348
1369
|
const managedLogicalRunOwner = fallbackManaged ? (this.#managedLogicalRunOwner ?? runId) : undefined;
|
|
1349
1370
|
const startsManagedLogicalRun = fallbackManaged && this.#managedLogicalRunOwner === undefined;
|
|
1371
|
+
this.#activeResourceRunId = String(managedLogicalRunOwner ?? runId);
|
|
1372
|
+
this.resourceLedger.open(this.#activeResourceRunId);
|
|
1373
|
+
options?.onRunAccepted?.();
|
|
1350
1374
|
if (startsManagedLogicalRun) {
|
|
1351
1375
|
this.#managedLogicalRunOwner = managedLogicalRunOwner;
|
|
1352
1376
|
this.#emit({ type: "agent_start" });
|
|
@@ -1358,6 +1382,7 @@ export class Agent {
|
|
|
1358
1382
|
this.#state.isStreaming = false;
|
|
1359
1383
|
this.#abortController = undefined;
|
|
1360
1384
|
this.#activeRunId = undefined;
|
|
1385
|
+
this.#activeResourceRunId = undefined;
|
|
1361
1386
|
this.#runningPrompt = undefined;
|
|
1362
1387
|
this.#resolveRunningPrompt = undefined;
|
|
1363
1388
|
resolve();
|
|
@@ -1431,6 +1456,7 @@ export class Agent {
|
|
|
1431
1456
|
maxRetryDelayMs: this.#maxRetryDelayMs,
|
|
1432
1457
|
requestMaxRetries: this.#requestMaxRetries,
|
|
1433
1458
|
streamMaxRetries: this.#streamMaxRetries,
|
|
1459
|
+
streamFirstEventTimeoutMs: this.#streamFirstEventTimeoutMs,
|
|
1434
1460
|
...(fallbackManaged
|
|
1435
1461
|
? {
|
|
1436
1462
|
fallbackManaged: true,
|
|
@@ -1454,6 +1480,8 @@ export class Agent {
|
|
|
1454
1480
|
onResponse: this.#onResponse,
|
|
1455
1481
|
onSseEvent: this.#onSseEvent,
|
|
1456
1482
|
signal: abortController.signal,
|
|
1483
|
+
resourceLedger: this.resourceLedger,
|
|
1484
|
+
resourceRunId: this.#activeResourceRunId,
|
|
1457
1485
|
getApiKey: this.getApiKey,
|
|
1458
1486
|
getAuthCredentialType: this.getAuthCredentialType,
|
|
1459
1487
|
getToolContext: this.#getToolContext,
|
|
@@ -1700,6 +1728,7 @@ export class Agent {
|
|
|
1700
1728
|
this.#state.pendingToolCalls = new Set<string>();
|
|
1701
1729
|
this.#abortController = undefined;
|
|
1702
1730
|
this.#activeRunId = undefined;
|
|
1731
|
+
this.#activeResourceRunId = undefined;
|
|
1703
1732
|
this.#activeFallbackManaged = false;
|
|
1704
1733
|
this.#resolveRunningPrompt?.();
|
|
1705
1734
|
this.#runningPrompt = undefined;
|
|
@@ -1758,8 +1787,13 @@ export class Agent {
|
|
|
1758
1787
|
if (this.#terminalizedLogicalRunIds.size > 256) {
|
|
1759
1788
|
this.#terminalizedLogicalRunIds.delete(this.#terminalizedLogicalRunIds.values().next().value!);
|
|
1760
1789
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
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
|
+
}
|
|
1763
1797
|
}
|
|
1764
1798
|
|
|
1765
1799
|
#getAssistantTextLength(message: AgentMessage | null): number {
|