unique-sdk 2026.28.0.dev10__tar.gz → 2026.28.0.dev11__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 (86) hide show
  1. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/PKG-INFO +1 -1
  2. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/pyproject.toml +1 -1
  3. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/_citation_manifest.py +63 -0
  4. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/mcp.py +107 -2
  5. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/README.md +0 -0
  6. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/__init__.py +0 -0
  7. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_api_requestor.py +0 -0
  8. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_api_resource.py +0 -0
  9. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_api_version.py +0 -0
  10. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_error.py +0 -0
  11. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_http_client.py +0 -0
  12. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_list_object.py +0 -0
  13. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_object_classes.py +0 -0
  14. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_request_options.py +0 -0
  15. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_unique_object.py +0 -0
  16. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_unique_ql.py +0 -0
  17. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_unique_response.py +0 -0
  18. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_util.py +0 -0
  19. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_version.py +0 -0
  20. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/_webhook.py +0 -0
  21. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/__init__.py +0 -0
  22. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_acronyms.py +0 -0
  23. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_agentic_table.py +0 -0
  24. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_analytics_order.py +0 -0
  25. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_benchmarking.py +0 -0
  26. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_briefing.py +0 -0
  27. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_chat_completion.py +0 -0
  28. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_content.py +0 -0
  29. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_dynamic_frontend.py +0 -0
  30. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_elicitation.py +0 -0
  31. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_embedding.py +0 -0
  32. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_event.py +0 -0
  33. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_folder.py +0 -0
  34. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_group.py +0 -0
  35. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_integrated.py +0 -0
  36. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_llm_models.py +0 -0
  37. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_mcp.py +0 -0
  38. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_message.py +0 -0
  39. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_message_assessment.py +0 -0
  40. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_message_execution.py +0 -0
  41. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_message_log.py +0 -0
  42. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_message_tool.py +0 -0
  43. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_module.py +0 -0
  44. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  45. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_search.py +0 -0
  46. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_search_string.py +0 -0
  47. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  48. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_space.py +0 -0
  49. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_user.py +0 -0
  50. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/api_resources/_web_search.py +0 -0
  51. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/__init__.py +0 -0
  52. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/__main__.py +0 -0
  53. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/cli.py +0 -0
  54. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/__init__.py +0 -0
  55. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/cite_file.py +0 -0
  56. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/dynamic_frontend.py +0 -0
  57. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/elicitation.py +0 -0
  58. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/files.py +0 -0
  59. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/folders.py +0 -0
  60. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/navigation.py +0 -0
  61. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/read.py +0 -0
  62. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  63. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/search.py +0 -0
  64. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/subagent.py +0 -0
  65. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/web_search.py +0 -0
  66. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/commands/web_search_config.py +0 -0
  67. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/config.py +0 -0
  68. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/formatting.py +0 -0
  69. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/metadata_filter.py +0 -0
  70. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/shell.py +0 -0
  71. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/skills/unique-cli-dynamic-frontend/SKILL.md +0 -0
  72. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  73. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
  74. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  75. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  76. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  77. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
  78. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  79. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/cli/state.py +0 -0
  80. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/utils/analytics_order_run.py +0 -0
  81. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/utils/benchmarking_run.py +0 -0
  82. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/utils/chat_history.py +0 -0
  83. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/utils/chat_in_space.py +0 -0
  84. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/utils/file_io.py +0 -0
  85. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/unique_sdk/utils/sources.py +0 -0
  86. {unique_sdk-2026.28.0.dev10 → unique_sdk-2026.28.0.dev11}/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.28.0.dev10
3
+ Version: 2026.28.0.dev11
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.28.0.dev10"
3
+ version = "2026.28.0.dev11"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -20,6 +20,7 @@ from __future__ import annotations
20
20
  import fcntl
21
21
  import json
22
22
  import os
23
+ import tempfile
23
24
  from collections.abc import Generator
24
25
  from contextlib import contextmanager
