unique-sdk 2026.30.0.dev3__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.dev3 → unique_sdk-2026.30.0.dev5}/PKG-INFO +1 -1
  2. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/pyproject.toml +1 -1
  3. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/mcp.py +88 -6
  4. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +1 -37
  5. unique_sdk-2026.30.0.dev5/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +113 -0
  6. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/state.py +10 -0
  7. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/README.md +0 -0
  8. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/__init__.py +0 -0
  9. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_api_requestor.py +0 -0
  10. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_api_resource.py +0 -0
  11. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_api_version.py +0 -0
  12. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_error.py +0 -0
  13. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_http_client.py +0 -0
  14. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_list_object.py +0 -0
  15. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_object_classes.py +0 -0
  16. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_request_options.py +0 -0
  17. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_unique_object.py +0 -0
  18. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_unique_ql.py +0 -0
  19. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_unique_response.py +0 -0
  20. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_util.py +0 -0
  21. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_version.py +0 -0
  22. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/_webhook.py +0 -0
  23. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/__init__.py +0 -0
  24. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_acronyms.py +0 -0
  25. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_agentic_table.py +0 -0
  26. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_analytics_order.py +0 -0
  27. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_benchmarking.py +0 -0
  28. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_briefing.py +0 -0
  29. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_chat_completion.py +0 -0
  30. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_content.py +0 -0
  31. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_dynamic_frontend.py +0 -0
  32. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_elicitation.py +0 -0
  33. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_embedding.py +0 -0
  34. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_event.py +0 -0
  35. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_folder.py +0 -0
  36. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_group.py +0 -0
  37. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_integrated.py +0 -0
  38. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_llm_models.py +0 -0
  39. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_mcp.py +0 -0
  40. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_message.py +0 -0
  41. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_message_assessment.py +0 -0
  42. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_message_execution.py +0 -0
  43. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_message_log.py +0 -0
  44. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_message_tool.py +0 -0
  45. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_module.py +0 -0
  46. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  47. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_search.py +0 -0
  48. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_search_string.py +0 -0
  49. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  50. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_space.py +0 -0
  51. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_user.py +0 -0
  52. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/api_resources/_web_search.py +0 -0
  53. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/__init__.py +0 -0
  54. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/__main__.py +0 -0
  55. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/cli.py +0 -0
  56. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/__init__.py +0 -0
  57. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
  58. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/browser.py +0 -0
  59. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/cite_file.py +0 -0
  60. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/dynamic_frontend.py +0 -0
  61. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/elicitation.py +0 -0
  62. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/files.py +0 -0
  63. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/folders.py +0 -0
  64. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/navigation.py +0 -0
  65. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/read.py +0 -0
  66. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  67. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/search.py +0 -0
  68. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/subagent.py +0 -0
  69. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/web_search.py +0 -0
  70. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/commands/web_search_config.py +0 -0
  71. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/config.py +0 -0
  72. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/formatting.py +0 -0
  73. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/metadata_filter.py +0 -0
  74. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/shell.py +0 -0
  75. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-dynamic-frontend/SKILL.md +0 -0
  76. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  77. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
  78. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  79. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  80. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
  81. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  82. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/utils/analytics_order_run.py +0 -0
  83. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/utils/benchmarking_run.py +0 -0
  84. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/utils/chat_history.py +0 -0
  85. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/utils/chat_in_space.py +0 -0
  86. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/utils/file_io.py +0 -0
  87. {unique_sdk-2026.30.0.dev3 → unique_sdk-2026.30.0.dev5}/unique_sdk/utils/sources.py +0 -0
  88. {unique_sdk-2026.30.0.dev3 → 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.dev3
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.dev3"
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
 
@@ -7,10 +7,7 @@ description: >-
7
7
  platform. Use whenever the user asks to find, search, or query documents
8
8
  or content on Unique, including filtering by folder or metadata.
9
9
  Also covers `unique-cli read <cont_id>` for reading the full indexed text
10
- of a document when its content ID is already known, and
11
- `unique-cli uploaded-search "<query>"` for searching documents uploaded for
12
- the current task/row, which are NOT in the knowledge-base scope and never
13
- appear in `unique-cli search`.
10
+ of a document when its content ID is already known.
14
11
  NOTE: This search uses combined vector + full-text indexing. Excel
15
12
  (.xlsx/.xls), CSV (.csv), and image files are NOT full-text indexed,
16
13
  so they will not appear in search results. To locate these file types,
@@ -95,43 +92,10 @@ unique-cli search "audit" -m department=Legal -m year=2025
95
92
  unique-cli search "regulatory" -f /Legal -m year=2025 -l 50
96
93
  ```
97
94
 
98
- ## Searching Uploaded Documents (`uploaded-search`)
99
-
100
- Documents **uploaded for the current task** (e.g. an Agentic Table row's
101
- attached files) are **not** part of the knowledge-base folder scope. They will
102
- **never** appear in `unique-cli search` results, no matter the folder or
103
- metadata filters — uploaded files are scoped to the chat, not to a KB folder.
104
- Use `uploaded-search` to retrieve them:
105
-
106
- ```bash
107
- # Search the documents uploaded for this row/task
108
- unique-cli uploaded-search "target asset classes and investment strategy"
109
-
110
- # Limit results
111
- unique-cli uploaded-search "fee structure" --limit 50
112
- ```
113
-
114
- When to use which:
115
-
116
- | You want to search… | Command |
117
- |---------------------|---------|
118
- | The knowledge base (folders the task scope grants) | `unique-cli search "<query>"` |
119
- | Documents uploaded for **this** row/task | `unique-cli uploaded-search "<query>"` |
120
-
121
- `uploaded-search` returns the same `<sourceN>...</sourceN>` blocks as `search`
122
- and shares the **same per-turn citation manifest**, so `[sourceN]` numbering is
123
- continuous across both commands within a turn — cite an uploaded-document fact
124
- exactly the same way (`[sourceN]`). If no documents were uploaded for the task,
125
- the command reports that and you should fall back to `unique-cli search`.
126
-
127
- > **Note:** there is no `--folder`/`--metadata` for `uploaded-search` — the set
128
- > of uploaded documents is fixed by what was attached to the task.
129
-
130
95
  ## Command Reference
131
96
 
132
97
  ```
133
98
  unique-cli search <query> [--folder <path|scope_id>] [--metadata <key=value>]... [--limit <N>]
134
- unique-cli uploaded-search <query> [--limit <N>]
135
99
  ```
136
100
 
137
101
  | Option | Short | Default | Description |
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: unique-cli-uploaded-search
3
+ description: >-
4
+ Search the documents uploaded for the CURRENT task/row (e.g. an Agentic
5
+ Table row's attached files) via the `unique-cli uploaded-search "<query>"`
6
+ command, with the same per-turn citation tracking as `unique-cli search`
7
+ so cited facts render as `<sup>N</sup>` footnotes and clickable reference
8
+ chips on the Unique platform. ALWAYS use this skill when the user refers to
9
+ documents they uploaded/attached to this task, or when you need facts from
10
+ the task's own attached files. These uploaded files are scoped to the chat,
11
+ NOT to a knowledge-base folder, so they will NEVER appear in
12
+ `unique-cli search` results no matter the folder or metadata filters. Use
13
+ `unique-cli search` for the knowledge base and this command for the task's
14
+ uploaded documents; the two are complementary and citation numbering is
15
+ shared across them within a turn.
16
+ ---
17
+
18
+ # Unique CLI -- Uploaded Document Search
19
+
20
+ Documents **uploaded for the current task** (for example an Agentic Table
21
+ row's attached files) are **not** part of the knowledge-base folder scope.
22
+ They are scoped to the chat, so they will **never** appear in
23
+ `unique-cli search` results — no folder or metadata filter can surface them.
24
+ Use `unique-cli uploaded-search` to retrieve them.
25
+
26
+ ## Basic Usage
27
+
28
+ ```bash
29
+ # Search the documents uploaded for this row/task
30
+ unique-cli uploaded-search "target asset classes and investment strategy"
31
+
32
+ # Limit results
33
+ unique-cli uploaded-search "fee structure" --limit 50
34
+ ```
35
+
36
+ There is **no** `--folder`/`--metadata` for `uploaded-search` — the set of
37
+ uploaded documents is fixed by what was attached to the task.
38
+
39
+ If no documents were uploaded for this task, the command reports that and you
40
+ should fall back to `unique-cli search` for the knowledge base.
41
+
42
+ ## When to use which command
43
+
44
+ | You want to search… | Command |
45
+ |---------------------|---------|
46
+ | The knowledge base (folders the task scope grants) | `unique-cli search "<query>"` |
47
+ | Documents uploaded for **this** row/task | `unique-cli uploaded-search "<query>"` |
48
+
49
+ ## Output Format
50
+
51
+ Each result is rendered as a `<sourceN>...</sourceN>` block, identical to
52
+ `unique-cli search`. `N` is **1-based** and **shared with `unique-cli search`
53
+ within the same turn** — both commands append to the same per-turn citation
54
+ manifest, so numbering stays continuous across them (a `search` call that
55
+ emitted `<source1>`–`<source3>` is followed by an `uploaded-search` call whose
56
+ first result is `<source4>`).
57
+
58
+ ```
59
+ Found 1 result(s):
60
+
61
+ <source1>
62
+ <|document|>investment-mandate.pdf</|document|>
63
+ <|page|>2</|page|>
64
+ <|info|>cont_abc123</|info|>
65
+ ...the mandate targets EMEA equities with a 5% cap per issuer...
66
+ </source1>
67
+ ```
68
+
69
+ ## Citation Rules
70
+
71
+ Cite a fact from `uploaded-search` results with `[sourceN]`, **exactly** as you
72
+ would for `unique-cli search` — the two share the same namespace and manifest.
73
+ The Unique platform converts each `[sourceN]` marker in your final answer into a
74
+ `<sup>N</sup>` footnote and a clickable reference chip.
75
+
76
+ ```
77
+ The uploaded mandate caps single-issuer exposure at 5% [source1].
78
+ ```
79
+
80
+ **Rules** (enforced by the platform's reference post-processor):
81
+
82
+ 1. **`[sourceN]` is for KB and uploaded-document results.** Web results from
83
+ `unique-cli web-search` use `[websourceN]` — never mix the two namespaces.
84
+ 2. Only cite numbers you saw in the **current** turn's `search` /
85
+ `uploaded-search` output. Numbers from previous turns are stale and will be
86
+ silently dropped.
87
+ 3. Write `source` in singular form with the number in digits: `[source1]`,
88
+ `[source2]` — not `[Source 1]` or `[source one]`.
89
+ 4. Prefer citing each fact with a single, most-relevant source.
90
+ 5. Do not invent source numbers for remembered or inferred facts.
91
+
92
+ ## Command Reference
93
+
94
+ ```
95
+ unique-cli uploaded-search <query> [--limit <N>]
96
+ ```
97
+
98
+ | Option | Short | Default | Description |
99
+ |--------|-------|---------|-------------|
100
+ | `--limit` | `-l` | 200 | Max results |
101
+
102
+ ## Prerequisites
103
+
104
+ Requires these environment variables:
105
+
106
+ ```bash
107
+ UNIQUE_USER_ID # User ID (required)
108
+ UNIQUE_COMPANY_ID # Company ID (required)
109
+ UNIQUE_API_KEY # API key — optional on localhost / secured cluster
110
+ UNIQUE_APP_ID # App ID — optional on localhost / secured cluster
111
+ ```
112
+
113
+ Install: `pip install unique-sdk`
@@ -263,6 +263,14 @@ class ShellState:
263
263
  """Return True if *content_id* is in the current workspace scope.
264
264
 
265
265
  Precedence (UN-21780):
266
+ 0. Per-row uploaded documents (``.unique-uploaded.json``) surfaced by
267
+ ``unique-cli uploaded-search`` are task inputs that live *outside* the
268
+ KB scope boundary by design, so neither the metadata filter nor the
269
+ static ``scopeIds`` path below would admit them. Exempt them for
270
+ non-destructive access so the agent can ``read``/``ls``/``cite`` a doc
271
+ it just found via uploaded-search — mirroring the chat-file exemption.
272
+ Mutating ops (``rm``/``mv``) pass ``allow_chat_files=False`` and stay
273
+ denied, so an uploaded input can't be deleted or renamed this way.
266
274
  1. A per-message UniqueQL ``metaDataFilter`` (e.g. an Agentic Table
267
275
  column's ``scope_rules``) is the authority for this turn and
268
276
  *replaces* the static ``scopeIds`` for content access. Files the
@@ -275,6 +283,8 @@ class ShellState:
275
283
  ``is_folder_target_within_workspace``).
276
284
  3. With neither configured, everything is in scope.
277
285
  """
286
+ if allow_chat_files and content_id in self.uploaded_search_content_ids:
287
+ return True
278
288
  if self.workspace_metadata_filter is not None:
279
289
  if allow_chat_files and content_id in self._chat_file_content_ids():
280
290
  return True