@librechat/agents 3.1.71-dev.1 → 3.1.72

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.
@@ -12,12 +12,14 @@ function makeToolMessage(fields: {
12
12
  name?: string;
13
13
  tool_call_id?: string;
14
14
  status?: 'success' | 'error';
15
+ artifact?: unknown;
15
16
  additional_kwargs?: Record<string, unknown>;
16
17
  }): ToolMessage {
17
18
  return new ToolMessage({
18
19
  name: fields.name ?? 'echo',
19
20
  tool_call_id: fields.tool_call_id ?? 'tc1',
20
21
  status: fields.status ?? 'success',
22
+ artifact: fields.artifact,
21
23
  additional_kwargs: fields.additional_kwargs,
22
24
  content: fields.content,
23
25
  });
@@ -190,6 +192,31 @@ describe('annotateMessagesForLLM', () => {
190
192
  expect(blocks[2].type).toBe('image_url');
191
193
  });
192
194
 
195
+ it('preserves artifact on the projected ToolMessage', () => {
196
+ /**
197
+ * Hosts attach `artifact` to ToolMessages via the
198
+ * `content_and_artifact` response format (e.g. code execution
199
+ * sessions, MCP tools that return structured side-data). The
200
+ * projection must round-trip the artifact untouched so downstream
201
+ * consumers (audit logs, code-session tracking) keep working.
202
+ */
203
+ const registry = new ToolOutputReferenceRegistry();
204
+ registry.set('r1', 'tool0turn0', 'raw');
205
+ const artifact = {
206
+ session_id: 'abc',
207
+ files: [{ id: 'f1', name: 'a.txt' }],
208
+ };
209
+ const tm = makeToolMessage({
210
+ content: 'output',
211
+ artifact,
212
+ additional_kwargs: { _refKey: 'tool0turn0' },
213
+ });
214
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
215
+ const projected = out[0] as ToolMessage;
216
+ expect(projected.artifact).toBe(artifact);
217
+ expect(projected.content).toBe('[ref: tool0turn0]\noutput');
218
+ });
219
+
193
220
  it('does not mutate the original ToolMessage instance or its content', () => {
194
221
  const registry = new ToolOutputReferenceRegistry();
195
222
  registry.set('r1', 'tool0turn0', 'raw');
@@ -404,6 +431,39 @@ describe('annotateMessagesForLLM', () => {
404
431
  expect((out[0] as ToolMessage).additional_kwargs._refKey).toBeUndefined();
405
432
  });
406
433
 
434
+ it('skips a ToolMessage whose additional_kwargs is a primitive without throwing', () => {
435
+ /**
436
+ * The `in` operator throws `TypeError` on primitives, so without
437
+ * a runtime object guard, a hydrated ToolMessage carrying e.g.
438
+ * `additional_kwargs: 'not-an-object'` (from a buggy serializer)
439
+ * would crash `attemptInvoke` before the provider call. Verify
440
+ * we skip that message and process subsequent live-ref messages
441
+ * normally.
442
+ */
443
+ const registry = new ToolOutputReferenceRegistry();
444
+ registry.set('r1', 'tool0turn0', 'raw');
445
+
446
+ const malformed = new ToolMessage({
447
+ name: 'echo',
448
+ tool_call_id: 'mal',
449
+ status: 'success',
450
+ content: 'malformed-output',
451
+ });
452
+ /* Force a primitive past LangChain's typed setter via a cast. */
453
+ (malformed as unknown as { additional_kwargs: unknown }).additional_kwargs =
454
+ 'not-an-object' as unknown;
455
+
456
+ const live = makeToolMessage({
457
+ content: 'live-output',
458
+ additional_kwargs: { _refKey: 'tool0turn0' },
459
+ });
460
+
461
+ const out = annotateMessagesForLLM([malformed, live], registry, 'r1');
462
+ /* Malformed message passes through unchanged; live ref still annotates. */
463
+ expect(out[0]).toBe(malformed);
464
+ expect(out[1].content).toBe('[ref: tool0turn0]\nlive-output');
465
+ });
466
+
407
467
  it('treats stale _refKey but live unresolved as unresolved-only', () => {
408
468
  const registry = new ToolOutputReferenceRegistry();
409
469
  const tm = makeToolMessage({
@@ -642,16 +642,19 @@ export function annotateMessagesForLLM(
642
642
  /**
643
643
  * `additional_kwargs` is untyped at the LangChain layer
644
644
  * (`Record<string, unknown>`), so persisted or client-supplied
645
- * ToolMessages can carry arbitrary shapes under our framework
646
- * keys. Treat them as untrusted input and coerce defensively
647
- * before any array operation a malformed field on a single
648
- * hydrated message must not crash `attemptInvoke` before the
649
- * provider call.
645
+ * ToolMessages can carry arbitrary shapes including primitives
646
+ * (a malformed serializer might write a string, or `null`).
647
+ * Guard with a runtime object check before the `in` probes
648
+ * because the `in` operator throws `TypeError` on primitives.
649
+ * A single malformed message must never crash the provider call
650
+ * path; skip its annotation/strip and continue.
650
651
  */
651
- const meta = m.additional_kwargs as Record<string, unknown> | undefined;
652
- const hasRefKey = meta != null && '_refKey' in meta;
653
- const hasRefScope = meta != null && '_refScope' in meta;
654
- const hasUnresolvedField = meta != null && '_unresolvedRefs' in meta;
652
+ const rawMeta = m.additional_kwargs as unknown;
653
+ if (rawMeta == null || typeof rawMeta !== 'object') continue;
654
+ const meta = rawMeta as Record<string, unknown>;
655
+ const hasRefKey = '_refKey' in meta;
656
+ const hasRefScope = '_refScope' in meta;
657
+ const hasUnresolvedField = '_unresolvedRefs' in meta;
655
658
  if (!hasRefKey && !hasRefScope && !hasUnresolvedField) continue;
656
659
 
657
660
  const refKey = readRefKey(meta);
@@ -688,6 +691,15 @@ export function annotateMessagesForLLM(
688
691
  type: 'text' as const,
689
692
  text: `[unresolved refs: ${unresolved.join(', ')}]`,
690
693
  };
694
+ /**
695
+ * `as unknown as ToolMessage['content']` is unavoidable here:
696
+ * LangChain's content union (`MessageContentComplex[] |
697
+ * DataContentBlock[] | string`) does not accept a freshly built
698
+ * mixed array literal even though the structural shape is valid
699
+ * at runtime. The double-cast is structurally safe — we
700
+ * preserve every block from `tm.content` and prepend a single
701
+ * `{ type: 'text', text }` block that all providers accept.
702
+ */
691
703
  nextContent = [
692
704
  warningBlock,
693
705
  ...tm.content,