25
26
  from pathlib import Path
@@ -30,6 +31,7 @@ __all__ = [
30
31
  "_append_turn_refs_manifest_entry",
31
32
  "_locked_turn_refs_manifest",
32
33
  "_read_turn_refs_manifest",
34
+ "_rewrite_turn_refs_manifest",
33
35
  ]
34
36
 
35
37
 
@@ -135,6 +137,67 @@ def _append_turn_refs_manifest_entry(
135
137
  ) from exc
136
138
 
137
139
 
140
+ def _rewrite_turn_refs_manifest(
141
+ refs_log_path: Path,
142
+ entries: list[dict[str, Any]],
143
+ ) -> None:
144
+ """Atomically replace the manifest with ``entries`` (one JSON object per
145
+ line, in order).
146
+
147
+ Used to backfill a previously-appended entry in place — e.g. when a later
148
+ tool call re-cites a deduped item and extracts ``details`` the first call
149
+ lacked. Must be called while holding ``_locked_turn_refs_manifest`` so the
150
+ read → mutate → rewrite sequence does not race a concurrent append.
151
+
152
+ Crash-safe: the new content is written to a sibling temp file and then
153
+ ``os.replace``-d over the manifest, so a failed/partial write never
154
+ truncates or corrupts the live manifest (the runner keeps reading the old
155
+ file until the rename succeeds). Same symlink/regular-file safety discipline
156
+ as the append path — rejects a symlinked dir/file, and the temp file is
157
+ created fresh with ``O_CREAT | O_EXCL | O_NOFOLLOW`` (via ``mkstemp``) so a
158
+ symlink swap between the check and the open still fails closed.
159
+ """
160
+ _assert_safe_refs_log_path(refs_log_path)
161
+ try:
162
+ refs_log_path.parent.mkdir(parents=True, exist_ok=True)
163
+ except OSError as exc:
164
+ raise UnsafeRefsLogPathError(
165
+ f"failed to create refs log directory: {refs_log_path.parent}"
166
+ ) from exc
167
+ _assert_safe_refs_log_path(refs_log_path)
168
+
169
+ # Unique temp name per call in the same directory (so os.replace is an
170
+ # atomic same-filesystem rename). ``mkstemp`` generates a fresh random name
171
+ # and opens it with ``O_CREAT | O_EXCL | O_NOFOLLOW`` (POSIX) at mode 0600 —
172
+ # a leftover temp from an earlier crashed rewrite can never collide and
173
+ # block subsequent backfills.
174
+ try:
175
+ fd, tmp_name = tempfile.mkstemp(
176
+ dir=refs_log_path.parent,
177
+ prefix=f"{refs_log_path.name}.",
178
+ suffix=".tmp",
179
+ )
180
+ except OSError as exc:
181
+ raise UnsafeRefsLogPathError(
182
+ f"failed to open refs log temp file safely: {refs_log_path}"
183
+ ) from exc
184
+ tmp_path = Path(tmp_name)
185
+ try:
186
+ with os.fdopen(fd, "w", encoding="utf-8") as fp:
187
+ for entry in entries:
188
+ fp.write(json.dumps(entry, default=str, ensure_ascii=False) + "\n")
189
+ os.replace(tmp_path, refs_log_path)
190
+ except OSError as exc:
191
+ # The live manifest is untouched; drop the partial temp file.
192
+ try:
193
+ os.unlink(tmp_path)
194
+ except OSError:
195
+ pass
196
+ raise UnsafeRefsLogPathError(
197
+ f"failed to write refs log: {refs_log_path}"
198
+ ) from exc
199
+
200
+
138
201
  @contextmanager
