unique-sdk 2026.30.0.dev4__tar.gz → 2026.30.0.dev5__tar.gz

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 (88) hide show
  1. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/PKG-INFO +1 -1
  2. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/pyproject.toml +1 -1
  3. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/mcp.py +88 -6
  4. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/README.md +0 -0
  5. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/__init__.py +0 -0
  6. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_api_requestor.py +0 -0
  7. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_api_resource.py +0 -0
  8. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_api_version.py +0 -0
  9. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_error.py +0 -0
  10. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_http_client.py +0 -0
  11. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_list_object.py +0 -0
  12. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_object_classes.py +0 -0
  13. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_request_options.py +0 -0
  14. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_unique_object.py +0 -0
  15. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_unique_ql.py +0 -0
  16. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_unique_response.py +0 -0
  17. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_util.py +0 -0
  18. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_version.py +0 -0
  19. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/_webhook.py +0 -0
  20. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/__init__.py +0 -0
  21. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_acronyms.py +0 -0
  22. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_agentic_table.py +0 -0
  23. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_analytics_order.py +0 -0
  24. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_benchmarking.py +0 -0
  25. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_briefing.py +0 -0
  26. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_chat_completion.py +0 -0
  27. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_content.py +0 -0
  28. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_dynamic_frontend.py +0 -0
  29. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_elicitation.py +0 -0
  30. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_embedding.py +0 -0
  31. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_event.py +0 -0
  32. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_folder.py +0 -0
  33. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_group.py +0 -0
  34. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_integrated.py +0 -0
  35. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_llm_models.py +0 -0
  36. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_mcp.py +0 -0
  37. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_message.py +0 -0
  38. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_message_assessment.py +0 -0
  39. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_message_execution.py +0 -0
  40. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_message_log.py +0 -0
  41. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_message_tool.py +0 -0
  42. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_module.py +0 -0
  43. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  44. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_search.py +0 -0
  45. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_search_string.py +0 -0
  46. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  47. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_space.py +0 -0
  48. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_user.py +0 -0
  49. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_web_search.py +0 -0
  50. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/__init__.py +0 -0
  51. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/__main__.py +0 -0
  52. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/cli.py +0 -0
  53. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/__init__.py +0 -0
  54. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
  55. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/browser.py +0 -0
  56. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/cite_file.py +0 -0
  57. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/dynamic_frontend.py +0 -0
  58. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/elicitation.py +0 -0
  59. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/files.py +0 -0
  60. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/folders.py +0 -0
  61. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/navigation.py +0 -0
  62. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/read.py +0 -0
  63. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  64. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/search.py +0 -0
  65. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/subagent.py +0 -0
  66. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/web_search.py +0 -0
  67. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/web_search_config.py +0 -0
  68. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/config.py +0 -0
  69. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/formatting.py +0 -0
  70. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/metadata_filter.py +0 -0
  71. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/shell.py +0 -0
  72. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-dynamic-frontend/SKILL.md +0 -0
  73. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  74. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
  75. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  76. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  77. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  78. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
  79. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
  80. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  81. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/state.py +0 -0
  82. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/utils/analytics_order_run.py +0 -0
  83. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/utils/benchmarking_run.py +0 -0
  84. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/utils/chat_history.py +0 -0
  85. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/utils/chat_in_space.py +0 -0
  86. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/utils/file_io.py +0 -0
  87. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/utils/sources.py +0 -0
  88. {unique_sdk-2026.30.0.dev4 → unique_sdk-2026.30.0.dev5}/unique_sdk/utils/token.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-sdk
3
- Version: 2026.30.0.dev4
3
+ Version: 2026.30.0.dev5
4
4
  Summary:
5
5
  Author: Martin Fadler, Konstantin Krauss, Andreas Hauri
6
6
  Author-email: Martin Fadler <martin.fadler@unique.ch>, Konstantin Krauss <konstantin@unique.ch>, Andreas Hauri <andreas@unique.ch>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_sdk"
3
- version = "2026.30.0.dev4"
3
+ version = "2026.30.0.dev5"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -45,6 +45,13 @@ _MCP_OUTPUT_TEXT_CHAR_LIMIT = 200_000
45
45
  _MCP_REFS_LOG_RELATIVE_PATH = Path(".unique") / "mcp-refs.jsonl"
46
46
  _MCP_REFS_LOCK_FILENAME = "mcp-refs.lock"
47
47
  _MCP_SNIPPET_CHAR_LIMIT = 300
48
+ # Writer-side cap on the per-item ``text`` recorded in the refs manifest — the
49
+ # cited item's underlying text, consumed by the runner's hallucination check to
50
+ # ground each ``[mcpsourceN]`` citation on what was actually retrieved
51
+ # (UN-22762). Half the flat-output cap (``_MCP_OUTPUT_TEXT_CHAR_LIMIT``): one
52
+ # cited item (a page, an issue record) rarely exceeds it, and the eval side
53
+ # bounds the combined cited-text payload separately.
54
+ _MCP_REF_TEXT_CHAR_LIMIT = 100_000
48
55
 
