@gajae-code/agent-core 0.11.0 → 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 +8 -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 +21 -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 +564 -60
- package/src/agent.ts +36 -3
- package/src/compaction/compaction.ts +79 -92
- package/src/compaction/openai.ts +8 -25
- 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,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
validateToolArguments,
|
|
18
20
|
zodToWireSchema,
|
|
19
21
|
} from "@gajae-code/ai";
|
|
22
|
+
import { isInvalidPromptError, neutralizeReservedControlTokens } from "@gajae-code/ai/utils";
|
|
20
23
|
import { sanitizeText } from "@gajae-code/utils";
|
|
21
24
|
import {
|
|
22
25
|
createHarmonyAuditEvent,
|
|
@@ -66,35 +69,58 @@ import type {
|
|
|
66
69
|
export const MANAGED_ATTEMPT_MAX_STAGED_EVENTS = 10_000;
|
|
67
70
|
export const MANAGED_ATTEMPT_MAX_STAGED_BYTES = 16 * 1024 * 1024;
|
|
68
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
|
+
*/
|
|
69
79
|
class ManagedAttemptBufferOverflowError extends Error {
|
|
70
|
-
readonly status = 503;
|
|
71
|
-
|
|
72
80
|
constructor() {
|
|
73
81
|
super("Managed fallback attempt exceeded the provisional event buffer limit");
|
|
74
82
|
this.name = "ManagedAttemptBufferOverflowError";
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
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
|
+
|
|
78
100
|
const managedAttemptTextEncoder = new TextEncoder();
|
|
79
101
|
|
|
80
102
|
const ABORTED: unique symbol = Symbol("agent-loop-aborted");
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
function isEmptyResponseOverflow(message: AssistantMessage): boolean {
|
|
88
|
-
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);
|
|
89
109
|
}
|
|
90
110
|
|
|
91
|
-
/** Managed fallback owns retry policy; only typed transport facts may discard an attempt. */
|
|
92
|
-
function
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
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;
|
|
96
118
|
}
|
|
97
|
-
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function managedTransportFailure(failure: unknown) {
|
|
122
|
+
const facts = managedProperty(failure, "transportFailure");
|
|
123
|
+
return facts && typeof facts === "object" ? transportFailureFacts(facts) : undefined;
|
|
98
124
|
}
|
|
99
125
|
|
|
100
126
|
function managedRetryableFailure(failure: unknown): boolean {
|
|
@@ -109,6 +135,39 @@ function managedRetryableFailure(failure: unknown): boolean {
|
|
|
109
135
|
);
|
|
110
136
|
}
|
|
111
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Neutralize leaked reserved control tokens in-place across the outgoing
|
|
140
|
+
* history so a re-send no longer carries the poison that triggered
|
|
141
|
+
* `Request blocked (code=invalid_prompt)`. Only string text fields are
|
|
142
|
+
* rewritten; no history item is ever dropped or reordered. Returns whether any
|
|
143
|
+
* byte actually changed — the circuit breaker uses this to decide between a
|
|
144
|
+
* single repaired resend (changed) and immediate fail-fast (unchanged).
|
|
145
|
+
*/
|
|
146
|
+
function repairInvalidPromptHistory(messages: AgentMessage[]): boolean {
|
|
147
|
+
let changed = false;
|
|
148
|
+
const repairString = (value: string): string => {
|
|
149
|
+
const next = neutralizeReservedControlTokens(value);
|
|
150
|
+
if (next !== value) changed = true;
|
|
151
|
+
return next;
|
|
152
|
+
};
|
|
153
|
+
for (const message of messages) {
|
|
154
|
+
const content = (message as { content?: unknown }).content;
|
|
155
|
+
if (typeof content === "string") {
|
|
156
|
+
(message as { content: string }).content = repairString(content);
|
|
157
|
+
} else if (Array.isArray(content)) {
|
|
158
|
+
for (const block of content) {
|
|
159
|
+
if (!block || typeof block !== "object") continue;
|
|
160
|
+
const record = block as Record<string, unknown>;
|
|
161
|
+
for (const key of ["text", "thinking"]) {
|
|
162
|
+
const value = record[key];
|
|
163
|
+
if (typeof value === "string") record[key] = repairString(value);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return changed;
|
|
169
|
+
}
|
|
170
|
+
|
|
112
171
|
function managedFailureOutcome(message: AssistantMessage): ManagedAttemptOutcome {
|
|
113
172
|
return {
|
|
114
173
|
type: "retryable_discarded",
|
|
@@ -116,16 +175,22 @@ function managedFailureOutcome(message: AssistantMessage): ManagedAttemptOutcome
|
|
|
116
175
|
};
|
|
117
176
|
}
|
|
118
177
|
|
|
178
|
+
function managedContextOverflowOutcome(message: AssistantMessage): ManagedAttemptOutcome {
|
|
179
|
+
return { type: "context_overflow_discarded", message };
|
|
180
|
+
}
|
|
181
|
+
|
|
119
182
|
function managedFailureMessage(error: unknown, config: AgentLoopConfig): AssistantMessage {
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
}
|
|
129
194
|
return {
|
|
130
195
|
role: "assistant",
|
|
131
196
|
content: [],
|
|
@@ -141,8 +206,7 @@ function managedFailureMessage(error: unknown, config: AgentLoopConfig): Assista
|
|
|
141
206
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
142
207
|
},
|
|
143
208
|
stopReason: "error",
|
|
144
|
-
errorMessage:
|
|
145
|
-
errorStatus: status,
|
|
209
|
+
errorMessage: fallbackMessage,
|
|
146
210
|
...(transportFailure ? { transportFailure } : {}),
|
|
147
211
|
timestamp: Date.now(),
|
|
148
212
|
};
|
|
@@ -225,7 +289,7 @@ export function agentLoop(
|
|
|
225
289
|
messages: [...context.messages, ...prompts],
|
|
226
290
|
};
|
|
227
291
|
const transaction = config.fallbackManaged
|
|
228
|
-
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent)
|
|
292
|
+
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent, config.model)
|
|
229
293
|
: undefined;
|
|
230
294
|
const attemptStream = transaction ?? stream;
|
|
231
295
|
if (!config.fallbackManaged || emitManagedAgentStart) stream.push({ type: "agent_start" });
|
|
@@ -274,7 +338,7 @@ export function agentLoopContinue(
|
|
|
274
338
|
const newMessages: AgentMessage[] = [];
|
|
275
339
|
const currentContext: AgentContext = { ...context };
|
|
276
340
|
const transaction = config.fallbackManaged
|
|
277
|
-
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent)
|
|
341
|
+
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent, config.model)
|
|
278
342
|
: undefined;
|
|
279
343
|
const attemptStream = transaction ?? stream;
|
|
280
344
|
if (!config.fallbackManaged || emitManagedAgentStart) stream.push({ type: "agent_start" });
|
|
@@ -297,9 +361,321 @@ function createAgentStream(): EventStream<AgentEvent, AgentMessage[]> {
|
|
|
297
361
|
);
|
|
298
362
|
}
|
|
299
363
|
|
|
300
|
-
/**
|
|
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
|
+
|
|
301
516
|
function managedAttemptSnapshot<T>(value: T): T {
|
|
302
|
-
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);
|
|
303
679
|
}
|
|
304
680
|
|
|
305
681
|
/**
|
|
@@ -319,7 +695,10 @@ class ManagedAttemptTransaction {
|
|
|
319
695
|
|
|
320
696
|
constructor(
|
|
321
697
|
private readonly stream: EventStream<AgentEvent, AgentMessage[]>,
|
|
322
|
-
private readonly onAssistantMessageEvent
|
|
698
|
+
private readonly onAssistantMessageEvent:
|
|
699
|
+
| ((message: AssistantMessage, event: AssistantMessageEvent) => void)
|
|
700
|
+
| undefined,
|
|
701
|
+
private readonly model: AgentLoopConfig["model"],
|
|
323
702
|
) {}
|
|
324
703
|
|
|
325
704
|
push(event: AgentEvent): void {
|
|
@@ -335,10 +714,11 @@ class ManagedAttemptTransaction {
|
|
|
335
714
|
}
|
|
336
715
|
|
|
337
716
|
stageAssistantMessageEvent(message: AssistantMessage, event: AssistantMessageEvent): void {
|
|
717
|
+
const partial = managedAssistantShell(message, this.model);
|
|
338
718
|
this.#batch.push({
|
|
339
719
|
type: "assistant_event",
|
|
340
|
-
message:
|
|
341
|
-
event:
|
|
720
|
+
message: partial,
|
|
721
|
+
event: managedAssistantEventSnapshot(event, partial),
|
|
342
722
|
});
|
|
343
723
|
}
|
|
344
724
|
|
|
@@ -364,25 +744,90 @@ class ManagedAttemptTransaction {
|
|
|
364
744
|
this.#discarded = true;
|
|
365
745
|
}
|
|
366
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
|
+
|
|
367
754
|
#stage(event: AgentEvent): void {
|
|
368
|
-
|
|
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;
|
|
369
762
|
try {
|
|
370
763
|
bytes = managedAttemptTextEncoder.encode(JSON.stringify(event)).byteLength;
|
|
371
764
|
} catch {
|
|
372
|
-
bytes =
|
|
765
|
+
bytes = undefined;
|
|
373
766
|
}
|
|
374
|
-
if (
|
|
375
|
-
this.#stagedEventCount + 1 > MANAGED_ATTEMPT_MAX_STAGED_EVENTS ||
|
|
376
|
-
this.#stagedBytes + bytes > MANAGED_ATTEMPT_MAX_STAGED_BYTES
|
|
377
|
-
) {
|
|
767
|
+
if (bytes !== undefined && this.#wouldOverflow(bytes)) {
|
|
378
768
|
this.discard();
|
|
379
769
|
throw new ManagedAttemptBufferOverflowError();
|
|
380
770
|
}
|
|
381
|
-
|
|
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 });
|
|
382
802
|
this.#stagedEventCount += 1;
|
|
383
803
|
|
|
384
804
|
this.#stagedBytes += bytes;
|
|
385
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
|
+
}
|
|
386
831
|
}
|
|
387
832
|
|
|
388
833
|
/**
|
|
@@ -789,6 +1234,9 @@ async function runLoopBody(
|
|
|
789
1234
|
// first iteration is skipped to avoid duplicating/racing it.
|
|
790
1235
|
let modelHasResponded = false;
|
|
791
1236
|
let harmonyTruncateResumeCount = 0;
|
|
1237
|
+
// Fires at most one repaired resend per run for the poisoned-history
|
|
1238
|
+
// `invalid_prompt` circuit breaker below.
|
|
1239
|
+
let invalidPromptRepairAttempted = false;
|
|
792
1240
|
|
|
793
1241
|
// Outer loop: continues when queued follow-up messages arrive after agent would stop
|
|
794
1242
|
while (true) {
|
|
@@ -799,7 +1247,7 @@ async function runLoopBody(
|
|
|
799
1247
|
const transaction =
|
|
800
1248
|
initialTransaction ??
|
|
801
1249
|
(config.fallbackManaged
|
|
802
|
-
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent)
|
|
1250
|
+
? new ManagedAttemptTransaction(stream, config.onAssistantMessageEvent, config.model)
|
|
803
1251
|
: undefined);
|
|
804
1252
|
initialTransaction = undefined;
|
|
805
1253
|
const attemptStream = transaction ?? stream;
|
|
@@ -894,11 +1342,20 @@ async function runLoopBody(
|
|
|
894
1342
|
harmonyTruncateResumeCount = 0;
|
|
895
1343
|
} catch (err) {
|
|
896
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
|
+
}
|
|
897
1354
|
if (config.fallbackManaged && transaction && managedRetryableFailure(err)) {
|
|
898
1355
|
transaction.discard();
|
|
899
1356
|
currentContext.messages.splice(contextMessageCount);
|
|
900
1357
|
newMessages.splice(newMessageCount);
|
|
901
|
-
await config.onManagedAttemptOutcome?.(managedFailureOutcome(
|
|
1358
|
+
await config.onManagedAttemptOutcome?.(managedFailureOutcome(failureMessage));
|
|
902
1359
|
stream.end(newMessages);
|
|
903
1360
|
return;
|
|
904
1361
|
}
|
|
@@ -949,17 +1406,46 @@ async function runLoopBody(
|
|
|
949
1406
|
continue;
|
|
950
1407
|
}
|
|
951
1408
|
}
|
|
1409
|
+
// Session-level invalid_prompt circuit breaker (bounded, neutralize-only).
|
|
1410
|
+
// A poisoned-history rejection (`Request blocked (code=invalid_prompt)`) is
|
|
1411
|
+
// a deterministic content fault: re-sending the same history re-triggers it,
|
|
1412
|
+
// so naive session auto-retry would burn its whole budget re-poisoning the
|
|
1413
|
+
// model. On the first invalid_prompt of this run, neutralize leaked control
|
|
1414
|
+
// tokens in history IN PLACE (never dropping items). If that changed the
|
|
1415
|
+
// outgoing bytes, resend exactly once with the repaired history; if
|
|
1416
|
+
// neutralization cannot change anything (nothing left to repair), fall
|
|
1417
|
+
// through to terminal handling and fail fast. Budget = one repaired resend.
|
|
1418
|
+
// Runs before the response is committed so the resend is a clean retry;
|
|
1419
|
+
// managed fallback owns its own retry policy, so this is scoped to the
|
|
1420
|
+
// non-managed session path where uncontrolled auto-retry would recur.
|
|
1421
|
+
if (
|
|
1422
|
+
!config.fallbackManaged &&
|
|
1423
|
+
message.stopReason === "error" &&
|
|
1424
|
+
!invalidPromptRepairAttempted &&
|
|
1425
|
+
isInvalidPromptError(message)
|
|
1426
|
+
) {
|
|
1427
|
+
invalidPromptRepairAttempted = true;
|
|
1428
|
+
if (repairInvalidPromptHistory(currentContext.messages)) {
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
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
|
+
|
|
952
1443
|
newMessages.push(message);
|
|
953
1444
|
modelHasResponded = true;
|
|
954
1445
|
let steeringMessagesFromExecution: AgentMessage[] | undefined;
|
|
955
1446
|
|
|
956
|
-
//
|
|
957
|
-
|
|
958
|
-
// window is exceeded, fabricating a near-zero usage instead of surfacing an
|
|
959
|
-
// error. Without this guard the agent loop treats the empty response as a
|
|
960
|
-
// natural turn completion and stops, leaving the user with a frozen session.
|
|
961
|
-
// Promote it to an error so the overflow/compaction recovery path can fire.
|
|
962
|
-
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) {
|
|
963
1449
|
message.stopReason = "error";
|
|
964
1450
|
message.errorMessage = message.errorMessage
|
|
965
1451
|
? `${message.errorMessage} | Provider returned an empty response with anomalously low token usage (possible context overflow via proxy)`
|
|
@@ -983,6 +1469,14 @@ async function runLoopBody(
|
|
|
983
1469
|
stream.end(newMessages);
|
|
984
1470
|
return;
|
|
985
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
|
+
}
|
|
986
1480
|
|
|
987
1481
|
// One provider invocation is committed before any tool can run.
|
|
988
1482
|
transaction?.flush();
|
|
@@ -1269,7 +1763,9 @@ async function streamAssistantResponse(
|
|
|
1269
1763
|
|
|
1270
1764
|
switch (event.type) {
|
|
1271
1765
|
case "start":
|
|
1272
|
-
partialMessage =
|
|
1766
|
+
partialMessage = config.fallbackManaged
|
|
1767
|
+
? managedAssistantShell(event.partial, config.model)
|
|
1768
|
+
: event.partial;
|
|
1273
1769
|
context.messages.push(partialMessage);
|
|
1274
1770
|
addedPartial = true;
|
|
1275
1771
|
stream.push({ type: "message_start", message: { ...partialMessage } });
|
|
@@ -1285,19 +1781,23 @@ async function streamAssistantResponse(
|
|
|
1285
1781
|
case "thinking_start":
|
|
1286
1782
|
case "thinking_delta":
|
|
1287
1783
|
case "thinking_end":
|
|
1784
|
+
case "reasoning_summary_start":
|
|
1785
|
+
case "reasoning_summary_delta":
|
|
1786
|
+
case "reasoning_summary_end":
|
|
1288
1787
|
case "toolcall_start":
|
|
1289
1788
|
case "toolcall_delta":
|
|
1290
1789
|
case "toolcall_end":
|
|
1291
1790
|
if (partialMessage) {
|
|
1292
|
-
partialMessage =
|
|
1791
|
+
partialMessage = config.fallbackManaged
|
|
1792
|
+
? managedAssistantShell(event.partial, config.model)
|
|
1793
|
+
: event.partial;
|
|
1794
|
+
const partialEvent = config.fallbackManaged ? { ...event, partial: partialMessage } : event;
|
|
1293
1795
|
context.messages[context.messages.length - 1] = partialMessage;
|
|
1294
|
-
config.onAssistantMessageEvent?.(partialMessage,
|
|
1295
|
-
if (signal?.aborted)
|
|
1296
|
-
continue;
|
|
1297
|
-
}
|
|
1796
|
+
config.onAssistantMessageEvent?.(partialMessage, partialEvent);
|
|
1797
|
+
if (signal?.aborted) continue;
|
|
1298
1798
|
stream.push({
|
|
1299
1799
|
type: "message_update",
|
|
1300
|
-
assistantMessageEvent:
|
|
1800
|
+
assistantMessageEvent: partialEvent,
|
|
1301
1801
|
message: { ...partialMessage },
|
|
1302
1802
|
});
|
|
1303
1803
|
}
|
|
@@ -1305,7 +1805,9 @@ async function streamAssistantResponse(
|
|
|
1305
1805
|
|
|
1306
1806
|
case "done":
|
|
1307
1807
|
case "error": {
|
|
1308
|
-
const finalMessage =
|
|
1808
|
+
const finalMessage = config.fallbackManaged
|
|
1809
|
+
? managedAssistantShell(await response.result(), config.model)
|
|
1810
|
+
: await response.result();
|
|
1309
1811
|
if (addedPartial) {
|
|
1310
1812
|
context.messages[context.messages.length - 1] = finalMessage;
|
|
1311
1813
|
} else {
|
|
@@ -1324,7 +1826,9 @@ async function streamAssistantResponse(
|
|
|
1324
1826
|
detachAbortListener?.();
|
|
1325
1827
|
}
|
|
1326
1828
|
|
|
1327
|
-
const trailing =
|
|
1829
|
+
const trailing = config.fallbackManaged
|
|
1830
|
+
? managedAssistantShell(await response.result(), config.model)
|
|
1831
|
+
: await response.result();
|
|
1328
1832
|
await finishChat(trailing);
|
|
1329
1833
|
return trailing;
|
|
1330
1834
|
});
|