@gajae-code/agent-core 0.11.1 → 0.11.2
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 +2 -0
- package/dist/types/agent-loop.d.ts +44 -0
- package/dist/types/agent.d.ts +8 -0
- package/dist/types/compaction/compaction.d.ts +6 -2
- package/dist/types/compaction/pruning.d.ts +6 -2
- package/dist/types/proxy.d.ts +11 -0
- package/dist/types/types.d.ts +8 -2
- package/package.json +4 -4
- package/src/agent-loop.ts +504 -60
- package/src/agent.ts +36 -3
- package/src/compaction/compaction.ts +45 -89
- package/src/compaction/pruning.ts +112 -4
- package/src/proxy.ts +82 -2
- package/src/types.ts +4 -1
package/src/agent-loop.ts
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
* Agent loop that works with AgentMessage throughout.
|
|
3
3
|
* Transforms to Message[] only at the LLM call boundary.
|
|
4
4
|
*/
|
|
5
|
+
|
|
6
|
+
import { types as nodeUtilTypes } from "node:util";
|
|
5
7
|
import {
|
|
6
8
|
type AssistantMessage,
|
|
7
9
|
type AssistantMessageEvent,
|
|
8
10
|
type Context,
|
|
11
|
+
classifyContextOverflow,
|
|
9
12
|
classifyFallbackTrigger,
|
|
10
13
|
EventStream,
|
|
11
|
-
isContextOverflow,
|
|
12
14
|
isZodSchema,
|
|
13
15
|
streamSimple,
|
|
14
16
|
type ToolResultMessage,
|
|
@@ -67,35 +69,58 @@ import type {
|
|
|
67
69
|
export const MANAGED_ATTEMPT_MAX_STAGED_EVENTS = 10_000;
|
|
68
70
|
export const MANAGED_ATTEMPT_MAX_STAGED_BYTES = 16 * 1024 * 1024;
|
|
69
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Local staging failure: the provisional buffer limit was exceeded. Carries
|
|
74
|
+
* NO transport facts or status by design — only original typed provider
|
|
75
|
+
* transport facts may authorize provider fallback, so local buffer machinery
|
|
76
|
+
* must never masquerade as provider evidence or consume the fallback chain.
|
|
77
|
+
* It is therefore non-retryable and surfaces as an explicit local error.
|
|
78
|
+
*/
|
|
70
79
|
class ManagedAttemptBufferOverflowError extends Error {
|
|
71
|
-
readonly status = 503;
|
|
72
|
-
|
|
73
80
|
constructor() {
|
|
74
81
|
super("Managed fallback attempt exceeded the provisional event buffer limit");
|
|
75
82
|
this.name = "ManagedAttemptBufferOverflowError";
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Local snapshot-machinery failure. Deliberately carries no transport facts
|
|
88
|
+
* or status, so managed fallback classification never treats it as a provider
|
|
89
|
+
* retry trigger — it fails fast instead of burning the fallback chain.
|
|
90
|
+
*/
|
|
91
|
+
class ManagedAttemptSnapshotError extends Error {
|
|
92
|
+
constructor() {
|
|
93
|
+
super(
|
|
94
|
+
"Managed fallback attempt could not produce a serializable event snapshot (local snapshot bug, not a provider failure)",
|
|
95
|
+
);
|
|
96
|
+
this.name = "ManagedAttemptSnapshotError";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
79
100
|
const managedAttemptTextEncoder = new TextEncoder();
|
|
80
101
|
|
|
81
102
|
const ABORTED: unique symbol = Symbol("agent-loop-aborted");
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
function isEmptyResponseOverflow(message: AssistantMessage): boolean {
|
|
89
|
-
return isContextOverflow(message);
|
|
103
|
+
function managedContextOverflow(message: AssistantMessage, config: AgentLoopConfig): boolean {
|
|
104
|
+
const transportFailure = managedTransportFailure(message);
|
|
105
|
+
// Managed empty-stop responses may be repaired by the managed shell below; only
|
|
106
|
+
// typed/error overflows are discardable before that normalization boundary.
|
|
107
|
+
if (config.fallbackManaged && message.stopReason !== "error") return false;
|
|
108
|
+
return classifyContextOverflow(message, transportFailure, config.model.contextWindow);
|
|
90
109
|
}
|
|
91
110
|
|
|
92
|
-
/** Managed fallback owns retry policy; only typed transport facts may discard an attempt. */
|
|
93
|
-
function
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
/** Managed fallback owns retry policy; only attached typed transport facts may discard an attempt. */
|
|
112
|
+
function managedProperty(value: unknown, key: string): unknown {
|
|
113
|
+
if (!value || typeof value !== "object") return undefined;
|
|
114
|
+
try {
|
|
115
|
+
return Reflect.get(value, key);
|
|
116
|
+
} catch {
|
|
117
|
+
return undefined;
|
|
97
118
|
}
|
|
98
|
-
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function managedTransportFailure(failure: unknown) {
|
|
122
|
+
const facts = managedProperty(failure, "transportFailure");
|
|
123
|
+
return facts && typeof facts === "object" ? transportFailureFacts(facts) : undefined;
|
|
99
124
|
}
|
|
100
125
|
|
|
101
126
|
function managedRetryableFailure(failure: unknown): boolean {
|
|
@@ -150,16 +175,22 @@ function managedFailureOutcome(message: AssistantMessage): ManagedAttemptOutcome
|
|
|
150
175
|
};
|
|
151
176
|
}
|
|
152
177
|
|
|
178
|
+
function managedContextOverflowOutcome(message: AssistantMessage): ManagedAttemptOutcome {
|
|
179
|
+
return { type: "context_overflow_discarded", message };
|
|
180
|
+
}
|
|
181
|
+
|
|
153
182
|
function managedFailureMessage(error: unknown, config: AgentLoopConfig): AssistantMessage {
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
183
|
+
const errorMessage = managedProperty(error, "message");
|
|
184
|
+
const transportFailure = managedTransportFailure(error);
|
|
185
|
+
let fallbackMessage = "Managed fallback attempt failed";
|
|
186
|
+
if (typeof errorMessage === "string") fallbackMessage = errorMessage;
|
|
187
|
+
else {
|
|
188
|
+
try {
|
|
189
|
+
fallbackMessage = String(error);
|
|
190
|
+
} catch {
|
|
191
|
+
// Keep the stable local message for hostile wrappers.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
163
194
|
return {
|
|
164
195
|
role: "assistant",
|
|
165
196
|
content: [],
|
|
@@ -175,8 +206,7 @@ function managedFailureMessage(error: unknown, config: AgentLoopConfig): Assista
|
|
|
175
206
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
176
207
|
},
|
|
177
208
|
stopReason: "error",
|
|
178
|
-
errorMessage:
|
|
179
|
-
errorStatus: status,
|
|
209
|
+
errorMessage: fallbackMessage,
|
|
180
210
|
...(transportFailure ? { transportFailure } : {}),
|
|
181
211
|
timestamp: Date.now(),
|
|
182
212
|
};
|
|
@@ -259,7 +289,7 @@ export function agentLoop(
|
|
|
259
289
|
messages: [...context.messages, ...prompts],
|
|
260
290
|
};
|
|
261
291
|
const transaction = config.fallbackManaged
|
|
262
|
-
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent)
|
|
292
|
+
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent, config.model)
|
|
263
293
|
: undefined;
|
|
264
294
|
const attemptStream = transaction ?? stream;
|
|
265
295
|
if (!config.fallbackManaged || emitManagedAgentStart) stream.push({ type: "agent_start" });
|
|
@@ -308,7 +338,7 @@ export function agentLoopContinue(
|
|
|
308
338
|
const newMessages: AgentMessage[] = [];
|
|
309
339
|
const currentContext: AgentContext = { ...context };
|
|
310
340
|
const transaction = config.fallbackManaged
|
|
311
|
-
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent)
|
|
341
|
+
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent, config.model)
|
|
312
342
|
: undefined;
|
|
313
343
|
const attemptStream = transaction ?? stream;
|
|
314
344
|
if (!config.fallbackManaged || emitManagedAgentStart) stream.push({ type: "agent_start" });
|
|
@@ -331,9 +361,321 @@ function createAgentStream(): EventStream<AgentEvent, AgentMessage[]> {
|
|
|
331
361
|
);
|
|
332
362
|
}
|
|
333
363
|
|
|
334
|
-
/**
|
|
364
|
+
/**
|
|
365
|
+
* Hard work budget for one degraded snapshot: every visited node AND every
|
|
366
|
+
* enumerated own key is debited against this budget before it is processed
|
|
367
|
+
* (accessor keys and re-visits of shared objects included), and any remainder
|
|
368
|
+
* collapses to the deterministic `"[truncated]"` placeholder. Well above
|
|
369
|
+
* ordinary streamed events; it only bounds hostile graphs.
|
|
370
|
+
*/
|
|
371
|
+
export const MANAGED_SNAPSHOT_MAX_NODES = 100_000;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Cycle-aware deep clone that always returns a detached, JSON-serializable
|
|
375
|
+
* value. Used whenever a detached snapshot cannot be safely obtained or
|
|
376
|
+
* measured: after `structuredClone` fails, and again when a (successfully
|
|
377
|
+
* cloned) snapshot cannot be serialized for byte accounting.
|
|
378
|
+
*
|
|
379
|
+
* Totality rules — the walk must never dispatch through payload-controlled
|
|
380
|
+
* code, throw, or do unbounded work:
|
|
381
|
+
* - proxies (revoked or live) are collapsed to `"[unserializable]"` BEFORE
|
|
382
|
+
* any reflective operation, so `ownKeys`/descriptor traps are never
|
|
383
|
+
* dispatched (`util.types.isProxy` identifies proxies without touching
|
|
384
|
+
* their handlers);
|
|
385
|
+
* - only intrinsics are used on the remaining ordinary objects (no
|
|
386
|
+
* `input.map`, no `input.getTime()`, no `input.length` reads);
|
|
387
|
+
* - arrays are enumerated through their own present keys, never their
|
|
388
|
+
* declared length, so a sparse array cannot force a dense allocation
|
|
389
|
+
* proportional to `length`; sparse/exotic arrays degrade to a null-proto
|
|
390
|
+
* record of their present indices, and the dense-shape decision verifies
|
|
391
|
+
* every index against its ordinal;
|
|
392
|
+
* - the walk debits `maxNodes` budget per visited node and per enumerated
|
|
393
|
+
* key before processing it; anything beyond the budget becomes
|
|
394
|
+
* `"[truncated]"` (the one linear primitive per visited node is a single
|
|
395
|
+
* `Object.keys` call on a non-proxy object the process already holds);
|
|
396
|
+
* - property values are read via own-property descriptors, so accessors are
|
|
397
|
+
* never invoked (a snapshot must not cause observable side effects) and are
|
|
398
|
+
* replaced with `"[accessor]"`;
|
|
399
|
+
* - functions/symbols and any property that cannot be read safely become
|
|
400
|
+
* short placeholders, `bigint` becomes its decimal string, and references
|
|
401
|
+
* back into the current path collapse to `"[Circular]"`;
|
|
402
|
+
* - records are built on a null prototype so a `__proto__` key cannot mutate
|
|
403
|
+
* the clone's prototype chain.
|
|
404
|
+
*
|
|
405
|
+
* Exported for direct regression coverage of the budget accounting; runtime
|
|
406
|
+
* callers use the default budget via {@link managedAttemptSnapshot}.
|
|
407
|
+
*/
|
|
408
|
+
export function sanitizedDetachedClone<T>(value: T, maxNodes: number = MANAGED_SNAPSHOT_MAX_NODES): T {
|
|
409
|
+
const path = new Set<object>();
|
|
410
|
+
let budget = maxNodes;
|
|
411
|
+
const takeBudget = (units: number): boolean => {
|
|
412
|
+
if (budget < units) {
|
|
413
|
+
budget = 0;
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
budget -= units;
|
|
417
|
+
return true;
|
|
418
|
+
};
|
|
419
|
+
const walk = (input: unknown): unknown => {
|
|
420
|
+
if (!takeBudget(1)) return "[truncated]";
|
|
421
|
+
if (typeof input === "bigint") return String(input);
|
|
422
|
+
if (typeof input === "function" || typeof input === "symbol") return "[unserializable]";
|
|
423
|
+
if (input === null || typeof input !== "object") return input;
|
|
424
|
+
if (nodeUtilTypes.isProxy(input)) return "[unserializable]";
|
|
425
|
+
if (path.has(input)) return "[Circular]";
|
|
426
|
+
path.add(input);
|
|
427
|
+
const readOwnValue = (key: string): unknown => {
|
|
428
|
+
try {
|
|
429
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
430
|
+
return descriptor === undefined
|
|
431
|
+
? "[unserializable]"
|
|
432
|
+
: "value" in descriptor
|
|
433
|
+
? walk(descriptor.value)
|
|
434
|
+
: "[accessor]";
|
|
435
|
+
} catch {
|
|
436
|
+
return "[unserializable]";
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
try {
|
|
440
|
+
if (Array.isArray(input)) {
|
|
441
|
+
// Own present keys only: iterating the declared length would
|
|
442
|
+
// densify holes, and `Object.keys` is proportional to the
|
|
443
|
+
// elements that actually exist.
|
|
444
|
+
const keys = Object.keys(input);
|
|
445
|
+
if (!takeBudget(keys.length)) return "[truncated]";
|
|
446
|
+
const indexKeys: string[] = [];
|
|
447
|
+
let hasExtraProps = false;
|
|
448
|
+
for (const key of keys) {
|
|
449
|
+
const index = Number(key);
|
|
450
|
+
if (String(index) === key && index >= 0) indexKeys.push(key);
|
|
451
|
+
else hasExtraProps = true;
|
|
452
|
+
}
|
|
453
|
+
let dense = !hasExtraProps;
|
|
454
|
+
if (dense) {
|
|
455
|
+
for (let ordinal = 0; ordinal < indexKeys.length; ordinal++) {
|
|
456
|
+
if (Number(indexKeys[ordinal]) !== ordinal) {
|
|
457
|
+
dense = false;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (dense) {
|
|
463
|
+
const out: unknown[] = [];
|
|
464
|
+
for (const key of indexKeys) out.push(readOwnValue(key));
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
467
|
+
const sparse: Record<string, unknown> = Object.create(null);
|
|
468
|
+
for (const key of indexKeys) sparse[key] = readOwnValue(key);
|
|
469
|
+
return sparse;
|
|
470
|
+
}
|
|
471
|
+
let dateTime: number | undefined;
|
|
472
|
+
try {
|
|
473
|
+
// `isDate` checks the [[DateValue]] internal slot without walking
|
|
474
|
+
// the prototype chain — `instanceof Date` would dispatch a proxy
|
|
475
|
+
// prototype's getPrototypeOf trap and do unbudgeted linear work
|
|
476
|
+
// on deep ordinary chains.
|
|
477
|
+
dateTime = nodeUtilTypes.isDate(input) ? Date.prototype.getTime.call(input) : undefined;
|
|
478
|
+
} catch {
|
|
479
|
+
dateTime = undefined;
|
|
480
|
+
}
|
|
481
|
+
if (dateTime !== undefined) return new Date(dateTime);
|
|
482
|
+
const keys = Object.keys(input);
|
|
483
|
+
if (!takeBudget(keys.length)) return "[truncated]";
|
|
484
|
+
const record: Record<string, unknown> = Object.create(null);
|
|
485
|
+
for (const key of keys) record[key] = readOwnValue(key);
|
|
486
|
+
return record;
|
|
487
|
+
} catch {
|
|
488
|
+
// Brand checks / key enumeration on exotic objects can throw;
|
|
489
|
+
// collapse only this node, not its ancestors.
|
|
490
|
+
return "[unserializable]";
|
|
491
|
+
} finally {
|
|
492
|
+
path.delete(input);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
return walk(value) as T;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Capture an event-time value because providers commonly mutate partial
|
|
500
|
+
* messages in place. The snapshot MUST always be detached from the caller's
|
|
501
|
+
* object graph — replaying a live reference would surface the final mutation
|
|
502
|
+
* instead of the event-time value. It must also never throw: staged payloads
|
|
503
|
+
* can carry non-cloneable objects during provisional assistant streaming
|
|
504
|
+
* (e.g. a live `Headers` inside a provider error's `transportFailure` from a
|
|
505
|
+
* legacy payload), and a thrown `DataCloneError` here would mask the real
|
|
506
|
+
* provider outcome and burn the whole fallback chain.
|
|
507
|
+
*/
|
|
508
|
+
function managedAttemptSnapshotDetailed<T>(value: T): { snapshot: T; degraded: boolean } {
|
|
509
|
+
try {
|
|
510
|
+
return { snapshot: structuredClone(value), degraded: false };
|
|
511
|
+
} catch {
|
|
512
|
+
return { snapshot: sanitizedDetachedClone(value), degraded: true };
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
335
516
|
function managedAttemptSnapshot<T>(value: T): T {
|
|
336
|
-
return
|
|
517
|
+
return managedAttemptSnapshotDetailed(value).snapshot;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Recover the required assistant-message shell when a managed snapshot degrades
|
|
522
|
+
* at its root (notably for Proxy-wrapped provider messages). Only known fields
|
|
523
|
+
* are read, and executable content is retained only when it has its complete
|
|
524
|
+
* discriminant shape.
|
|
525
|
+
*/
|
|
526
|
+
function managedAssistantShell(value: unknown, model: AgentLoopConfig["model"]): AssistantMessage {
|
|
527
|
+
const detailed = managedAttemptSnapshotDetailed(value);
|
|
528
|
+
const source = isManagedPlainRecord(detailed.snapshot) ? detailed.snapshot : value;
|
|
529
|
+
if (managedProperty(source, "role") !== "assistant") throw new ManagedAttemptSnapshotError();
|
|
530
|
+
const rawContent = managedAttemptSnapshot(managedProperty(source, "content"));
|
|
531
|
+
if (!Array.isArray(rawContent)) throw new ManagedAttemptSnapshotError();
|
|
532
|
+
const content = rawContent.flatMap(block => {
|
|
533
|
+
const normalized = managedAssistantContent(block);
|
|
534
|
+
return normalized ? [normalized] : [];
|
|
535
|
+
});
|
|
536
|
+
const usage = managedAssistantUsage(managedAttemptSnapshot(managedProperty(source, "usage")));
|
|
537
|
+
const api = managedProperty(source, "api");
|
|
538
|
+
const provider = managedProperty(source, "provider");
|
|
539
|
+
const messageModel = managedProperty(source, "model");
|
|
540
|
+
const stopReasonValue = managedProperty(source, "stopReason");
|
|
541
|
+
const stopReason =
|
|
542
|
+
stopReasonValue === "stop" ||
|
|
543
|
+
stopReasonValue === "length" ||
|
|
544
|
+
stopReasonValue === "toolUse" ||
|
|
545
|
+
stopReasonValue === "error" ||
|
|
546
|
+
stopReasonValue === "aborted"
|
|
547
|
+
? stopReasonValue
|
|
548
|
+
: "stop";
|
|
549
|
+
const timestamp = managedProperty(source, "timestamp");
|
|
550
|
+
const transportFailure = managedTransportFailure(value);
|
|
551
|
+
const errorMessage = managedProperty(source, "errorMessage");
|
|
552
|
+
const errorStatus = managedProperty(source, "errorStatus");
|
|
553
|
+
const safeMetadata: Record<string, unknown> = isManagedPlainRecord(detailed.snapshot)
|
|
554
|
+
? { ...detailed.snapshot }
|
|
555
|
+
: {};
|
|
556
|
+
delete safeMetadata.errorMessage;
|
|
557
|
+
delete safeMetadata.errorStatus;
|
|
558
|
+
delete safeMetadata.transportFailure;
|
|
559
|
+
return {
|
|
560
|
+
...safeMetadata,
|
|
561
|
+
role: "assistant",
|
|
562
|
+
content,
|
|
563
|
+
api: typeof api === "string" ? (api as AssistantMessage["api"]) : model.api,
|
|
564
|
+
provider: typeof provider === "string" ? (provider as AssistantMessage["provider"]) : model.provider,
|
|
565
|
+
model: typeof messageModel === "string" ? messageModel : model.id,
|
|
566
|
+
usage,
|
|
567
|
+
stopReason,
|
|
568
|
+
timestamp: typeof timestamp === "number" && Number.isFinite(timestamp) ? timestamp : Date.now(),
|
|
569
|
+
...(transportFailure ? { transportFailure } : {}),
|
|
570
|
+
...(typeof errorMessage === "string" ? { errorMessage } : {}),
|
|
571
|
+
...(typeof errorStatus === "number" && Number.isFinite(errorStatus) ? { errorStatus } : {}),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function managedAssistantContent(value: unknown): AssistantMessage["content"][number] | undefined {
|
|
576
|
+
if (!isManagedPlainRecord(value)) return undefined;
|
|
577
|
+
const type = managedProperty(value, "type");
|
|
578
|
+
if (type === "text") {
|
|
579
|
+
const text = managedProperty(value, "text");
|
|
580
|
+
return typeof text === "string" ? { type, text } : undefined;
|
|
581
|
+
}
|
|
582
|
+
if (type === "thinking") {
|
|
583
|
+
const thinking = managedProperty(value, "thinking");
|
|
584
|
+
return typeof thinking === "string" ? { type, thinking } : undefined;
|
|
585
|
+
}
|
|
586
|
+
if (type === "redactedThinking") {
|
|
587
|
+
const data = managedProperty(value, "data");
|
|
588
|
+
return typeof data === "string" ? { type, data } : undefined;
|
|
589
|
+
}
|
|
590
|
+
if (type !== "toolCall") return undefined;
|
|
591
|
+
const id = managedProperty(value, "id");
|
|
592
|
+
const name = managedProperty(value, "name");
|
|
593
|
+
const argumentsValue = managedProperty(value, "arguments");
|
|
594
|
+
if (typeof id !== "string" || typeof name !== "string" || !isManagedPlainRecord(argumentsValue)) return undefined;
|
|
595
|
+
const thoughtSignature = managedProperty(value, "thoughtSignature");
|
|
596
|
+
const intent = managedProperty(value, "intent");
|
|
597
|
+
const customWireName = managedProperty(value, "customWireName");
|
|
598
|
+
const incompleteArguments = managedProperty(value, "incompleteArguments");
|
|
599
|
+
return {
|
|
600
|
+
type,
|
|
601
|
+
id,
|
|
602
|
+
name,
|
|
603
|
+
arguments: argumentsValue,
|
|
604
|
+
...(typeof thoughtSignature === "string" ? { thoughtSignature } : {}),
|
|
605
|
+
...(typeof intent === "string" ? { intent } : {}),
|
|
606
|
+
...(typeof customWireName === "string" ? { customWireName } : {}),
|
|
607
|
+
...(typeof incompleteArguments === "boolean" ? { incompleteArguments } : {}),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function managedAssistantUsage(value: unknown): AssistantMessage["usage"] {
|
|
612
|
+
const number = (key: string): number => {
|
|
613
|
+
const candidate = managedProperty(value, key);
|
|
614
|
+
return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : 0;
|
|
615
|
+
};
|
|
616
|
+
const costValue = managedProperty(value, "cost");
|
|
617
|
+
const costNumber = (key: string): number => {
|
|
618
|
+
const candidate = managedProperty(costValue, key);
|
|
619
|
+
return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : 0;
|
|
620
|
+
};
|
|
621
|
+
return {
|
|
622
|
+
input: number("input"),
|
|
623
|
+
output: number("output"),
|
|
624
|
+
cacheRead: number("cacheRead"),
|
|
625
|
+
cacheWrite: number("cacheWrite"),
|
|
626
|
+
totalTokens: number("totalTokens"),
|
|
627
|
+
cost: {
|
|
628
|
+
input: costNumber("input"),
|
|
629
|
+
output: costNumber("output"),
|
|
630
|
+
cacheRead: costNumber("cacheRead"),
|
|
631
|
+
cacheWrite: costNumber("cacheWrite"),
|
|
632
|
+
total: costNumber("total"),
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function managedAssistantEventSnapshot(event: AssistantMessageEvent, message: AssistantMessage): AssistantMessageEvent {
|
|
638
|
+
const snapshot = managedAttemptSnapshot(event);
|
|
639
|
+
if (!isManagedPlainRecord(snapshot)) throw new ManagedAttemptSnapshotError();
|
|
640
|
+
const type = managedProperty(snapshot, "type");
|
|
641
|
+
const contentIndex = managedProperty(snapshot, "contentIndex");
|
|
642
|
+
const indexed = () => {
|
|
643
|
+
if (!Number.isInteger(contentIndex) || (contentIndex as number) < 0) throw new ManagedAttemptSnapshotError();
|
|
644
|
+
return contentIndex as number;
|
|
645
|
+
};
|
|
646
|
+
if (type === "start") return { type, partial: message };
|
|
647
|
+
if (type === "text_start" || type === "thinking_start" || type === "toolcall_start")
|
|
648
|
+
return { type, contentIndex: indexed(), partial: message };
|
|
649
|
+
if (type === "text_delta" || type === "thinking_delta" || type === "toolcall_delta") {
|
|
650
|
+
const delta = managedProperty(snapshot, "delta");
|
|
651
|
+
if (typeof delta !== "string") throw new ManagedAttemptSnapshotError();
|
|
652
|
+
return { type, contentIndex: indexed(), delta, partial: message };
|
|
653
|
+
}
|
|
654
|
+
if (type === "text_end" || type === "thinking_end") {
|
|
655
|
+
const content = managedProperty(snapshot, "content");
|
|
656
|
+
if (typeof content !== "string") throw new ManagedAttemptSnapshotError();
|
|
657
|
+
return { type, contentIndex: indexed(), content, partial: message };
|
|
658
|
+
}
|
|
659
|
+
if (type === "toolcall_end") {
|
|
660
|
+
const toolCall = managedAssistantContent(managedProperty(snapshot, "toolCall"));
|
|
661
|
+
if (toolCall?.type !== "toolCall") throw new ManagedAttemptSnapshotError();
|
|
662
|
+
return { type, contentIndex: indexed(), toolCall, partial: message };
|
|
663
|
+
}
|
|
664
|
+
if (type === "done") {
|
|
665
|
+
const reason = managedProperty(snapshot, "reason");
|
|
666
|
+
if (reason !== "stop" && reason !== "length" && reason !== "toolUse") throw new ManagedAttemptSnapshotError();
|
|
667
|
+
return { type, reason, message };
|
|
668
|
+
}
|
|
669
|
+
if (type === "error") {
|
|
670
|
+
const reason = managedProperty(snapshot, "reason");
|
|
671
|
+
if (reason !== "aborted" && reason !== "error") throw new ManagedAttemptSnapshotError();
|
|
672
|
+
return { type, reason, error: message };
|
|
673
|
+
}
|
|
674
|
+
throw new ManagedAttemptSnapshotError();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function isManagedPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
678
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !nodeUtilTypes.isProxy(value);
|
|
337
679
|
}
|
|
338
680
|
|
|
339
681
|
/**
|
|
@@ -353,7 +695,10 @@ class ManagedAttemptTransaction {
|
|
|
353
695
|
|
|
354
696
|
constructor(
|
|
355
697
|
private readonly stream: EventStream<AgentEvent, AgentMessage[]>,
|
|
356
|
-
private readonly onAssistantMessageEvent
|
|
698
|
+
private readonly onAssistantMessageEvent:
|
|
699
|
+
| ((message: AssistantMessage, event: AssistantMessageEvent) => void)
|
|
700
|
+
| undefined,
|
|
701
|
+
private readonly model: AgentLoopConfig["model"],
|
|
357
702
|
) {}
|
|
358
703
|
|
|
359
704
|
push(event: AgentEvent): void {
|
|
@@ -369,10 +714,11 @@ class ManagedAttemptTransaction {
|
|
|
369
714
|
}
|
|
370
715
|
|
|
371
716
|
stageAssistantMessageEvent(message: AssistantMessage, event: AssistantMessageEvent): void {
|
|
717
|
+
const partial = managedAssistantShell(message, this.model);
|
|
372
718
|
this.#batch.push({
|
|
373
719
|
type: "assistant_event",
|
|
374
|
-
message:
|
|
375
|
-
event:
|
|
720
|
+
message: partial,
|
|
721
|
+
event: managedAssistantEventSnapshot(event, partial),
|
|
376
722
|
});
|
|
377
723
|
}
|
|
378
724
|
|
|
@@ -398,25 +744,90 @@ class ManagedAttemptTransaction {
|
|
|
398
744
|
this.#discarded = true;
|
|
399
745
|
}
|
|
400
746
|
|
|
747
|
+
#wouldOverflow(bytes: number): boolean {
|
|
748
|
+
return (
|
|
749
|
+
this.#stagedEventCount + 1 > MANAGED_ATTEMPT_MAX_STAGED_EVENTS ||
|
|
750
|
+
this.#stagedBytes + bytes > MANAGED_ATTEMPT_MAX_STAGED_BYTES
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
401
754
|
#stage(event: AgentEvent): void {
|
|
402
|
-
|
|
755
|
+
// Measure the raw event FIRST so an oversized payload is rejected
|
|
756
|
+
// before the snapshot duplicates it — the staged-byte cap exists to
|
|
757
|
+
// bound memory, so cloning ahead of the check would defeat it.
|
|
758
|
+
// Cyclic/JSON-hostile events cannot be pre-measured; only those fall
|
|
759
|
+
// through to snapshot-then-measure, where the sanitized detached form
|
|
760
|
+
// is the cycle-safe estimator.
|
|
761
|
+
let bytes: number | undefined;
|
|
403
762
|
try {
|
|
404
763
|
bytes = managedAttemptTextEncoder.encode(JSON.stringify(event)).byteLength;
|
|
405
764
|
} catch {
|
|
406
|
-
bytes =
|
|
765
|
+
bytes = undefined;
|
|
407
766
|
}
|
|
408
|
-
if (
|
|
409
|
-
this.#stagedEventCount + 1 > MANAGED_ATTEMPT_MAX_STAGED_EVENTS ||
|
|
410
|
-
this.#stagedBytes + bytes > MANAGED_ATTEMPT_MAX_STAGED_BYTES
|
|
411
|
-
) {
|
|
767
|
+
if (bytes !== undefined && this.#wouldOverflow(bytes)) {
|
|
412
768
|
this.discard();
|
|
413
769
|
throw new ManagedAttemptBufferOverflowError();
|
|
414
770
|
}
|
|
415
|
-
|
|
771
|
+
const detailed = managedAttemptSnapshotDetailed(this.#repairAssistantEvent(event));
|
|
772
|
+
let snapshot = detailed.snapshot;
|
|
773
|
+
if (bytes === undefined || detailed.degraded) {
|
|
774
|
+
// Account the bytes of what is actually retained: a degraded
|
|
775
|
+
// snapshot replaces non-JSON leaves with placeholders, so the raw
|
|
776
|
+
// pre-measure (which omits e.g. function-valued properties) can
|
|
777
|
+
// undercount the staged form.
|
|
778
|
+
try {
|
|
779
|
+
bytes = managedAttemptTextEncoder.encode(JSON.stringify(snapshot)).byteLength;
|
|
780
|
+
} catch {
|
|
781
|
+
try {
|
|
782
|
+
snapshot = sanitizedDetachedClone(snapshot);
|
|
783
|
+
bytes = managedAttemptTextEncoder.encode(JSON.stringify(snapshot)).byteLength;
|
|
784
|
+
} catch {
|
|
785
|
+
bytes = undefined;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (bytes === undefined) {
|
|
789
|
+
// The sanitizer's output is total (detached, JSON-safe), so this
|
|
790
|
+
// is unreachable unless the sanitizer itself regresses. Fail as a
|
|
791
|
+
// dedicated local error: it carries no transport facts, so it is
|
|
792
|
+
// non-retryable and can never be misattributed to the provider.
|
|
793
|
+
this.discard();
|
|
794
|
+
throw new ManagedAttemptSnapshotError();
|
|
795
|
+
}
|
|
796
|
+
if (this.#wouldOverflow(bytes)) {
|
|
797
|
+
this.discard();
|
|
798
|
+
throw new ManagedAttemptBufferOverflowError();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
this.#batch.push({ type: "event", event: snapshot });
|
|
416
802
|
this.#stagedEventCount += 1;
|
|
417
803
|
|
|
418
804
|
this.#stagedBytes += bytes;
|
|
419
805
|
}
|
|
806
|
+
|
|
807
|
+
#repairAssistantEvent(event: AgentEvent): AgentEvent {
|
|
808
|
+
if (event.type === "message_start" || event.type === "message_end" || event.type === "turn_end") {
|
|
809
|
+
return event.message.role === "assistant"
|
|
810
|
+
? { ...event, message: managedAssistantShell(event.message, this.model) }
|
|
811
|
+
: event;
|
|
812
|
+
}
|
|
813
|
+
if (event.type === "message_update") {
|
|
814
|
+
const message = managedAssistantShell(event.message, this.model);
|
|
815
|
+
return {
|
|
816
|
+
...event,
|
|
817
|
+
message,
|
|
818
|
+
assistantMessageEvent: managedAssistantEventSnapshot(event.assistantMessageEvent, message),
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
if (event.type === "agent_end") {
|
|
822
|
+
return {
|
|
823
|
+
...event,
|
|
824
|
+
messages: event.messages.map(message =>
|
|
825
|
+
message.role === "assistant" ? managedAssistantShell(message, this.model) : message,
|
|
826
|
+
),
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
return event;
|
|
830
|
+
}
|
|
420
831
|
}
|
|
421
832
|
|
|
422
833
|
/**
|
|
@@ -836,7 +1247,7 @@ async function runLoopBody(
|
|
|
836
1247
|
const transaction =
|
|
837
1248
|
initialTransaction ??
|
|
838
1249
|
(config.fallbackManaged
|
|
839
|
-
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent)
|
|
1250
|
+
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent, config.model)
|
|
840
1251
|
: undefined);
|
|
841
1252
|
initialTransaction = undefined;
|
|
842
1253
|
const attemptStream = transaction ?? stream;
|
|
@@ -931,11 +1342,20 @@ async function runLoopBody(
|
|
|
931
1342
|
harmonyTruncateResumeCount = 0;
|
|
932
1343
|
} catch (err) {
|
|
933
1344
|
if (!(err instanceof HarmonyLeakInterruption)) {
|
|
1345
|
+
const failureMessage = managedFailureMessage(err, config);
|
|
1346
|
+
if (config.fallbackManaged && transaction && managedContextOverflow(failureMessage, config)) {
|
|
1347
|
+
transaction.discard();
|
|
1348
|
+
currentContext.messages.splice(contextMessageCount);
|
|
1349
|
+
newMessages.splice(newMessageCount);
|
|
1350
|
+
await config.onManagedAttemptOutcome?.(managedContextOverflowOutcome(failureMessage));
|
|
1351
|
+
stream.end(newMessages);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
934
1354
|
if (config.fallbackManaged && transaction && managedRetryableFailure(err)) {
|
|
935
1355
|
transaction.discard();
|
|
936
1356
|
currentContext.messages.splice(contextMessageCount);
|
|
937
1357
|
newMessages.splice(newMessageCount);
|
|
938
|
-
await config.onManagedAttemptOutcome?.(managedFailureOutcome(
|
|
1358
|
+
await config.onManagedAttemptOutcome?.(managedFailureOutcome(failureMessage));
|
|
939
1359
|
stream.end(newMessages);
|
|
940
1360
|
return;
|
|
941
1361
|
}
|
|
@@ -1009,17 +1429,23 @@ async function runLoopBody(
|
|
|
1009
1429
|
continue;
|
|
1010
1430
|
}
|
|
1011
1431
|
}
|
|
1432
|
+
|
|
1433
|
+
const overflow = managedContextOverflow(message, config);
|
|
1434
|
+
if (config.fallbackManaged && overflow) {
|
|
1435
|
+
transaction?.discard();
|
|
1436
|
+
currentContext.messages.splice(contextMessageCount);
|
|
1437
|
+
newMessages.splice(newMessageCount);
|
|
1438
|
+
await config.onManagedAttemptOutcome?.(managedContextOverflowOutcome(message));
|
|
1439
|
+
stream.end(newMessages);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1012
1443
|
newMessages.push(message);
|
|
1013
1444
|
modelHasResponded = true;
|
|
1014
1445
|
let steeringMessagesFromExecution: AgentMessage[] | undefined;
|
|
1015
1446
|
|
|
1016
|
-
//
|
|
1017
|
-
|
|
1018
|
-
// window is exceeded, fabricating a near-zero usage instead of surfacing an
|
|
1019
|
-
// error. Without this guard the agent loop treats the empty response as a
|
|
1020
|
-
// natural turn completion and stops, leaving the user with a frozen session.
|
|
1021
|
-
// Promote it to an error so the overflow/compaction recovery path can fire.
|
|
1022
|
-
if (message.stopReason === "stop" && message.content.length === 0 && isEmptyResponseOverflow(message)) {
|
|
1447
|
+
// Preserve the historical public error conversion for unmanaged proxy overflows.
|
|
1448
|
+
if (!config.fallbackManaged && message.stopReason === "stop" && message.content.length === 0 && overflow) {
|
|
1023
1449
|
message.stopReason = "error";
|
|
1024
1450
|
message.errorMessage = message.errorMessage
|
|
1025
1451
|
? `${message.errorMessage} | Provider returned an empty response with anomalously low token usage (possible context overflow via proxy)`
|
|
@@ -1043,6 +1469,14 @@ async function runLoopBody(
|
|
|
1043
1469
|
stream.end(newMessages);
|
|
1044
1470
|
return;
|
|
1045
1471
|
}
|
|
1472
|
+
if (attemptTransaction) {
|
|
1473
|
+
message = managedAssistantShell(message, config.model);
|
|
1474
|
+
const index = currentContext.messages.length - 1;
|
|
1475
|
+
if (index >= 0 && currentContext.messages[index]?.role === "assistant") {
|
|
1476
|
+
currentContext.messages[index] = message;
|
|
1477
|
+
}
|
|
1478
|
+
newMessages[newMessages.length - 1] = message;
|
|
1479
|
+
}
|
|
1046
1480
|
|
|
1047
1481
|
// One provider invocation is committed before any tool can run.
|
|
1048
1482
|
transaction?.flush();
|
|
@@ -1329,7 +1763,9 @@ async function streamAssistantResponse(
|
|
|
1329
1763
|
|
|
1330
1764
|
switch (event.type) {
|
|
1331
1765
|
case "start":
|
|
1332
|
-
partialMessage =
|
|
1766
|
+
partialMessage = config.fallbackManaged
|
|
1767
|
+
? managedAssistantShell(event.partial, config.model)
|
|
1768
|
+
: event.partial;
|
|
1333
1769
|
context.messages.push(partialMessage);
|
|
1334
1770
|
addedPartial = true;
|
|
1335
1771
|
stream.push({ type: "message_start", message: { ...partialMessage } });
|
|
@@ -1345,19 +1781,23 @@ async function streamAssistantResponse(
|
|
|
1345
1781
|
case "thinking_start":
|
|
1346
1782
|
case "thinking_delta":
|
|
1347
1783
|
case "thinking_end":
|
|
1784
|
+
case "reasoning_summary_start":
|
|
1785
|
+
case "reasoning_summary_delta":
|
|
1786
|
+
case "reasoning_summary_end":
|
|
1348
1787
|
case "toolcall_start":
|
|
1349
1788
|
case "toolcall_delta":
|
|
1350
1789
|
case "toolcall_end":
|
|
1351
1790
|
if (partialMessage) {
|
|
1352
|
-
partialMessage =
|
|
1791
|
+
partialMessage = config.fallbackManaged
|
|
1792
|
+
? managedAssistantShell(event.partial, config.model)
|
|
1793
|
+
: event.partial;
|
|
1794
|
+
const partialEvent = config.fallbackManaged ? { ...event, partial: partialMessage } : event;
|
|
1353
1795
|
context.messages[context.messages.length - 1] = partialMessage;
|
|
1354
|
-
config.onAssistantMessageEvent?.(partialMessage,
|
|
1355
|
-
if (signal?.aborted)
|
|
1356
|
-
continue;
|
|
1357
|
-
}
|
|
1796
|
+
config.onAssistantMessageEvent?.(partialMessage, partialEvent);
|
|
1797
|
+
if (signal?.aborted) continue;
|
|
1358
1798
|
stream.push({
|
|
1359
1799
|
type: "message_update",
|
|
1360
|
-
assistantMessageEvent:
|
|
1800
|
+
assistantMessageEvent: partialEvent,
|
|
1361
1801
|
message: { ...partialMessage },
|
|
1362
1802
|
});
|
|
1363
1803
|
}
|
|
@@ -1365,7 +1805,9 @@ async function streamAssistantResponse(
|
|
|
1365
1805
|
|
|
1366
1806
|
case "done":
|
|
1367
1807
|
case "error": {
|
|
1368
|
-
const finalMessage =
|
|
1808
|
+
const finalMessage = config.fallbackManaged
|
|
1809
|
+
? managedAssistantShell(await response.result(), config.model)
|
|
1810
|
+
: await response.result();
|
|
1369
1811
|
if (addedPartial) {
|
|
1370
1812
|
context.messages[context.messages.length - 1] = finalMessage;
|
|
1371
1813
|
} else {
|
|
@@ -1384,7 +1826,9 @@ async function streamAssistantResponse(
|
|
|
1384
1826
|
detachAbortListener?.();
|
|
1385
1827
|
}
|
|
1386
1828
|
|
|
1387
|
-
const trailing =
|
|
1829
|
+
const trailing = config.fallbackManaged
|
|
1830
|
+
? managedAssistantShell(await response.result(), config.model)
|
|
1831
|
+
: await response.result();
|
|
1388
1832
|
await finishChat(trailing);
|
|
1389
1833
|
return trailing;
|
|
1390
1834
|
});
|