unique-sdk 2026.42.0.dev0__tar.gz → 2026.42.0.dev1__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.42.0.dev0 → unique_sdk-2026.42.0.dev1}/PKG-INFO +1 -1
  2. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/pyproject.toml +1 -1
  3. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/mcp.py +54 -35
  4. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/README.md +0 -0
  5. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/__init__.py +0 -0
  6. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_api_requestor.py +0 -0
  7. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_api_resource.py +0 -0
  8. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_api_version.py +0 -0
  9. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_error.py +0 -0
  10. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_http_client.py +0 -0
  11. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_list_object.py +0 -0
  12. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_object_classes.py +0 -0
  13. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_request_options.py +0 -0
  14. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_unique_object.py +0 -0
  15. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_unique_ql.py +0 -0
  16. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_unique_response.py +0 -0
  17. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_util.py +0 -0
  18. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_version.py +0 -0
  19. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/_webhook.py +0 -0
  20. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/__init__.py +0 -0
  21. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_acronyms.py +0 -0
  22. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_agentic_table.py +0 -0
  23. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_analytics_order.py +0 -0
  24. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_benchmarking.py +0 -0
  25. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_briefing.py +0 -0
  26. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_chat_completion.py +0 -0
  27. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_content.py +0 -0
  28. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_context_memory.py +0 -0
  29. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_elicitation.py +0 -0
  30. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_embedding.py +0 -0
  31. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_event.py +0 -0
  32. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_folder.py +0 -0
  33. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_group.py +0 -0
  34. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_integrated.py +0 -0
  35. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_llm_models.py +0 -0
  36. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_mcp.py +0 -0
  37. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_message.py +0 -0
  38. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_message_assessment.py +0 -0
  39. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_message_execution.py +0 -0
  40. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_message_log.py +0 -0
  41. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_message_tool.py +0 -0
  42. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_module.py +0 -0
  43. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  44. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_search.py +0 -0
  45. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_search_string.py +0 -0
  46. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  47. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_space.py +0 -0
  48. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_user.py +0 -0
  49. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/api_resources/_web_search.py +0 -0
  50. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/__init__.py +0 -0
  51. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/__main__.py +0 -0
  52. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/cli.py +0 -0
  53. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/__init__.py +0 -0
  54. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
  55. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/agentic_table.py +0 -0
  56. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/agentic_table_write.py +0 -0
  57. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/browser.py +0 -0
  58. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/cite_file.py +0 -0
  59. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/elicitation.py +0 -0
  60. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/files.py +0 -0
  61. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/folders.py +0 -0
  62. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/navigation.py +0 -0
  63. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/read.py +0 -0
  64. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  65. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/search.py +0 -0
  66. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/subagent.py +0 -0
  67. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/web_search.py +0 -0
  68. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/commands/web_search_config.py +0 -0
  69. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/config.py +0 -0
  70. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/formatting.py +0 -0
  71. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/identity.py +0 -0
  72. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/metadata_filter.py +0 -0
  73. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/shell.py +0 -0
  74. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/skills/unique-cli-agentic-table/SKILL.md +0 -0
  75. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  76. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
  77. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  78. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  79. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  80. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
  81. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
  82. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  83. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/state.py +0 -0
  84. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/cli/workspace.py +0 -0
  85. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/utils/analytics_order_run.py +0 -0
  86. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/utils/benchmarking_run.py +0 -0
  87. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/utils/chat_history.py +0 -0
  88. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/utils/chat_in_space.py +0 -0
  89. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/utils/file_io.py +0 -0
  90. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/unique_sdk/utils/sources.py +0 -0
  91. {unique_sdk-2026.42.0.dev0 → unique_sdk-2026.42.0.dev1}/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.42.0.dev0
3
+ Version: 2026.42.0.dev1
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.42.0.dev0"
3
+ version = "2026.42.0.dev1"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -31,14 +31,10 @@ _LOGGER = logging.getLogger(__name__)
31
31
  # information (UN-21951). Carries *text only* — no source numbers/markers
32
32
  # (referencing is UN-21285, tracked separately).
33
33
  _MCP_OUTPUT_LOG_RELATIVE_PATH = Path(".unique") / "mcp-output.jsonl"
