@librechat/agents 4.0.2 → 4.0.4

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.
Files changed (162) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +3 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/decisions/structuredChat.cjs +2 -8
  4. package/dist/cjs/decisions/structuredChat.cjs.map +1 -1
  5. package/dist/cjs/decisions/structuredOutput.cjs +18 -0
  6. package/dist/cjs/decisions/structuredOutput.cjs.map +1 -0
  7. package/dist/cjs/graphs/Graph.cjs +25 -9
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/llm/google/index.cjs +1 -1
  10. package/dist/cjs/llm/invoke.cjs +34 -12
  11. package/dist/cjs/llm/invoke.cjs.map +1 -1
  12. package/dist/cjs/llm/openai/index.cjs +7 -0
  13. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  14. package/dist/cjs/llm/providerTextBoundary.cjs +355 -0
  15. package/dist/cjs/llm/providerTextBoundary.cjs.map +1 -0
  16. package/dist/cjs/llm/providerTextChunk.cjs +14 -0
  17. package/dist/cjs/llm/providerTextChunk.cjs.map +1 -0
  18. package/dist/cjs/llm/providerTextControls.cjs +236 -0
  19. package/dist/cjs/llm/providerTextControls.cjs.map +1 -0
  20. package/dist/cjs/llm/stream/smoother.cjs +2 -1
  21. package/dist/cjs/llm/stream/smoother.cjs.map +1 -1
  22. package/dist/cjs/main.cjs +18 -1
  23. package/dist/cjs/protection/providerText.cjs +224 -0
  24. package/dist/cjs/protection/providerText.cjs.map +1 -0
  25. package/dist/cjs/protection/providerTextInput.cjs +25 -0
  26. package/dist/cjs/protection/providerTextInput.cjs.map +1 -0
  27. package/dist/cjs/run.cjs +7 -1
  28. package/dist/cjs/run.cjs.map +1 -1
  29. package/dist/cjs/stream.cjs +42 -14
  30. package/dist/cjs/stream.cjs.map +1 -1
  31. package/dist/cjs/summarization/node.cjs +26 -8
  32. package/dist/cjs/summarization/node.cjs.map +1 -1
  33. package/dist/cjs/tools/GitHubCompare.cjs +181 -0
  34. package/dist/cjs/tools/GitHubCompare.cjs.map +1 -0
  35. package/dist/cjs/tools/SubagentTool.cjs +11 -3
  36. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  37. package/dist/cjs/tools/ToolNode.cjs +6 -5
  38. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  39. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +2 -2
  40. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -1
  41. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +66 -16
  42. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  43. package/dist/cjs/tools/subagent/SubagentReplay.cjs +4 -2
  44. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  45. package/dist/cjs/tools/subagent/childGraphConfig.cjs +3 -1
  46. package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -1
  47. package/dist/cjs/tools/subagent/diagnostics.cjs +48 -5
  48. package/dist/cjs/tools/subagent/diagnostics.cjs.map +1 -1
  49. package/dist/cjs/tools/subagent/hostArgs.cjs +214 -0
  50. package/dist/cjs/tools/subagent/hostArgs.cjs.map +1 -0
  51. package/dist/cjs/tools/subagent/index.cjs +1 -0
  52. package/dist/cjs/tools/toolErrorContent.cjs +1 -1
  53. package/dist/esm/agents/AgentContext.mjs +3 -2
  54. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  55. package/dist/esm/decisions/structuredChat.mjs +2 -8
  56. package/dist/esm/decisions/structuredChat.mjs.map +1 -1
  57. package/dist/esm/decisions/structuredOutput.mjs +18 -0
  58. package/dist/esm/decisions/structuredOutput.mjs.map +1 -0
  59. package/dist/esm/graphs/Graph.mjs +25 -9
  60. package/dist/esm/graphs/Graph.mjs.map +1 -1
  61. package/dist/esm/llm/google/index.mjs +1 -1
  62. package/dist/esm/llm/invoke.mjs +34 -12
  63. package/dist/esm/llm/invoke.mjs.map +1 -1
  64. package/dist/esm/llm/openai/index.mjs +7 -0
  65. package/dist/esm/llm/openai/index.mjs.map +1 -1
  66. package/dist/esm/llm/providerTextBoundary.mjs +355 -0
  67. package/dist/esm/llm/providerTextBoundary.mjs.map +1 -0
  68. package/dist/esm/llm/providerTextChunk.mjs +13 -0
  69. package/dist/esm/llm/providerTextChunk.mjs.map +1 -0
  70. package/dist/esm/llm/providerTextControls.mjs +230 -0
  71. package/dist/esm/llm/providerTextControls.mjs.map +1 -0
  72. package/dist/esm/llm/stream/smoother.mjs +2 -1
  73. package/dist/esm/llm/stream/smoother.mjs.map +1 -1
  74. package/dist/esm/main.mjs +6 -3
  75. package/dist/esm/protection/providerText.mjs +218 -0
  76. package/dist/esm/protection/providerText.mjs.map +1 -0
  77. package/dist/esm/protection/providerTextInput.mjs +24 -0
  78. package/dist/esm/protection/providerTextInput.mjs.map +1 -0
  79. package/dist/esm/run.mjs +7 -1
  80. package/dist/esm/run.mjs.map +1 -1
  81. package/dist/esm/stream.mjs +42 -14
  82. package/dist/esm/stream.mjs.map +1 -1
  83. package/dist/esm/summarization/node.mjs +26 -8
  84. package/dist/esm/summarization/node.mjs.map +1 -1
  85. package/dist/esm/tools/GitHubCompare.mjs +175 -0
  86. package/dist/esm/tools/GitHubCompare.mjs.map +1 -0
  87. package/dist/esm/tools/SubagentTool.mjs +11 -3
  88. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  89. package/dist/esm/tools/ToolNode.mjs +6 -5
  90. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  91. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +2 -2
  92. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -1
  93. package/dist/esm/tools/subagent/SubagentExecutor.mjs +66 -16
  94. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  95. package/dist/esm/tools/subagent/SubagentReplay.mjs +4 -2
  96. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  97. package/dist/esm/tools/subagent/childGraphConfig.mjs +3 -1
  98. package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -1
  99. package/dist/esm/tools/subagent/diagnostics.mjs +46 -6
  100. package/dist/esm/tools/subagent/diagnostics.mjs.map +1 -1
  101. package/dist/esm/tools/subagent/hostArgs.mjs +207 -0
  102. package/dist/esm/tools/subagent/hostArgs.mjs.map +1 -0
  103. package/dist/esm/tools/subagent/index.mjs +1 -0
  104. package/dist/esm/tools/toolErrorContent.mjs +1 -1
  105. package/dist/types/decisions/structuredChat.d.ts +3 -2
  106. package/dist/types/decisions/structuredOutput.d.ts +4 -0
  107. package/dist/types/graphs/Graph.d.ts +6 -3
  108. package/dist/types/index.d.ts +3 -0
  109. package/dist/types/llm/invoke.d.ts +4 -1
  110. package/dist/types/llm/providerTextBoundary.d.ts +4 -0
  111. package/dist/types/llm/providerTextChunk.d.ts +7 -0
  112. package/dist/types/llm/providerTextControls.d.ts +18 -0
  113. package/dist/types/protection/providerText.d.ts +77 -0
  114. package/dist/types/protection/providerTextInput.d.ts +3 -0
  115. package/dist/types/run.d.ts +1 -0
  116. package/dist/types/tools/BashExecutor.d.ts +4 -0
  117. package/dist/types/tools/CodeExecutor.d.ts +4 -0
  118. package/dist/types/tools/GitHubCompare.d.ts +104 -0
  119. package/dist/types/tools/ReadFile.d.ts +4 -0
  120. package/dist/types/tools/SkillTool.d.ts +4 -0
  121. package/dist/types/tools/SubagentTool.d.ts +2 -0
  122. package/dist/types/tools/ToolSearch.d.ts +4 -0
  123. package/dist/types/tools/search/schema.d.ts +4 -0
  124. package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +2 -0
  125. package/dist/types/tools/subagent/SubagentExecutor.d.ts +10 -2
  126. package/dist/types/tools/subagent/SubagentReplay.d.ts +3 -1
  127. package/dist/types/tools/subagent/diagnostics.d.ts +27 -3
  128. package/dist/types/tools/subagent/hostArgs.d.ts +57 -0
  129. package/dist/types/tools/subagent/index.d.ts +3 -2
  130. package/dist/types/types/graph.d.ts +36 -0
  131. package/dist/types/types/run.d.ts +4 -2
  132. package/dist/types/types/tools.d.ts +2 -0
  133. package/package.json +4 -4
  134. package/src/agents/AgentContext.ts +3 -2
  135. package/src/decisions/structuredChat.ts +9 -15
  136. package/src/decisions/structuredOutput.ts +25 -0
  137. package/src/graphs/Graph.ts +53 -9
  138. package/src/index.ts +8 -0
  139. package/src/llm/invoke.ts +48 -7
  140. package/src/llm/openai/index.ts +8 -0
  141. package/src/llm/providerTextBoundary.ts +446 -0
  142. package/src/llm/providerTextChunk.ts +15 -0
  143. package/src/llm/providerTextControls.ts +173 -0
  144. package/src/llm/stream/smoother.ts +4 -2
  145. package/src/protection/providerText.ts +325 -0
  146. package/src/protection/providerTextInput.ts +21 -0
  147. package/src/run.ts +9 -0
  148. package/src/stream.ts +33 -7
  149. package/src/summarization/node.ts +32 -14
  150. package/src/tools/GitHubCompare.ts +319 -0
  151. package/src/tools/SubagentTool.ts +14 -3
  152. package/src/tools/ToolNode.ts +7 -3
  153. package/src/tools/subagent/SubagentExecutionRegistry.ts +6 -2
  154. package/src/tools/subagent/SubagentExecutor.ts +145 -18
  155. package/src/tools/subagent/SubagentReplay.ts +10 -3
  156. package/src/tools/subagent/childGraphConfig.ts +12 -1
  157. package/src/tools/subagent/diagnostics.ts +98 -6
  158. package/src/tools/subagent/hostArgs.ts +413 -0
  159. package/src/tools/subagent/index.ts +9 -0
  160. package/src/types/graph.ts +41 -0
  161. package/src/types/run.ts +4 -2
  162. package/src/types/tools.ts +6 -1