139
202
  def _locked_turn_refs_manifest(
140
203
  refs_log_path: Path,
@@ -14,6 +14,7 @@ from unique_sdk.cli.commands._citation_manifest import (
14
14
  _append_turn_refs_manifest_entry,
15
15
  _locked_turn_refs_manifest,
16
16
  _read_turn_refs_manifest,
17
+ _rewrite_turn_refs_manifest,
17
18
  )
18
19
  from unique_sdk.cli.formatting import format_mcp_response
19
20
  from unique_sdk.cli.state import ShellState
@@ -43,6 +44,41 @@ _MCP_MAX_ITEMS_PER_CALL = 8
43
44
  # Keys an MCP tool's JSON result commonly uses for a record's human title.
44
45
  _TITLE_KEYS = ("title", "name", "displayName", "subject", "summary", "key")
45
46
 
47
+ # Keys an MCP tool's JSON result commonly uses for the optional "details" line
48
+ # (UN-22310) — e.g. a date and an author such as "10/10/2026 - Jamie Dimon".
49
+ # Best-effort: only top-level record keys are inspected for these (nested
50
+ # provenance is too tool-specific to generalize). The values themselves may be
51
+ # nested one level — a date is read as a top-level string, while an author may
52
+ # be a plain string or an object whose name is pulled via `_AUTHOR_NAME_KEYS`
53
+ # (e.g. Atlassian's `{"displayName": "..."}`). The line is omitted when neither
54
+ # a date nor an author is found.
55
+ _DETAILS_DATE_KEYS = (
56
+ "date",
57
+ "updated",
58
+ "updatedAt",
59
+ "updatedDate",
60
+ "lastModified",
61
+ "modified",
62
+ "created",
63
+ "createdAt",
64
+ "createdDate",
65
+ "timestamp",
66
+ )
67
+ _DETAILS_AUTHOR_KEYS = (
68
+ "author",
69
+ "creator",
70
+ "owner",
71
+ "createdBy",
72
+ "updatedBy",
73
+ "by",
74
+ "sender",
75
+ "from",
76
+ )
77
+ # Keys to read a human name out of an author value that is itself an object
78
+ # (e.g. Atlassian's ``{"displayName": "Jamie Dimon"}``).
79
+ _AUTHOR_NAME_KEYS = ("displayName", "name", "fullName", "emailAddress", "email")
80
+ _MCP_DETAILS_CHAR_LIMIT = 200
81
+
46
82
  # Canonical ``toolName`` written to both manifests: the **bare advertised tool
47
83
  # name** (the payload ``name`` the agent passes to ``unique-cli mcp``).
48
84
  # ``unique-cli mcp`` only runs in skills mode, where the agent invokes a tool by
@@ -83,6 +119,39 @@ def _title_from_json(obj: dict[str, Any]) -> str | None:
83
119
  return None
84
120
 
85
121
 
122
+ def _first_str_by_keys(obj: dict[str, Any], keys: tuple[str, ...]) -> str | None:
123
+ for key in keys:
124
+ value = obj.get(key)
125
+ if isinstance(value, str) and value.strip():
126
+ return value.strip()
127
+ return None
128
+
129
+
130
+ def _author_display(obj: dict[str, Any]) -> str | None:
131
+ """Read an author name from a record, whether the author value is a plain
132
+ string or a nested object (e.g. ``{"displayName": "..."}``)."""
133
+ for key in _DETAILS_AUTHOR_KEYS:
134
+ value = obj.get(key)
135
+ if isinstance(value, str) and value.strip():
136
+ return value.strip()
137
+ if isinstance(value, dict):
138
+ name = _first_str_by_keys(value, _AUTHOR_NAME_KEYS)
139
+ if name:
140
+ return name
141
+ return None
142
+
143
+
144
+ def _details_from_json(obj: dict[str, Any]) -> str | None:
145
+ """Best-effort optional "details" line (UN-22310): a date and/or author
146
+ composed as "<date> - <author>". Returns ``None`` when neither is found."""
147
+ date = _first_str_by_keys(obj, _DETAILS_DATE_KEYS)
148
+ author = _author_display(obj)
149
+ parts = [part for part in (date, author) if part]
150
+ if not parts:
151
+ return None
152
+ return " - ".join(parts)[:_MCP_DETAILS_CHAR_LIMIT]
153
+
154
+
86
155
  def _titles_from_json(text: str) -> list[dict[str, Any]]:
87
156
  """Best-effort: pull a human title out of a JSON result (object or list of
88
157
  objects), e.g. an Atlassian page/issue returned as JSON-in-text. Returns []
@@ -98,7 +167,13 @@ def _titles_from_json(text: str) -> list[dict[str, Any]]:
98
167
  if isinstance(entry, dict):
99
168
  title = _title_from_json(entry)
100
169
  if title:
101
- items.append({"title": title, "snippet": None})
170
+ items.append(
171
+ {
172
+ "title": title,
173
+ "snippet": None,
174
+ "details": _details_from_json(entry),
175
+ }
176
+ )
102
177
  return items
103
178
 
104
179
 
@@ -189,6 +264,12 @@ def _annotate_mcp_results_for_citations(
189
264
  numbers_by_key[_item_dedup_key(stored_tool, entry)] = entry[
190
265
  "sourceNumber"
191
266
  ]
267
+ entries_by_number = {
268
+ entry["sourceNumber"]: entry
269
+ for entry in entries
270
+ if isinstance(entry.get("sourceNumber"), int)
271
+ }
272
+ needs_rewrite = False
192
273
  for item in items:
193
274
  key = _item_dedup_key(tool_name, item)
194
275
  source_number = numbers_by_key.get(key)
@@ -200,6 +281,7 @@ def _annotate_mcp_results_for_citations(
200
281
  "serverName": server_name,
201
282
  "title": item.get("title"),
202
283
  "snippet": item.get("snippet"),
284
+ "details": item.get("details"),
203
285
  }
204
286
  try:
205
287
  _append_turn_refs_manifest_entry(refs_log_path, manifest_entry)
@@ -210,7 +292,25 @@ def _annotate_mcp_results_for_citations(
210
292
  break
211
293
  numbers_by_key[key] = source_number
212
294
  entries.append(manifest_entry)
295
+ entries_by_number[source_number] = manifest_entry
296
+ else:
297
+ # Deduped: a prior call already claimed this source number.
298
+ # Backfill ``details`` the first call may have lacked (the
299
+ # runner reads one entry per source number, so enriching it
300
+ # in place is sufficient) — only when newly extracted.
301
+ stored = entries_by_number.get(source_number)
302
+ new_details = item.get("details")
303
+ if stored is not None and new_details and not stored.get("details"):
304
+ stored["details"] = new_details
305
+ needs_rewrite = True
213
306
  annotated.append((source_number, item))
307
+ if needs_rewrite:
308
+ try:
309
+ _rewrite_turn_refs_manifest(refs_log_path, entries)
310
+ except (UnsafeRefsLogPathError, OSError) as exc:
311
+ _LOGGER.warning(
312
+ "mcp: failed to backfill refs manifest details: %s", exc
313
+ )
214
314
  except (UnsafeRefsLogPathError, OSError) as exc:
215
315
  _LOGGER.warning("mcp: failed to append refs manifest: %s", exc)
216
316
  return []
@@ -224,7 +324,12 @@ def _citation_footer(annotated: list[tuple[int, dict[str, Any]]]) -> str:
224
324
  """Tell the agent which marker to cite each retrieved item with."""
225
325
  if not annotated:
226
326
  return ""
227
- lines = ["", "Sources — cite each fact with the marker for the item it came from:"]
327
+ lines = [
328
+ "",
329
+ "Sources — MANDATORY: every fact you take from this result MUST be "
330
+ "cited inline with its [mcpsourceN] marker below, or it will not be "
331
+ "referenced in the answer:",
332
+ ]
228
333
  for source_number, item in annotated:
229
334
  label = item.get("title") or "this MCP tool result"
230
335
  lines.append(f" [mcpsource{source_number}] {label}")