34
- # Writer-side cap so a single huge/raw tool result cannot bloat the manifest.
35
- # This manifest is the groundedness check's source of truth, so the cap is
36
- # sized against the eval model's context window rather than kept minimal:
37
- # GPT-4o's 128k-token input fits ~400k chars, and the runner bounds the
38
- # combined per-turn payload separately (UN-22309). At the previous 50k, a
39
- # large list result (e.g. a 115k-char Jira search) lost most of its items
40
- # before the judge saw them, flagging well-grounded answers as hallucinations.
41
- _MCP_OUTPUT_TEXT_CHAR_LIMIT = 200_000
34
+ # Runaway guard, four times the largest judge window; the runner trims by tokens.
35
+ _MCP_TEXT_GUARD_CHARS = 16_000_000
36
+ # Bytes of tool text each per-turn manifest may hold, about 16 judge windows.
37
+ _MCP_TURN_TEXT_BUDGET_BYTES = 64_000_000
42
38
 
43
39
  # Per-turn manifest of citable MCP sources, consumed by the runner to stitch
44
40
  # ``[mcpsourceN]`` markers into ``<sup>N</sup>`` footnotes + reference chips
@@ -58,13 +54,6 @@ _MCP_REFS_LOCK_FILENAME = "mcp-refs.lock"
58
54
  # behavior (forward/backward compatible).
59
55
  _MCP_REFS_SEED_FILENAME = "mcp-refs-seed.json"
60
56
  _MCP_SNIPPET_CHAR_LIMIT = 300
61
- # Writer-side cap on the per-item ``text`` recorded in the refs manifest — the
62
- # cited item's underlying text, consumed by the runner's hallucination check to
63
- # ground each ``[mcpsourceN]`` citation on what was actually retrieved
64
- # (UN-22762). Half the flat-output cap (``_MCP_OUTPUT_TEXT_CHAR_LIMIT``): one
65
- # cited item (a page, an issue record) rarely exceeds it, and the eval side
66
- # bounds the combined cited-text payload separately.
67
- _MCP_REF_TEXT_CHAR_LIMIT = 100_000
68
57
 
69
58
  # Keys an MCP tool's JSON result commonly uses for a record's human title.
70
59
  _TITLE_KEYS = ("title", "name", "displayName", "subject", "summary", "key")
@@ -721,30 +710,35 @@ def _item_dedup_key(tool_name: str, item: dict[str, Any]) -> str:
721
710
  one number (identical bodies still merge). NOTE: this intentionally weakens
722
711
  the search-then-fetch text-upgrade merge for title-less items only — titled
723
712
  items still merge by title as before.
724
-
725
- The text is capped at ``_MCP_REF_TEXT_CHAR_LIMIT`` BEFORE hashing — the same
726
- cap the manifest stores under ``text``. Without it, the first call (live,
727
- full-length item text) and a later call rebuilding this key from the
728
- truncated manifest entry would hash to different values, so an oversized
729
- title-less result would be re-assigned a duplicate ``[mcpsourceN]`` instead
730
- of deduping.
731
713
  """
732
714
  title = item.get("title")
733
715
  if isinstance(title, str) and title.strip():
734
716
  return f"title:{tool_name}:{title.strip()}"
735
- capped_text = (item.get("text") or "")[:_MCP_REF_TEXT_CHAR_LIMIT]
736
- text_hash = hashlib.sha256(capped_text.encode("utf-8")).hexdigest()[:12]
717
+ text = (item.get("text") or "")[:_MCP_TEXT_GUARD_CHARS]
718
+ text_hash = hashlib.sha256(text.encode("utf-8")).hexdigest()[:12]
737
719
  return f"tool:{tool_name}:{text_hash}"
738
720
 
739
721
 
740
722
  def _ref_text(item: dict[str, Any]) -> str | None:
741
- """The item's underlying text for the manifest, capped at
742
- ``_MCP_REF_TEXT_CHAR_LIMIT`` (single write-side cap shared by all
743
- extraction modes)."""
723
+ """The item's underlying text for the manifest, up to the runaway guard."""
744
724
  text = item.get("text")
745
725
  if not isinstance(text, str) or not text:
746
726
  return None
747
- return text[:_MCP_REF_TEXT_CHAR_LIMIT]
727
+ return text[:_MCP_TEXT_GUARD_CHARS]
728
+
729
+
730
+ def _utf8_size(text: str) -> int:
731
+ return len(text.encode("utf-8", errors="surrogatepass"))
732
+
733
+
734
+ def _within_bytes(text: str, room: int) -> str:
735
+ """Longest prefix of ``text`` that fits ``room`` UTF-8 bytes."""
736
+ if room <= 0:
737
+ return ""
738
+ encoded = text.encode("utf-8", errors="surrogatepass")
739
+ if len(encoded) <= room:
740
+ return text
741
+ return encoded[:room].decode("utf-8", errors="ignore")
748
742
 