@@ -0,0 +1,325 @@
1
+ import { performance } from 'node:perf_hooks';
2
+ import { PreparedSubagentError } from '@/tools/preparedSubagents';
3
+ import { StreamLimitExceededError } from '@/llm/streamLimits';
4
+
5
+ export const PROVIDER_TEXT_PROTECTION_VERSION = 1;
6
+
7
+ export type ProviderTextProtectionErrorCode =
8
+ | 'blocked'
9
+ | 'unavailable'
10
+ | 'timeout'
11
+ | 'overflow'
12
+ | 'cancelled'
13
+ | 'unsupported'
14
+ | 'incompatible';
15
+
16
+ export type ProviderTextProtectionResult =
17
+ | {
18
+ readonly version: 1;
19
+ readonly ok: true;
20
+ readonly value: {
21
+ readonly content: string;
22
+ readonly replacements: number;
23
+ readonly categories: readonly {
24
+ readonly category: string;
25
+ readonly count: number;
26
+ }[];
27
+ };
28
+ }
29
+ | {
30
+ readonly version: 1;
31
+ readonly ok: false;
32
+ readonly error: { readonly code: ProviderTextProtectionErrorCode };
33
+ };
34
+
35
+ /** Trusted host adapter. Classification must reject code, JSON and ambiguous text. */
36
+ export interface ProviderTextProtection {
37
+ readonly version: 1;
38
+ readonly timeoutMs: number;
39
+ readonly maxAttemptBytes: number;
40
+ /** Shared across all attempts using this policy, including child graphs. */
41
+ readonly maxBufferedBytes: number;
42
+ readonly classify: (content: string) => 'prose' | 'unsupported';
43
+ readonly inspect: (input: {
44
+ readonly version: 1;
45
+ readonly content: string;
46
+ readonly target: {
47
+ readonly source: 'message';
48
+ readonly field: 'text';
49
+ readonly provenance: 'model';
50
+ };
51
+ readonly signal: AbortSignal;
52
+ }) => ProviderTextProtectionResult | Promise<ProviderTextProtectionResult>;
53
+ }
54
+
55
+ export class ProviderTextProtectionError extends Error {
56
+ constructor(readonly code: ProviderTextProtectionErrorCode) {
57
+ const safeCode = errorCodes.has(code) ? code : 'incompatible';
58
+ super(`Provider text protection: ${safeCode}`);
59
+ this.code = safeCode;
60
+ this.name = 'ProviderTextProtectionError';
61
+ }
62
+ }
63
+
64
+ const restartCancellations = new WeakSet<ProviderTextProtectionError>();
65
+
66
+ /** Internal control flow, never a host policy decision or public capability. */
67
+ export function createProviderTextRestartCancellation(): ProviderTextProtectionError {
68
+ const error = new ProviderTextProtectionError('cancelled');
69
+ restartCancellations.add(error);
70
+ return error;
71
+ }
72
+
73
+ export function isProviderTextRestartCancellation(error: unknown): boolean {
74
+ return error instanceof ProviderTextProtectionError && restartCancellations.has(error);
75
+ }
76
+
77
+ function protectionAbortError(parent?: AbortSignal): ProviderTextProtectionError | StreamLimitExceededError | PreparedSubagentError {
78
+ const reason: unknown = parent?.reason;
79
+ if (reason instanceof StreamLimitExceededError || reason instanceof PreparedSubagentError) return reason;
80
+ if (reason instanceof ProviderTextProtectionError) {
81
+ return isProviderTextRestartCancellation(reason) ? reason : new ProviderTextProtectionError(reason.code);
82
+ }
83
+ return new ProviderTextProtectionError('cancelled');
84
+ }
85
+
86
+ const protectedSignals = new WeakSet<AbortSignal>();
87
+
88
+ /** Provider smoothing must stay lazy while the release boundary owns raw text. */
89
+ export function isProtectedProviderTextSignal(signal?: AbortSignal): boolean {
90
+ return signal != null && protectedSignals.has(signal);
91
+ }
92
+
93
+ const budgets = new WeakMap<ProviderTextProtection, { bytes: number }>();
94
+ const errorCodes = new Set<ProviderTextProtectionErrorCode>([
95
+ 'blocked',
96
+ 'unavailable',
97
+ 'timeout',
98
+ 'overflow',
99
+ 'cancelled',
100
+ 'unsupported',
101
+ 'incompatible',
102
+ ]);
103
+
104
+ export function validateProviderTextProtection(
105
+ policy: ProviderTextProtection
106
+ ): void {
107
+ const version: number = policy.version;
108
+ if (version !== 1)
109
+ throw new ProviderTextProtectionError('incompatible');
110
+ if (
111
+ typeof policy.inspect !== 'function' ||
112
+ typeof policy.classify !== 'function'
113
+ ) {
114
+ throw new ProviderTextProtectionError('unavailable');
115
+ }
116
+ for (const value of [
117
+ policy.timeoutMs,
118
+ policy.maxAttemptBytes,
119
+ policy.maxBufferedBytes,
120
+ ]) {
121
+ if (!Number.isSafeInteger(value) || value <= 0) {
122
+ throw new ProviderTextProtectionError('incompatible');
123
+ }
124
+ }
125
+ if (
126
+ policy.timeoutMs > 2_147_483_647 ||
127
+ policy.maxAttemptBytes > policy.maxBufferedBytes
128
+ ) {
129
+ throw new ProviderTextProtectionError('incompatible');
130
+ }
131
+ }
132
+
133
+ /** A lease stays charged while a cancellation-ignoring producer/handler still owns raw text. */
134
+ export class ProviderTextAttempt {
135
+ private readonly controller = new AbortController();
136
+ private readonly budget: { bytes: number };
137
+ private readonly deadline: number;
138
+ private bytes = 0;
139
+ private textLength = 0;
140
+ private readonly fragments: string[] = [];
141
+ private readonly pending = new Set<Promise<unknown>>();
142
+ private readonly timer: ReturnType<typeof setTimeout>;
143
+ private readonly abort: () => void;
144
+ private finished = false;
145
+
146
+ constructor(
147
+ private readonly policy: ProviderTextProtection,
148
+ private readonly parent?: AbortSignal
149
+ ) {
150
+ validateProviderTextProtection(policy);
151
+ this.deadline = performance.now() + policy.timeoutMs;
152
+ this.budget = budgets.get(policy) ?? { bytes: 0 };
153
+ budgets.set(policy, this.budget);
154
+ if (parent?.aborted === true) throw protectionAbortError(parent);
155
+ this.charge(512);
156
+ protectedSignals.add(this.signal);
157
+ this.abort = (): void =>
158
+ this.controller.abort(protectionAbortError(this.parent));
159
+ parent?.addEventListener('abort', this.abort, { once: true });
160
+ this.timer = setTimeout(() => {
161
+ this.controller.abort(new ProviderTextProtectionError('timeout'));
162
+ }, policy.timeoutMs);
163
+ this.timer.unref();
164
+ }
165
+
166
+ get signal(): AbortSignal {
167
+ return this.controller.signal;
168
+ }
169
+
170
+ check(): void {
171
+ if (this.signal.aborted) throw this.signal.reason;
172
+ if (this.parent?.aborted === true)
173
+ throw protectionAbortError(this.parent);
174
+ if (performance.now() >= this.deadline) {
175
+ this.controller.abort(new ProviderTextProtectionError('timeout'));
176
+ throw this.signal.reason;
177
+ }
178
+ }
179
+
180
+ private charge(bytes: number): void {
181
+ if (
182
+ this.bytes + bytes > this.policy.maxAttemptBytes ||
183
+ this.budget.bytes + bytes > this.policy.maxBufferedBytes
184
+ ) {
185
+ this.controller.abort(new ProviderTextProtectionError('overflow'));
186
+ throw this.signal.reason;
187
+ }
188
+ this.bytes += bytes;
189
+ this.budget.bytes += bytes;
190
+ }
191
+
192
+ append(text: string, cumulativeReplay = false): void {
193
+ this.check();
194
+ if (cumulativeReplay && this.hasCandidatePrefix(text)) text = text.slice(this.textLength);
195
+ if (!text) return;
196
+ const bytes = text.length * 2;
197
+ this.charge(bytes * 2 + 64);
198
+ this.fragments.push(text);
199
+ this.textLength += text.length;
200
+ }
201
+
202
+ /** Compare buffered fragments without allocating an uncharged joined copy. */
203
+ private hasCandidatePrefix(text: string): boolean {
204
+ if (this.textLength === 0 || text.length < this.textLength) return false;
205
+ let offset = 0;
206
+ for (const fragment of this.fragments) {
207
+ if (!text.startsWith(fragment, offset)) return false;
208
+ offset += fragment.length;
209
+ }
210
+ return true;
211
+ }
212
+
213
+ matchesText(text: string): boolean {
214
+ return text.length === this.textLength && (text.length === 0 || this.hasCandidatePrefix(text));
215
+ }
216
+
217
+ retain(bytes: number): void {
218
+ this.check();
219
+ this.charge(bytes);
220
+ }
221
+
222
+ observeChunk(): void {
223
+ this.check();
224
+ this.charge(128);
225
+ }
226
+
227
+ async wait<T>(work: Promise<T>): Promise<T> {
228
+ this.pending.add(work);
229
+ return new Promise<T>((resolve, reject) => {
230
+ const abort = (): void => {
231
+ this.signal.removeEventListener('abort', abort);
232
+ reject(this.signal.reason);
233
+ };
234
+ this.signal.addEventListener('abort', abort, { once: true });
235
+ if (this.signal.aborted) abort();
236
+ const settle = (): void => {
237
+ this.signal.removeEventListener('abort', abort);
238
+ this.pending.delete(work);
239
+ };
240
+ void work.then(
241
+ (value) => { settle(); resolve(value); },
242
+ (error: Error) => { settle(); reject(error); }
243
+ );
244
+ });
245
+ }
246
+
247
+ async release(): Promise<string> {
248
+ this.check();
249
+ const content = this.fragments.join('');
250
+ if (!content) return '';
251
+ let result: ProviderTextProtectionResult;
252
+ try {
253
+ const treatment = this.policy.classify(content);
254
+ this.check();
255
+ if (treatment !== 'prose') {
256
+ throw new ProviderTextProtectionError('unsupported');
257
+ }
258
+ result = await this.wait(
259
+ Promise.resolve(
260
+ this.policy.inspect({
261
+ version: 1,
262
+ content,
263
+ target: { source: 'message', field: 'text', provenance: 'model' },
264
+ signal: this.signal,
265
+ })
266
+ )
267
+ );
268
+ } catch (error) {
269
+ this.check();
270
+ if (isProviderTextRestartCancellation(error)) throw error;
271
+ if (error instanceof ProviderTextProtectionError) throw new ProviderTextProtectionError(error.code);
272
+ throw new ProviderTextProtectionError('unavailable');
273
+ }
274
+ this.check();
275
+ const canonical = validateResult(result);
276
+ this.check();
277
+ this.charge(canonical.length * 2);
278
+ return canonical;
279
+ }
280
+
281
+ finish(): void {
282
+ if (this.finished) return;
283
+ this.finished = true;
284
+ clearTimeout(this.timer);
285
+ this.parent?.removeEventListener('abort', this.abort);
286
+ const dispose = (): void => {
287
+ this.fragments.length = 0;
288
+ this.budget.bytes -= this.bytes;
289
+ this.bytes = 0;
290
+ };
291
+ if (this.pending.size === 0) dispose();
292
+ else void Promise.allSettled([...this.pending]).then(dispose);
293
+ }
294
+ }
295
+
296
+ interface UncheckedResult {
297
+ version?: number;
298
+ ok?: boolean;
299
+ value?: { content?: string; replacements?: number; categories?: readonly { category?: string; count?: number }[] };
300
+ error?: { code?: ProviderTextProtectionErrorCode };
301
+ }
302
+
303
+ function validateResult(result: UncheckedResult | undefined): string {
304
+ try {
305
+ if (result?.version !== 1 || typeof result.ok !== 'boolean') {
306
+ throw new ProviderTextProtectionError('incompatible');
307
+ }
308
+ if (!result.ok) {
309
+ const code = result.error?.code;
310
+ throw new ProviderTextProtectionError(code != null && errorCodes.has(code) ? code : 'incompatible');
311
+ }
312
+ const value = result.value;
313
+ if (typeof value?.content !== 'string' || typeof value.replacements !== 'number' ||
314
+ !Number.isSafeInteger(value.replacements) || value.replacements < 0 ||
315
+ !Array.isArray(value.categories) || value.categories.length > 32 ||
316
+ value.categories.some((entry) => typeof entry.category !== 'string' ||
317
+ typeof entry.count !== 'number' || !Number.isSafeInteger(entry.count) || entry.count <= 0)) {
318
+ throw new ProviderTextProtectionError('incompatible');
319
+ }
320
+ return value.content;
321
+ } catch (error) {
322
+ if (error instanceof ProviderTextProtectionError) throw error;
323
+ throw new ProviderTextProtectionError('incompatible');
324
+ }
325
+ }
@@ -0,0 +1,21 @@
1
+ const inputs = new WeakMap<object, { prototype: object | null; descriptors: PropertyDescriptorMap }>();
2
+
3
+ /** Internal provenance for the SDK's pure instruction transform, not a host capability. */
4
+ export function markProviderTextInput<T extends object>(input: T): T {
5
+ inputs.set(input, { prototype: Object.getPrototypeOf(input), descriptors: Object.getOwnPropertyDescriptors(input) });
6
+ return input;
7
+ }
8
+
9
+ export function isProviderTextInput(input: object): boolean {
10
+ const original = inputs.get(input);
11
+ if (original == null || original.prototype !== Object.getPrototypeOf(input)) return false;
12
+ const current: Partial<PropertyDescriptorMap> = Object.getOwnPropertyDescriptors(input);
13
+ const keys = Object.keys(original.descriptors);
14
+ return keys.length === Object.keys(current).length && keys.every((key) => {
15
+ const before = original.descriptors[key];
16
+ const after = current[key];
17
+ return after != null && before.value === after.value && before.get === after.get &&
18
+ before.set === after.set && before.writable === after.writable &&
19
+ before.enumerable === after.enumerable && before.configurable === after.configurable;
20
+ });
21
+ }
package/src/run.ts CHANGED
@@ -102,6 +102,7 @@ import {
102
102
  createTitleRunnable,
103
103
  } from '@/utils/title';
104
104
  import { applyGraphRuntimeConfig } from '@/graphs/applyGraphRuntimeConfig';
105
+ import { ProviderTextProtectionError } from '@/protection/providerText';
105
106
  import { LANGFUSE_OPERATION_METADATA_KEY } from '@/langfuseOperation';
106
107
  import { createTokenCounter, encodingForModel } from '@/utils/tokens';
107
108
  import { stampSyntheticProviderMessage } from '@/messages/provenance';
@@ -477,6 +478,7 @@ export class Run<_T extends t.BaseGraphState> {
477
478
  private subagentUsageSink?: t.SubagentUsageSink;
478
479
  private preemption?: t.StreamPreemption;
479
480
  private maxStopContinuations: number;
481
+ private providerTextProtection?: t.StandardGraphInput['providerTextProtection'];
480
482
  private streamLimits?: t.StreamLimits;
481
483
  private subagentTasks?: t.SubagentTaskConfig;
482
484
  private subagentContext?: t.SubagentContextAdapter;
@@ -578,10 +580,15 @@ export class Run<_T extends t.BaseGraphState> {
578
580
  config.maxStopContinuations
579
581
  );
580
582
  this.streamLimits = config.streamLimits;
583
+ this.providerTextProtection = config.providerTextProtection;
581
584
 
582
585
  if (!config.graphConfig) {
583
586
  throw new Error('Graph config not provided');
584
587
  }
588
+ if ('providerTextProtection' in config.graphConfig &&
589
+ config.graphConfig.providerTextProtection != null) {
590
+ throw new ProviderTextProtectionError('incompatible');
591
+ }
585
592
 
586
593
  /** Handle different graph types */
587
594
  if (config.graphConfig.type === 'multi-agent') {
@@ -678,6 +685,7 @@ export class Run<_T extends t.BaseGraphState> {
678
685
  onSubagentResolutionFailure: this.onSubagentResolutionFailure,
679
686
  preemption: this.preemption,
680
687
  streamLimits: this.streamLimits,
688
+ providerTextProtection: this.providerTextProtection,
681
689
  toolExecution: this.toolExecution,
682
690
  clientDelegatedToolNames: this.clientDelegatedToolNames,
683
691
  },
@@ -725,6 +733,7 @@ export class Run<_T extends t.BaseGraphState> {
725
733
  onSubagentResolutionFailure: this.onSubagentResolutionFailure,
726
734
  preemption: this.preemption,
727
735
  streamLimits: this.streamLimits,
736
+ providerTextProtection: this.providerTextProtection,
728
737
  toolExecution: this.toolExecution,
729
738
  },
730
739
  });