49
56
  # Keys an MCP tool's JSON result commonly uses for a record's human title.
50
57
  _TITLE_KEYS = ("title", "name", "displayName", "subject", "summary", "key")
@@ -247,6 +254,7 @@ def _titles_from_json(text: str) -> list[dict[str, Any]]:
247
254
  "title": title,
248
255
  "snippet": None,
249
256
  "details": _details_from_json(entry),
257
+ "text": _record_text(entry),
250
258
  }
251
259
  )
252
260
  return items
@@ -350,6 +358,31 @@ def _first_text_title(response: Any, max_chars: int) -> str | None:
350
358
  return None
351
359
 
352
360
 
361
+ def _all_text_blocks(response: Any) -> str | None:
362
+ """Concatenation of every text block — the underlying text of a single-item
363
+ result (e.g. a fetched document) recorded as that item's ground truth."""
364
+ texts: list[str] = []
365
+ for block in getattr(response, "content", None) or []:
366
+ if not isinstance(block, dict) or block.get("type") != "text":
367
+ continue
368
+ text = block.get("text")
369
+ if isinstance(text, str) and text.strip():
370
+ texts.append(text)
371
+ return "\n\n".join(texts) or None
372
+
373
+
374
+ def _record_text(record: Any) -> str | None:
375
+ """A record's underlying text for citation grounding: the record itself
376
+ when it is a plain string, else the serialized record — which carries the
377
+ titled field plus all metadata the agent may cite."""
378
+ if isinstance(record, str):
379
+ return record or None
380
+ try:
381
+ return json.dumps(record, ensure_ascii=False, default=str)
382
+ except (TypeError, ValueError):
383
+ return None
384
+
385
+
353
386
  def _extract_with_reference_mapping(
354
387
  response: Any, mapping: dict[str, Any]
355
388
  ) -> list[dict[str, Any]]:
@@ -409,6 +442,7 @@ def _extract_with_reference_mapping(
409
442
  "title": title,
410
443
  "snippet": None,
411
444
  "details": str(details).strip() if details else None,
445
+ "text": _record_text(record),
412
446
  }
413
447
  )
414
448
  if items:
@@ -421,7 +455,14 @@ def _extract_with_reference_mapping(
421
455
  if not records and title_from_text:
422
456
  title = _first_text_title(response, title_max_chars)
423
457
  if title:
424
- return [{"title": title, "snippet": None, "details": None}]
458
+ return [
459
+ {
460
+ "title": title,
461
+ "snippet": None,
462
+ "details": None,
463
+ "text": _all_text_blocks(response),
464
+ }
465
+ ]
425
466
  return items
426
467
 
427
468
 
@@ -431,8 +472,9 @@ def _extract_mcp_citation_items(
431
472
  tool_name: str,
432
473
  server_name: str | None,
433
474
  reference_mapping: dict[str, Any] | None = None,
475
+ fallback_text: str | None = None,
434
476
  ) -> list[dict[str, Any]]:
435
- """Context for what the tool retrieved: ``{title, snippet}`` per item.
477
+ """Context for what the tool retrieved: ``{title, snippet, text}`` per item.
436
478
 
437
479
  An optional admin ``reference_mapping`` is applied first (deterministic
438
480
  destructuring of a list result); when it yields nothing we fall back to the
@@ -441,6 +483,12 @@ def _extract_mcp_citation_items(
441
483
  JSON-in-text). No URLs are extracted — the chip is display-only. Falls back
442
484
  to a single title-less item (the runner names it after the tool) when the
443
485
  result carries no recognizable title.
486
+
487
+ ``text`` is the item's underlying retrieved text (the serialized record, a
488
+ fetched document body, or — for the title-less fallback — ``fallback_text``,
489
+ the whole formatted output). The runner grounds the hallucination check for
490
+ each cited ``[mcpsourceN]`` on it. A ``resource_link`` carries no body, so
491
+ its ``text`` is the link description only.
444
492
  """
445
493
  if reference_mapping:
446
494
  mapped = _extract_with_reference_mapping(response, reference_mapping)
@@ -457,7 +505,11 @@ def _extract_mcp_citation_items(
457
505
  name = (block.get("name") or "").strip()
458
506
  if name:
459
507
  items.append(
460
- {"title": name, "snippet": _snippet(block.get("description"))}
508
+ {
509
+ "title": name,
510
+ "snippet": _snippet(block.get("description")),
511
+ "text": block.get("description") or None,
512
+ }
461
513
  )
462
514
 
463
515
  if not items:
@@ -467,7 +519,7 @@ def _extract_mcp_citation_items(
467
519
 
468
520
  if not items:
469
521
  # No recognizable title — one chip named after the tool itself.
470
- items.append({"title": None, "snippet": None})
522
+ items.append({"title": None, "snippet": None, "text": fallback_text})
471
523
 
472
524
  return items
473
525
 
@@ -489,6 +541,16 @@ def _item_dedup_key(tool_name: str, item: dict[str, Any]) -> str:
489
541
  return f"tool:{tool_name}"
490
542
 
491
543
 
544
+ def _ref_text(item: dict[str, Any]) -> str | None:
545
+ """The item's underlying text for the manifest, capped at
546
+ ``_MCP_REF_TEXT_CHAR_LIMIT`` (single write-side cap shared by all
547
+ extraction modes)."""
548
+ text = item.get("text")
549
+ if not isinstance(text, str) or not text:
550
+ return None
551
+ return text[:_MCP_REF_TEXT_CHAR_LIMIT]
552
+
553
+
492
554
  def _annotate_mcp_results_for_citations(
493
555
  response: Any,
494
556
  *,
@@ -496,6 +558,7 @@ def _annotate_mcp_results_for_citations(
496
558
  server_name: str | None,
497
559
  refs_log_path: Path | None = None,
498
560
  reference_mapping: dict[str, Any] | None = None,
561
+ fallback_text: str | None = None,
499
562
  ) -> list[tuple[int, dict[str, Any]]]:
500
563
  """Assign per-turn ``[mcpsourceN]`` numbers to each retrieved item and append
501
564
  the refs manifest. Returns ``[(sourceNumber, item)]`` for the Sources block.
@@ -512,6 +575,7 @@ def _annotate_mcp_results_for_citations(
512
575
  tool_name=tool_name,
513
576
  server_name=server_name,
514
577
  reference_mapping=reference_mapping,
578
+ fallback_text=fallback_text,
515
579
  )
516
580
  with _locked_turn_refs_manifest(
517
581
  refs_log_path, lock_filename=_MCP_REFS_LOCK_FILENAME
@@ -542,6 +606,7 @@ def _annotate_mcp_results_for_citations(
542
606
  "title": item.get("title"),
543
607
  "snippet": item.get("snippet"),
544
608
  "details": item.get("details"),
609
+ "text": _ref_text(item),
545
610
  }
546
611
  try:
547
612
  _append_turn_refs_manifest_entry(refs_log_path, manifest_entry)
@@ -563,13 +628,26 @@ def _annotate_mcp_results_for_citations(
563
628
  if stored is not None and new_details and not stored.get("details"):
564
629
  stored["details"] = new_details
565
630
  needs_rewrite = True
631
+ # ``text`` upgrades to the longer capture: the common turn
632
+ # is a search (small per-record JSON) followed by a full
633
+ # fetch of the same titled item — the later, richer text is
634
+ # the better ground truth for the hallucination check.
635
+ new_text = _ref_text(item)
636
+ if stored is not None and new_text:
637
+ stored_text = stored.get("text")
638
+ stored_len = (
639
+ len(stored_text) if isinstance(stored_text, str) else 0
640
+ )
641
+ if len(new_text) > stored_len:
642
+ stored["text"] = new_text
643
+ needs_rewrite = True
566
644
  annotated.append((source_number, item))
567
645
  if needs_rewrite:
568
646
  try:
569
647
  _rewrite_turn_refs_manifest(refs_log_path, entries)
570
648
  except (UnsafeRefsLogPathError, OSError) as exc:
571
649
  _LOGGER.warning(
572
- "mcp: failed to backfill refs manifest details: %s", exc
650
+ "mcp: failed to backfill refs manifest enrichment: %s", exc
573
651
  )
574
652
  except (UnsafeRefsLogPathError, OSError) as exc:
575
653
  _LOGGER.warning("mcp: failed to append refs manifest: %s", exc)
@@ -656,7 +734,10 @@ def record_mcp_citations(
656
734
  - ``response`` is the raw ``unique_sdk.MCP`` result, used for citation
657
735
  extraction (titles from ``resource_link`` names / JSON bodies).
658
736
  - ``formatted_text`` is the source text the model actually saw for this
659
- tool result, recorded as the hallucination groundedness context.
737
+ tool result, recorded as the hallucination groundedness context. It also
738
+ serves as the per-item ``text`` of the title-less fallback chip, so a
739
+ cited ``[mcpsourceN]`` without extractable records still grounds on the
740
+ whole output.
660
741
 
661
742
  Best-effort and never raises: the underlying manifest writers swallow their
662
743
  own errors, and ``_annotate_mcp_results_for_citations`` owns the per-turn
@@ -675,6 +756,7 @@ def record_mcp_citations(
675
756
  server_name=server_name,
676
757
  refs_log_path=unique_dir / _MCP_REFS_LOG_RELATIVE_PATH.name,
677
758
  reference_mapping=reference_mapping,
759
+ fallback_text=formatted_text,
678
760
  )
679
761
  return _citation_sources_block(annotated)
680
762