749
743
 
750
744
  def _annotate_mcp_results_for_citations(
@@ -793,21 +787,30 @@ def _annotate_mcp_results_for_citations(
793
787
  numbers_by_key: dict[str, int] = {}
794
788
  for entry in entries:
795
789
  if isinstance(entry.get("sourceNumber"), int):
796
- stored_tool = entry.get("toolName") or tool_name
797
- numbers_by_key[_item_dedup_key(stored_tool, entry)] = entry[
798
- "sourceNumber"
799
- ]
790
+ stored_key = entry.get("dedupKey")
791
+ if not isinstance(stored_key, str):
792
+ stored_tool = entry.get("toolName") or tool_name
793
+ stored_key = _item_dedup_key(stored_tool, entry)
794
+ numbers_by_key[stored_key] = entry["sourceNumber"]
800
795
  entries_by_number = {
801
796
  entry["sourceNumber"]: entry
802
797
  for entry in entries
803
798
  if isinstance(entry.get("sourceNumber"), int)
804
799
  }
805
800
  needs_rewrite = False
801
+ text_room = _MCP_TURN_TEXT_BUDGET_BYTES - sum(
802
+ _utf8_size(entry["text"])
803
+ for entry in entries
804
+ if isinstance(entry.get("text"), str)
805
+ )
806
806
  for item in items:
807
807
  key = _item_dedup_key(tool_name, item)
808
808
  source_number = numbers_by_key.get(key)
809
809
  if source_number is None:
810
810
  source_number = max(_next_mcp_source_number(entries), seed + 1)
811
+ full_text = _ref_text(item) or ""
812
+ entry_text = _within_bytes(full_text, text_room)
813
+ text_room -= _utf8_size(entry_text)
811
814
  manifest_entry = {
812
815
  "sourceNumber": source_number,
813
816
  "toolName": tool_name,
@@ -815,8 +818,11 @@ def _annotate_mcp_results_for_citations(
815
818
  "title": item.get("title"),
816
819
  "snippet": item.get("snippet"),
817
820
  "details": item.get("details"),
818
- "text": _ref_text(item),
821
+ "text": entry_text or None,
819
822
  }
823
+ # Budget-cut text no longer hashes to the key, so it is kept.
824
+ if entry_text != full_text:
825
+ manifest_entry["dedupKey"] = key
820
826
  item_url = item.get("url")
821
827
  if isinstance(item_url, str) and item_url:
822
828
  manifest_entry["url"] = item_url
@@ -855,7 +861,13 @@ def _annotate_mcp_results_for_citations(
855
861
  stored_len = (
856
862
  len(stored_text) if isinstance(stored_text, str) else 0
857
863
  )
858
- if len(new_text) > stored_len:
864
+ growth = _utf8_size(new_text) - (
865
+ _utf8_size(stored_text)
866
+ if isinstance(stored_text, str)
867
+ else 0
868
+ )
869
+ if len(new_text) > stored_len and growth <= text_room:
870
+ text_room -= growth
859
871
  stored["text"] = new_text
860
872
  needs_rewrite = True
861
873
  annotated.append((source_number, item))
@@ -931,12 +943,19 @@ def _append_mcp_output_manifest(
931
943
  refs_log_path = output_path or workspace_manifest_path(
932
944
  _MCP_OUTPUT_LOG_RELATIVE_PATH
933
945
  )
946
+ recorded = refs_log_path.stat().st_size if refs_log_path.is_file() else 0
947
+ text = _within_bytes(
948
+ text[:_MCP_TEXT_GUARD_CHARS], _MCP_TURN_TEXT_BUDGET_BYTES - recorded
949
+ )
950
+ if not text:
951
+ _LOGGER.warning("mcp: turn output budget spent, row skipped")
952
+ return
934
953
  _append_turn_refs_manifest_entry(
935
954
  refs_log_path,
936
955
  {
937
956
  "toolName": name,
938
957
  "serverName": server_name,
939
- "text": text[:_MCP_OUTPUT_TEXT_CHAR_LIMIT],
958
+ "text": text,
940
959
  },
941
960
  )
942
961
  except (UnsafeRefsLogPathError, OSError) as exc: