unique-sdk 2026.32.0.dev3__tar.gz → 2026.32.0.dev4__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 (91) hide show
  1. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/PKG-INFO +1 -1
  2. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/pyproject.toml +1 -1
  3. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/mcp.py +107 -8
  4. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/README.md +0 -0
  5. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/__init__.py +0 -0
  6. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_api_requestor.py +0 -0
  7. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_api_resource.py +0 -0
  8. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_api_version.py +0 -0
  9. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_error.py +0 -0
  10. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_http_client.py +0 -0
  11. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_list_object.py +0 -0
  12. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_object_classes.py +0 -0
  13. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_request_options.py +0 -0
  14. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_unique_object.py +0 -0
  15. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_unique_ql.py +0 -0
  16. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_unique_response.py +0 -0
  17. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_util.py +0 -0
  18. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_version.py +0 -0
  19. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/_webhook.py +0 -0
  20. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/__init__.py +0 -0
  21. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_acronyms.py +0 -0
  22. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_agentic_table.py +0 -0
  23. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_analytics_order.py +0 -0
  24. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_benchmarking.py +0 -0
  25. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_briefing.py +0 -0
  26. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_chat_completion.py +0 -0
  27. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_content.py +0 -0
  28. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_dynamic_frontend.py +0 -0
  29. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_elicitation.py +0 -0
  30. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_embedding.py +0 -0
  31. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_event.py +0 -0
  32. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_folder.py +0 -0
  33. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_group.py +0 -0
  34. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_integrated.py +0 -0
  35. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_llm_models.py +0 -0
  36. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_mcp.py +0 -0
  37. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_message.py +0 -0
  38. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_message_assessment.py +0 -0
  39. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_message_execution.py +0 -0
  40. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_message_log.py +0 -0
  41. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_message_tool.py +0 -0
  42. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_module.py +0 -0
  43. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  44. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_search.py +0 -0
  45. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_search_string.py +0 -0
  46. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  47. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_space.py +0 -0
  48. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_user.py +0 -0
  49. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/api_resources/_web_search.py +0 -0
  50. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/__init__.py +0 -0
  51. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/__main__.py +0 -0
  52. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/cli.py +0 -0
  53. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/__init__.py +0 -0
  54. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
  55. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/agentic_table.py +0 -0
  56. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/browser.py +0 -0
  57. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/cite_file.py +0 -0
  58. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/dynamic_frontend.py +0 -0
  59. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/elicitation.py +0 -0
  60. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/files.py +0 -0
  61. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/folders.py +0 -0
  62. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/navigation.py +0 -0
  63. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/read.py +0 -0
  64. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  65. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/search.py +0 -0
  66. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/subagent.py +0 -0
  67. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/web_search.py +0 -0
  68. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/commands/web_search_config.py +0 -0
  69. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/config.py +0 -0
  70. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/formatting.py +0 -0
  71. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/identity.py +0 -0
  72. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/metadata_filter.py +0 -0
  73. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/shell.py +0 -0
  74. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/skills/unique-cli-agentic-table/SKILL.md +0 -0
  75. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/skills/unique-cli-dynamic-frontend/SKILL.md +0 -0
  76. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  77. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
  78. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  79. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  80. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  81. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
  82. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
  83. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  84. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/cli/state.py +0 -0
  85. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/utils/analytics_order_run.py +0 -0
  86. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/utils/benchmarking_run.py +0 -0
  87. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/utils/chat_history.py +0 -0
  88. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/utils/chat_in_space.py +0 -0
  89. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/utils/file_io.py +0 -0
  90. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/unique_sdk/utils/sources.py +0 -0
  91. {unique_sdk-2026.32.0.dev3 → unique_sdk-2026.32.0.dev4}/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.32.0.dev3
3
+ Version: 2026.32.0.dev4
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.32.0.dev3"
3
+ version = "2026.32.0.dev4"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -2,8 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import hashlib
5
6
  import json
6
7
  import logging
8
+ import os
7
9
  import re
8
10
  import sys
9
11
  from pathlib import Path
@@ -44,6 +46,14 @@ _MCP_OUTPUT_TEXT_CHAR_LIMIT = 200_000
44
46
  # the result carries no recognizable title.
45
47
  _MCP_REFS_LOG_RELATIVE_PATH = Path(".unique") / "mcp-refs.jsonl"
46
48
  _MCP_REFS_LOCK_FILENAME = "mcp-refs.lock"
49
+ # Persistent per-chat seed for ``[mcpsourceN]`` numbering. The SI runner wipes
50
+ # ``mcp-refs.jsonl`` between turns, so manifest-derived numbering restarts at 1
51
+ # every turn; this sibling file records the highest source number ever assigned
52
+ # in the chat so numbering stays monotonic across turns (UN-23199). Body is
53
+ # ``{"maxSourceNumber": <int>}``; the monorepo runner preserves this file across
54
+ # its per-turn reset. Absent/unreadable → seed 0 → identical to pre-seed
55
+ # behavior (forward/backward compatible).
56
+ _MCP_REFS_SEED_FILENAME = "mcp-refs-seed.json"
47
57
  _MCP_SNIPPET_CHAR_LIMIT = 300
48
58
  # Writer-side cap on the per-item ``text`` recorded in the refs manifest — the
49
59
  # cited item's underlying text, consumed by the runner's hallucination check to
@@ -533,12 +543,75 @@ def _next_mcp_source_number(entries: list[dict[str, Any]]) -> int:
533
543
  return max(numbers, default=0) + 1
534
544
 
535
545
 
546
+ def _read_mcp_refs_seed(seed_path: Path) -> int:
547
+ """Read the persisted chat-wide ``maxSourceNumber`` from the seed file.
548
+
549
+ Best-effort: returns ``0`` when the file is absent, unreadable, or malformed
550
+ (never raises), so a missing seed reproduces the pre-seed numbering exactly.
551
+ """
552
+ try:
553
+ raw = seed_path.read_text(encoding="utf-8")
554
+ value = json.loads(raw).get("maxSourceNumber")
555
+ if isinstance(value, int) and not isinstance(value, bool):
556
+ return value
557
+ except (OSError, ValueError, TypeError, AttributeError):
558
+ return 0
559
+ return 0
560
+
561
+
562
+ def _write_mcp_refs_seed(seed_path: Path, value: int) -> None:
563
+ """Persist the chat-wide ``maxSourceNumber`` seed (best-effort, never raises).
564
+
565
+ Only writes when ``value`` exceeds the currently persisted seed, so numbers
566
+ never regress across turns.
567
+
568
+ The temp file is opened with ``O_NOFOLLOW`` so a symlink pre-planted at the
569
+ predictable temp path cannot redirect the write outside the refs directory
570
+ (defense in depth on top of the per-user workspace sandbox). ``os.replace``
571
+ operates on the directory entry and does not follow a symlink at the target.
572
+ ``O_NOFOLLOW`` is POSIX-only; on platforms lacking it (e.g. Windows) it
573
+ degrades to ``0`` via ``getattr`` rather than raising ``AttributeError`` —
574
+ which, being outside the ``OSError`` handler, would otherwise bubble out of
575
+ ``_annotate_mcp_results_for_citations`` after entries were written and blank
576
+ the Sources block.
577
+ """
578
+ try:
579
+ if value <= _read_mcp_refs_seed(seed_path):
580
+ return
581
+ seed_path.parent.mkdir(parents=True, exist_ok=True)
582
+ tmp_path = seed_path.with_suffix(seed_path.suffix + ".tmp")
583
+ flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, "O_NOFOLLOW", 0)
584
+ fd = os.open(tmp_path, flags, 0o600)
585
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
586
+ handle.write(json.dumps({"maxSourceNumber": value}))
587
+ os.replace(tmp_path, seed_path)
588
+ except OSError as exc:
589
+ _LOGGER.warning("mcp: failed to write refs seed: %s", exc)
590
+
591
+
536
592
  def _item_dedup_key(tool_name: str, item: dict[str, Any]) -> str:
537
- """Dedup by title when present (same item = one chip), else by tool."""
593
+ """Dedup by title when present (same item = one chip), else by tool + a short
594
+ content hash of the item's text.
595
+
596
+ Two distinct title-less fetches through one tool return different bodies, so
597
+ hashing the text keeps them as separate sources instead of collapsing onto
598
+ one number (identical bodies still merge). NOTE: this intentionally weakens
599
+ the search-then-fetch text-upgrade merge for title-less items only — titled
600
+ items still merge by title as before.
601
+
602
+ The text is capped at ``_MCP_REF_TEXT_CHAR_LIMIT`` BEFORE hashing — the same
603
+ cap the manifest stores under ``text``. Without it, the first call (live,
604
+ full-length item text) and a later call rebuilding this key from the
605
+ truncated manifest entry would hash to different values, so an oversized
606
+ title-less result would be re-assigned a duplicate ``[mcpsourceN]`` instead
607
+ of deduping.
608
+ """
538
609
  title = item.get("title")
539
610
  if isinstance(title, str) and title.strip():
540
611
  return f"title:{tool_name}:{title.strip()}"
541
- return f"tool:{tool_name}"
612
+ capped_text = (item.get("text") or "")[:_MCP_REF_TEXT_CHAR_LIMIT]
613
+ text_hash = hashlib.sha256(capped_text.encode("utf-8")).hexdigest()[:12]
614
+ return f"tool:{tool_name}:{text_hash}"
542
615
 
543
616
 
544
617
  def _ref_text(item: dict[str, Any]) -> str | None:
@@ -563,8 +636,11 @@ def _annotate_mcp_results_for_citations(
563
636
  """Assign per-turn ``[mcpsourceN]`` numbers to each retrieved item and append
564
637
  the refs manifest. Returns ``[(sourceNumber, item)]`` for the Sources block.
565
638
 
566
- Items dedup by title across the turn (same item keeps one number), or by
567
- tool for the title-less fallback. Best-effort — returns ``[]`` on any failure
639
+ Items dedup by title across the turn (same item keeps one number); a
640
+ title-less item dedups by tool plus a content hash of its (capped) text, so
641
+ two distinct title-less results through one tool get distinct numbers while
642
+ identical bodies still merge (this weakens the search-then-fetch text-upgrade
643
+ merge for title-less items only). Best-effort — returns ``[]`` on any failure
568
644
  (the tool result is unaffected; only the citation Sources block is skipped).
569
645
  """
570
646
  refs_log_path = refs_log_path or (Path.cwd() / _MCP_REFS_LOG_RELATIVE_PATH)
@@ -581,6 +657,14 @@ def _annotate_mcp_results_for_citations(
581
657
  refs_log_path, lock_filename=_MCP_REFS_LOCK_FILENAME
582
658
  ):
583
659
  entries = _read_turn_refs_manifest(refs_log_path)
660
+ # Chat-wide monotonic seed: the runner wipes ``mcp-refs.jsonl``
661
+ # between turns, so manifest-derived numbering restarts at 1 each
662
+ # turn. The seed file (preserved across the runner's per-turn reset)
663
+ # records the highest number ever assigned in the chat so a fresh
664
+ # number exceeds both the current manifest max AND the chat-wide max.
665
+ seed_path = refs_log_path.parent / _MCP_REFS_SEED_FILENAME
666
+ seed = _read_mcp_refs_seed(seed_path)
667
+ highest_assigned = seed
584
668
  numbers_by_key: dict[str, int] = {}
585
669
  for entry in entries:
586
670
  if isinstance(entry.get("sourceNumber"), int):
@@ -598,7 +682,7 @@ def _annotate_mcp_results_for_citations(
598
682
  key = _item_dedup_key(tool_name, item)
599
683
  source_number = numbers_by_key.get(key)
600
684
  if source_number is None:
601
- source_number = _next_mcp_source_number(entries)
685
+ source_number = max(_next_mcp_source_number(entries), seed + 1)
602
686
  manifest_entry = {
603
687
  "sourceNumber": source_number,
604
688
  "toolName": tool_name,
@@ -618,6 +702,7 @@ def _annotate_mcp_results_for_citations(
618
702
  numbers_by_key[key] = source_number
619
703
  entries.append(manifest_entry)
620
704
  entries_by_number[source_number] = manifest_entry
705
+ highest_assigned = max(highest_assigned, source_number)
621
706
  else:
622
707
  # Deduped: a prior call already claimed this source number.
623
708
  # Backfill ``details`` the first call may have lacked (the
@@ -649,6 +734,10 @@ def _annotate_mcp_results_for_citations(
649
734
  _LOGGER.warning(
650
735
  "mcp: failed to backfill refs manifest enrichment: %s", exc
651
736
  )
737
+ # Persist the chat-wide max so next turn's numbering continues above
738
+ # it even after the runner wipes ``mcp-refs.jsonl``.
739
+ if highest_assigned > seed:
740
+ _write_mcp_refs_seed(seed_path, highest_assigned)
652
741
  except (UnsafeRefsLogPathError, OSError) as exc:
653
742
  _LOGGER.warning("mcp: failed to append refs manifest: %s", exc)
654
743
  return []
@@ -658,13 +747,19 @@ def _annotate_mcp_results_for_citations(
658
747
  return annotated
659
748
 
660
749
 
661
- def _citation_sources_block(annotated: list[tuple[int, dict[str, Any]]]) -> str:
750
+ def _citation_sources_block(
751
+ annotated: list[tuple[int, dict[str, Any]]], *, tool_name: str
752
+ ) -> str:
662
753
  """Tell the agent which marker to cite each retrieved item with.
663
754
 
664
755
  Rendered *before* the tool output (leading block, not a trailing footer):
665
756
  the agent harness spills oversized tool results to a file wholesale, and a
666
757
  partial or programmatic read of that file only reliably sees the head — a
667
758
  trailing marker list would be exactly what such reads miss (UN-22309).
759
+
760
+ Each line names the ``tool_name`` alongside the marker so the model has a
761
+ stronger anchor than a bare integer: ``{title} — {tool_name}`` when the item
762
+ carries a title, or ``{tool_name}`` for the title-less fallback.
668
763
  """
669
764
  if not annotated:
670
765
  return ""
@@ -674,7 +769,11 @@ def _citation_sources_block(annotated: list[tuple[int, dict[str, Any]]]) -> str:
674
769
  "will not be referenced in the answer:",
675
770
  ]
676
771
  for source_number, item in annotated:
677
- label = item.get("title") or "this MCP tool result"
772
+ title = item.get("title")
773
+ if isinstance(title, str) and title.strip():
774
+ label = f"{title.strip()} — {tool_name}"
775
+ else:
776
+ label = tool_name
678
777
  lines.append(f" [mcpsource{source_number}] {label}")
679
778
  return "\n".join(lines)
680
779
 
@@ -758,7 +857,7 @@ def record_mcp_citations(
758
857
  reference_mapping=reference_mapping,
759
858
  fallback_text=formatted_text,
760
859
  )
761
- return _citation_sources_block(annotated)
860
+ return _citation_sources_block(annotated, tool_name=tool_name)
762
861
 
763
862
 
764
863
  def _read_payload(