package/src/stream.ts CHANGED
@@ -58,6 +58,7 @@ import {
58
58
  import { createToolCallsDispatchedEvent, safeDispatchCustomEvent } from '@/utils/events';
59
59
  import { resolveToolOutcome, outcomeFieldsFromResult } from '@/tools/intentArg';
60
60
  import { snapshotValidatedModelChunk } from '@/graphs/acceptedModelResponse';
61
+ import { ProviderTextProtectionError } from '@/protection/providerText';
61
62
  import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
62
63
  import { formatToolErrorContent } from '@/tools/toolErrorContent';
63
64
  import { PreparedSubagentError } from '@/tools/preparedSubagents';
@@ -1687,6 +1688,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
1687
1688
  graph.breakerAbort instanceof AbortController
1688
1689
  ? graph.breakerAbort
1689
1690
  : undefined;
1691
+ const entrySignals = [eventBreaker?.signal, graph.signal, graph.config.signal];
1690
1692
  /** Immutable scope captured at handler entry. A reset while this
1691
1693
  * handler is suspended in an await replaces the object, so ONE
1692
1694
  * reference comparison proves the event still belongs to the live run
@@ -1695,13 +1697,16 @@ export class ChatModelStreamHandler implements t.EventHandler {
1695
1697
  const runScopeInvalidated = (): boolean =>
1696
1698
  entryRunScope != null && graph.runScope !== entryRunScope;
1697
1699
  const throwIfRunBreakerTripped = (): void => {
1698
- if (
1699
- eventBreaker != null &&
1700
- eventBreaker.signal.aborted &&
1701
- (eventBreaker.signal.reason instanceof StreamLimitExceededError ||
1702
- eventBreaker.signal.reason instanceof PreparedSubagentError)
1703
- ) {
1704
- throw eventBreaker.signal.reason;
1700
+ for (const signal of entrySignals) {
1701
+ if (signal?.aborted === true &&
1702
+ (signal.reason instanceof StreamLimitExceededError ||
1703
+ signal.reason instanceof PreparedSubagentError ||
1704
+ signal.reason instanceof ProviderTextProtectionError)) {
1705
+ throw signal.reason;
1706
+ }
1707
+ if (signal?.aborted === true && graph.providerTextProtection != null) {
1708
+ throw new ProviderTextProtectionError('cancelled');
1709
+ }
1705
1710
  }
1706
1711
  };
1707
1712
 
@@ -1797,6 +1802,8 @@ export class ChatModelStreamHandler implements t.EventHandler {
1797
1802
  metadata,
1798
1803
  agentContext,
1799
1804
  });
1805
+ if (runScopeInvalidated()) return;
1806
+ throwIfRunBreakerTripped();
1800
1807
  if (skipHandling) {
1801
1808
  return;
1802
1809
  }
@@ -1831,6 +1838,8 @@ export class ChatModelStreamHandler implements t.EventHandler {
1831
1838
  content,
1832
1839
  metadata,
1833
1840
  });
1841
+ if (runScopeInvalidated()) return;
1842
+ throwIfRunBreakerTripped();
1834
1843
  }
1835
1844
 
1836
1845
  if (
@@ -2033,6 +2042,23 @@ export class ChatModelStreamHandler implements t.EventHandler {
2033
2042
  });
2034
2043
  runStep = graph.getRunStep(stepId);
2035
2044
  }
2045
+ if (
2046
+ graph.providerTextProtection != null &&
2047
+ runStep?.type === StepTypes.TOOL_CALLS &&
2048
+ agentContext.currentTokenType === ContentTypes.TEXT &&
2049
+ (typeof content === 'string' || content.every(isTextContentPart))
2050
+ ) {
2051
+ stepId = await dispatchMessageCreationStep({
2052
+ graph,
2053
+ stepKey,
2054
+ content,
2055
+ contentType: ContentTypes.TEXT,
2056
+ metadata,
2057
+ });
2058
+ if (runScopeInvalidated()) return;
2059
+ throwIfRunBreakerTripped();
2060
+ runStep = graph.getRunStep(stepId);
2061
+ }
2036
2062
  if (!runStep) {
2037
2063
  console.warn(`\n
2038
2064
  ==============================================================
@@ -56,6 +56,7 @@ import {
56
56
  resolveClientOptionsModel,
57
57
  } from '@/llm/request';
58
58
  import { renderCompactionSemanticIndex } from '@/summarization/semanticIndex';
59
+ import { ProviderTextProtectionError } from '@/protection/providerText';
59
60
  import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
60
61
  import { prepareToolsForPromptCache } from '@/llm/promptCacheTools';
61
62
  import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
@@ -739,16 +740,6 @@ async function executeSummarizationWithFallback(params: {
739
740
  summaryText = result.text;
740
741
  summaryUsage = result.usage;
741
742
  } catch (primaryError) {
742
- const primaryDescribed = describeProviderError(
743
- primaryError,
744
- clientConfig.provider,
745
- clientConfig.modelName
746
- );
747
- log('error', `Summarization LLM call failed ${primaryDescribed.suffix}`, {
748
- ...primaryDescribed.data,
749
- messagesToRefineCount: messages.length,
750
- });
751
-
752
743
  /**
753
744
  * A tripped stream circuit breaker is a safety abort, not a
754
745
  * summarization failure to recover from: retrying on fallback providers
@@ -758,7 +749,7 @@ async function executeSummarizationWithFallback(params: {
758
749
  * rejects the run. Rethrown so the summarize node fails the run with
759
750
  * the actionable limit error, consistent with agent turns and subagents.
760
751
  */
761
- if (primaryError instanceof StreamLimitExceededError) {
752
+ if (primaryError instanceof StreamLimitExceededError || primaryError instanceof ProviderTextProtectionError) {
762
753
  throw primaryError;
763
754
  }
764
755
  /** A parallel branch's trip aborts the composed summarization signal,
@@ -777,6 +768,16 @@ async function executeSummarizationWithFallback(params: {
777
768
  }
778
769
  }
779
770
 
771
+ const primaryDescribed = describeProviderError(
772
+ primaryError,
773
+ clientConfig.provider,
774
+ clientConfig.modelName
775
+ );
776
+ log('error', `Summarization LLM call failed ${primaryDescribed.suffix}`, {
777
+ ...primaryDescribed.data,
778
+ messagesToRefineCount: messages.length,
779
+ });
780
+
780
781
  const rawFallbacks = (
781
782
  clientConfig.clientOptions as unknown as t.LLMConfig | undefined
782
783
  )?.fallbacks;
@@ -819,7 +820,9 @@ async function executeSummarizationWithFallback(params: {
819
820
  );
820
821
  }
821
822
  } catch (fbErr) {
822
- if (fbErr instanceof StreamLimitExceededError) {
823
+ const trip = findStreamLimitAbortReason(graph?.getBreakerSignal?.(), summarizeConfig?.signal);
824
+ if (trip != null) throw trip;
825
+ if (fbErr instanceof StreamLimitExceededError || fbErr instanceof ProviderTextProtectionError) {
823
826
  throw fbErr;
824
827
  }
825
828
  const fbDescribed = describeFallbackError(fbErr, fallbacks);
@@ -842,6 +845,8 @@ async function executeSummarizationWithFallback(params: {
842
845
  }
843
846
  }
844
847
 
848
+ const trip = findStreamLimitAbortReason(graph?.getBreakerSignal?.(), summarizeConfig?.signal);
849
+ if (trip != null) throw trip;
845
850
  return { text: summaryText, usage: summaryUsage, usedMetadataStub };
846
851
  }
847
852
 
@@ -875,6 +880,11 @@ async function dispatchCompletionEvents(params: {
875
880
  messagesAfterCount,
876
881
  } = params;
877
882
 
883
+ const assertNotTripped = (): void => {
884
+ const trip = findStreamLimitAbortReason(graph.getBreakerSignal?.(), runnableConfig?.signal);
885
+ if (trip != null) throw trip;
886
+ };
887
+ assertNotTripped();
878
888
  runStep.summary = summaryBlock;
879
889
  if (summaryUsage) {
880
890
  runStep.usage = {
@@ -892,6 +902,7 @@ async function dispatchCompletionEvents(params: {
892
902
  runnableConfig
893
903
  );
894
904
 
905
+ assertNotTripped();
895
906
  if (runnableConfig) {
896
907
  await safeDispatchCustomEvent(
897
908
  GraphEvents.ON_SUMMARIZE_COMPLETE,
@@ -904,6 +915,7 @@ async function dispatchCompletionEvents(params: {
904
915
  );
905
916
  }
906
917
 
918
+ assertNotTripped();
907
919
  const sessionId = graph.runId ?? '';
908
920
  if (graph.hookRegistry?.hasHookFor('PostCompact', sessionId) === true) {
909
921
  const threadId = (
@@ -1017,11 +1029,11 @@ function withBreakerSignal<T extends { signal?: AbortSignal }>(
1017
1029
  * child's private breaker never fires for it. */
1018
1030
  function findStreamLimitAbortReason(
1019
1031
  ...signals: Array<AbortSignal | undefined>
1020
- ): StreamLimitExceededError | undefined {
1032
+ ): StreamLimitExceededError | ProviderTextProtectionError | undefined {
1021
1033
  for (const signal of signals) {
1022
1034
  if (
1023
1035
  signal?.aborted === true &&
1024
- signal.reason instanceof StreamLimitExceededError
1036
+ (signal.reason instanceof StreamLimitExceededError || signal.reason instanceof ProviderTextProtectionError)
1025
1037
  ) {
1026
1038
  return signal.reason;
1027
1039
  }
@@ -1526,6 +1538,8 @@ export function createSummarizeNode({
1526
1538
  return { summarizationRequest: undefined };
1527
1539
  }
1528
1540
 
1541
+ const postCallTrip = findStreamLimitAbortReason(entryBreakerSignal, config?.signal);
1542
+ if (postCallTrip != null) throw postCallTrip;
1529
1543
  const summaryText = enrichSummary(rawText, messagesToRefine);
1530
1544
 
1531
1545
  const tokenCount = await computeSummaryTokenCount(
@@ -1533,6 +1547,8 @@ export function createSummarizeNode({
1533
1547
  agentContext
1534
1548
  );
1535
1549
 
1550
+ const commitTrip = findStreamLimitAbortReason(entryBreakerSignal, config?.signal);
1551
+ if (commitTrip != null) throw commitTrip;
1536
1552
  if (usedIntraTurnFallback) {
1537
1553
  agentContext.setSummary(summaryText, tokenCount, {
1538
1554
  precedesMessages: true,
@@ -1712,6 +1728,8 @@ export function createSummarizationChunkHandler({
1712
1728
  throw error;
1713
1729
  }
1714
1730
  }
1731
+ const protectionTrip = findStreamLimitAbortReason(graph?.getBreakerController?.().signal, config.signal);
1732
+ if (protectionTrip != null) throw protectionTrip;
1715
1733
  const chunkAny = chunk as Parameters<typeof getChunkContent>[0]['chunk'];
1716
1734
  const raw = getChunkContent({ chunk: chunkAny, provider, reasoningKey });
1717
1735
  if (raw == null || (typeof raw === 'string' && !raw)) {