unique-sdk 2026.30.0.dev6__tar.gz → 2026.30.0.dev7__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 (89) hide show
  1. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/PKG-INFO +1 -1
  2. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/pyproject.toml +1 -1
  3. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/cli.py +65 -12
  4. unique_sdk-2026.30.0.dev7/unique_sdk/cli/identity.py +150 -0
  5. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +98 -22
  6. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +1 -1
  7. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +4 -2
  8. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/README.md +0 -0
  9. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/__init__.py +0 -0
  10. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_api_requestor.py +0 -0
  11. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_api_resource.py +0 -0
  12. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_api_version.py +0 -0
  13. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_error.py +0 -0
  14. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_http_client.py +0 -0
  15. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_list_object.py +0 -0
  16. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_object_classes.py +0 -0
  17. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_request_options.py +0 -0
  18. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_unique_object.py +0 -0
  19. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_unique_ql.py +0 -0
  20. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_unique_response.py +0 -0
  21. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_util.py +0 -0
  22. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_version.py +0 -0
  23. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/_webhook.py +0 -0
  24. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/__init__.py +0 -0
  25. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_acronyms.py +0 -0
  26. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_agentic_table.py +0 -0
  27. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_analytics_order.py +0 -0
  28. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_benchmarking.py +0 -0
  29. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_briefing.py +0 -0
  30. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_chat_completion.py +0 -0
  31. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_content.py +0 -0
  32. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_dynamic_frontend.py +0 -0
  33. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_elicitation.py +0 -0
  34. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_embedding.py +0 -0
  35. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_event.py +0 -0
  36. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_folder.py +0 -0
  37. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_group.py +0 -0
  38. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_integrated.py +0 -0
  39. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_llm_models.py +0 -0
  40. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_mcp.py +0 -0
  41. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_message.py +0 -0
  42. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_message_assessment.py +0 -0
  43. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_message_execution.py +0 -0
  44. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_message_log.py +0 -0
  45. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_message_tool.py +0 -0
  46. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_module.py +0 -0
  47. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  48. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_search.py +0 -0
  49. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_search_string.py +0 -0
  50. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  51. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_space.py +0 -0
  52. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_user.py +0 -0
  53. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/api_resources/_web_search.py +0 -0
  54. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/__init__.py +0 -0
  55. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/__main__.py +0 -0
  56. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/__init__.py +0 -0
  57. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
  58. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/browser.py +0 -0
  59. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/cite_file.py +0 -0
  60. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/dynamic_frontend.py +0 -0
  61. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/elicitation.py +0 -0
  62. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/files.py +0 -0
  63. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/folders.py +0 -0
  64. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/mcp.py +0 -0
  65. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/navigation.py +0 -0
  66. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/read.py +0 -0
  67. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  68. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/search.py +0 -0
  69. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/subagent.py +0 -0
  70. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/web_search.py +0 -0
  71. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/commands/web_search_config.py +0 -0
  72. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/config.py +0 -0
  73. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/formatting.py +0 -0
  74. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/metadata_filter.py +0 -0
  75. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/shell.py +0 -0
  76. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/skills/unique-cli-dynamic-frontend/SKILL.md +0 -0
  77. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
  78. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  79. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  80. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
  81. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  82. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/cli/state.py +0 -0
  83. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/utils/analytics_order_run.py +0 -0
  84. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/utils/benchmarking_run.py +0 -0
  85. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/utils/chat_history.py +0 -0
  86. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/utils/chat_in_space.py +0 -0
  87. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/utils/file_io.py +0 -0
  88. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/unique_sdk/utils/sources.py +0 -0
  89. {unique_sdk-2026.30.0.dev6 → unique_sdk-2026.30.0.dev7}/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.dev6
3
+ Version: 2026.30.0.dev7
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.dev6"
3
+ version = "2026.30.0.dev7"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -84,11 +84,38 @@ from unique_sdk.cli.commands.web_search import (
84
84
  )
85
85
  from unique_sdk.cli.commands.web_search_config import ENV_CONFIG_PATH
86
86
  from unique_sdk.cli.config import load_config
87
+ from unique_sdk.cli.identity import TurnIdentityError, resolve_message_id
87
88
  from unique_sdk.cli.shell import UniqueShell
88
89
  from unique_sdk.cli.state import ShellState
89
90
 
90
91
  _DYNAMIC_FRONTEND_ERROR_PREFIX = "dynamic-frontend "
91
92
 
93
+
94
+ def _resolve_cli_message_id(
95
+ ctx: click.Context,
96
+ explicit: str | None,
97
+ *,
98
+ required: bool = False,
99
+ ) -> str | None:
100
+ """Resolve message id for Click commands; exit on turn-identity errors.
101
+
102
+ When *required* is True and no source yields a value, exits with code 2.
103
+ """
104
+ try:
105
+ resolved = resolve_message_id(explicit)
106
+ except TurnIdentityError as exc:
107
+ click.echo(f"Error: {exc}", err=True)
108
+ ctx.exit(2)
109
+ if required and not resolved:
110
+ click.echo(
111
+ "Error: message id is required. Pass --message-id, or set "
112
+ "UNIQUE_TURN_IDENTITY_FILE / UNIQUE_MESSAGE_ID.",
113
+ err=True,
114
+ )
115
+ ctx.exit(2)
116
+ return resolved
117
+
118
+
92
119
  MAIN_HELP = """\
93
120
  Unique CLI -- Linux-like file explorer for the Unique AI Platform.
94
121
 
@@ -802,8 +829,12 @@ def uploaded_search(
802
829
  @click.option(
803
830
  "--message-id",
804
831
  "-m",
805
- required=True,
806
- help="Message ID for the MCP tool call context.",
832
+ default=None,
833
+ help=(
834
+ "Message ID for the MCP tool call context. Defaults to the "
835
+ "current turn identity file ($UNIQUE_TURN_IDENTITY_FILE), then "
836
+ "$UNIQUE_MESSAGE_ID."
837
+ ),
807
838
  )
808
839
  @click.option(
809
840
  "--file",
@@ -824,7 +855,7 @@ def mcp(
824
855
  ctx: click.Context,
825
856
  payload: str | None,
826
857
  chat_id: str,
827
- message_id: str,
858
+ message_id: str | None,
828
859
  file_path: str | None,
829
860
  use_stdin: bool,
830
861
  ) -> None:
@@ -837,7 +868,8 @@ def mcp(
837
868
  \b
838
869
  The JSON is forwarded 1:1 to the MCP call-tool API. Chat ID and
839
870
  message ID are provided as separate flags to identify the
840
- conversation context.
871
+ conversation context. When --message-id is omitted, the CLI resolves
872
+ it from $UNIQUE_TURN_IDENTITY_FILE (preferred) or $UNIQUE_MESSAGE_ID.
841
873
 
842
874
  \b
843
875
  Input sources (exactly one required):
@@ -854,11 +886,12 @@ def mcp(
854
886
 
855
887
  cat payload.json | unique-cli mcp -c chat_123 -m msg_456 --stdin
856
888
  """
889
+ resolved_message_id = _resolve_cli_message_id(ctx, message_id, required=True)
857
890
  click.echo(
858
891
  cmd_mcp(
859
892
  LazyState.get(ctx),
860
893
  chat_id=chat_id,
861
- message_id=message_id,
894
+ message_id=resolved_message_id or "",
862
895
  payload=payload,
863
896
  file=file_path,
864
897
  stdin=use_stdin,
@@ -887,8 +920,11 @@ def mcp(
887
920
  "--message-id",
888
921
  "parent_message_id",
889
922
  default=None,
890
- envvar="UNIQUE_MESSAGE_ID",
891
- help="Parent message ID for message correlation.",
923
+ help=(
924
+ "Parent message ID for message correlation. Defaults to the "
925
+ "current turn identity file ($UNIQUE_TURN_IDENTITY_FILE), then "
926
+ "$UNIQUE_MESSAGE_ID."
927
+ ),
892
928
  )
893
929
  @click.option(
894
930
  "--assistant-id",
@@ -931,13 +967,14 @@ def subagent(
931
967
  unique-cli subagent LegalReview "Review this contract clause"
932
968
  unique-cli subagent Finance "Summarize Q4 revenue" --reset-chat
933
969
  """
970
+ resolved_message_id = _resolve_cli_message_id(ctx, parent_message_id)
934
971
  output = cmd_subagent(
935
972
  LazyState.get(ctx),
936
973
  tool_name=tool_name,
937
974
  message=message,
938
975
  config_path=config_path,
939
976
  parent_chat_id=parent_chat_id,
940
- parent_message_id=parent_message_id,
977
+ parent_message_id=resolved_message_id,
941
978
  parent_assistant_id=parent_assistant_id,
942
979
  reset_chat=reset_chat,
943
980
  output_json=output_json,
@@ -1195,7 +1232,15 @@ def elicit() -> None:
1195
1232
  ),
1196
1233
  )
1197
1234
  @click.option("--chat-id", "-c", default=None, help="Associated chat ID.")
1198
- @click.option("--message-id", "-m", default=None, help="Associated message ID.")
1235
+ @click.option(
1236
+ "--message-id",
1237
+ "-m",
1238
+ default=None,
1239
+ help=(
1240
+ "Associated message ID. Defaults to the current turn identity "
1241
+ "file ($UNIQUE_TURN_IDENTITY_FILE), then $UNIQUE_MESSAGE_ID."
1242
+ ),
1243
+ )
1199
1244
  @click.option(
1200
1245
  "--timeout",
1201
1246
  type=int,
@@ -1299,7 +1344,7 @@ def elicit_ask(
1299
1344
  "tool_name": tool_name,
1300
1345
  "schema": schema,
1301
1346
  "chat_id": chat_id,
1302
- "message_id": message_id,
1347
+ "message_id": _resolve_cli_message_id(ctx, message_id),
1303
1348
  "timeout": timeout,
1304
1349
  "poll_interval": poll_interval,
1305
1350
  "metadata": parsed_metadata or None,
@@ -1334,7 +1379,15 @@ def elicit_ask(
1334
1379
  )
1335
1380
  @click.option("--url", default=None, help="External URL (required for --mode URL).")
1336
1381
  @click.option("--chat-id", "-c", default=None, help="Associated chat ID.")
1337
- @click.option("--message-id", "-m", default=None, help="Associated message ID.")
1382
+ @click.option(
1383
+ "--message-id",
1384
+ "-m",
1385
+ default=None,
1386
+ help=(
1387
+ "Associated message ID. Defaults to the current turn identity "
1388
+ "file ($UNIQUE_TURN_IDENTITY_FILE), then $UNIQUE_MESSAGE_ID."
1389
+ ),
1390
+ )
1338
1391
  @click.option(
1339
1392
  "--expires-in",
1340
1393
  "expires_in_seconds",
@@ -1439,7 +1492,7 @@ def elicit_create(
1439
1492
  "schema": schema,
1440
1493
  "url": url,
1441
1494
  "chat_id": chat_id,
1442
- "message_id": message_id,
1495
+ "message_id": _resolve_cli_message_id(ctx, message_id),
1443
1496
  "expires_in_seconds": expires_in_seconds,
1444
1497
  "external_elicitation_id": external_elicitation_id,
1445
1498
  "metadata": parsed_metadata or None,
@@ -0,0 +1,150 @@
1
+ """Resolve the current turn's assistant message identity.
2
+
3
+ Persistent agent subprocesses freeze their OS environment at spawn time, so
4
+ ``UNIQUE_MESSAGE_ID`` can be stale on later turns. The parent runner writes a
5
+ per-turn identity file and exposes its path via ``UNIQUE_TURN_IDENTITY_FILE``
6
+ (stable across turns). Fresh ``unique-cli`` invocations then read the current
7
+ message ID from that file.
8
+
9
+ Resolution precedence for message IDs:
10
+
11
+ 1. Explicit ``--message-id`` / ``-m`` flag value
12
+ 2. Turn-identity file pointed to by ``$UNIQUE_TURN_IDENTITY_FILE``
13
+ 3. ``$UNIQUE_MESSAGE_ID`` environment variable (one-shot / external callers)
14
+
15
+ When ``UNIQUE_TURN_IDENTITY_FILE`` is set but the file is missing or malformed,
16
+ resolution fails loudly — silent fallback to a stale env value is forbidden.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ from dataclasses import dataclass
24
+ from pathlib import Path
25
+
26
+ TURN_IDENTITY_ENV_VAR = "UNIQUE_TURN_IDENTITY_FILE"
27
+ MESSAGE_ID_ENV_VAR = "UNIQUE_MESSAGE_ID"
28
+ CHAT_ID_ENV_VAR = "UNIQUE_CHAT_ID"
29
+
30
+
31
+ class TurnIdentityError(ValueError):
32
+ """Raised when the turn-identity file is configured but unusable."""
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class TurnIdentity:
37
+ """Parsed contents of the per-turn identity file.
38
+
39
+ Mirrors the JSON contract written by the platform runner. Only
40
+ ``message_id`` is mandatory; the remaining fields are informational.
41
+ """
42
+
43
+ message_id: str
44
+ chat_id: str | None = None
45
+ user_id: str | None = None
46
+ company_id: str | None = None
47
+ assistant_id: str | None = None
48
+ turn: int | None = None
49
+
50
+
51
+ def _optional_str(payload: dict[str, object], key: str) -> str | None:
52
+ value = payload.get(key)
53
+ if isinstance(value, str) and value.strip():
54
+ return value.strip()
55
+ return None
56
+
57
+
58
+ def read_turn_identity(
59
+ path: str | Path | None = None,
60
+ ) -> TurnIdentity | None:
61
+ """Load and validate the turn-identity JSON file.
62
+
63
+ When *path* is ``None``, reads ``$UNIQUE_TURN_IDENTITY_FILE``. Returns
64
+ ``None`` when neither is set. Raises ``TurnIdentityError`` if the env
65
+ var / path is set but the file cannot be read or does not contain a
66
+ non-empty ``message_id`` string.
67
+ """
68
+ raw_path = str(path) if path is not None else os.environ.get(TURN_IDENTITY_ENV_VAR)
69
+ if not raw_path:
70
+ return None
71
+
72
+ identity_path = Path(raw_path)
73
+ if not identity_path.is_file():
74
+ raise TurnIdentityError(
75
+ f"{TURN_IDENTITY_ENV_VAR} is set to {raw_path!r} but the file "
76
+ "is missing; refusing to fall back to a stale message id"
77
+ )
78
+ if identity_path.is_symlink():
79
+ raise TurnIdentityError(
80
+ f"refusing to read turn-identity file {raw_path!r}: path is a symlink"
81
+ )
82
+
83
+ try:
84
+ payload = json.loads(identity_path.read_text(encoding="utf-8"))
85
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
86
+ raise TurnIdentityError(
87
+ f"failed to read turn-identity file {raw_path!r}: {exc}"
88
+ ) from exc
89
+
90
+ if not isinstance(payload, dict):
91
+ raise TurnIdentityError(
92
+ f"turn-identity file {raw_path!r} must contain a JSON object"
93
+ )
94
+
95
+ message_id = payload.get("message_id")
96
+ if not isinstance(message_id, str) or not message_id.strip():
97
+ raise TurnIdentityError(
98
+ f"turn-identity file {raw_path!r} is missing a non-empty "
99
+ "'message_id' string"
100
+ )
101
+
102
+ raw_turn = payload.get("turn")
103
+ return TurnIdentity(
104
+ message_id=message_id.strip(),
105
+ chat_id=_optional_str(payload, "chat_id"),
106
+ user_id=_optional_str(payload, "user_id"),
107
+ company_id=_optional_str(payload, "company_id"),
108
+ assistant_id=_optional_str(payload, "assistant_id"),
109
+ turn=raw_turn if isinstance(raw_turn, int) else None,
110
+ )
111
+
112
+
113
+ def resolve_message_id(explicit: str | None = None) -> str | None:
114
+ """Resolve the assistant message ID for a message-bound CLI operation.
115
+
116
+ Returns ``None`` when no source yields a value (callers may then mint a
117
+ placeholder message, as elicit does for visible prompts without a chat
118
+ context). Raises ``TurnIdentityError`` when the turn-identity file is
119
+ configured but unusable.
120
+ """
121
+ if explicit is not None and str(explicit).strip():
122
+ return str(explicit).strip()
123
+
124
+ identity = read_turn_identity()
125
+ if identity is not None:
126
+ return identity.message_id
127
+
128
+ env_id = os.environ.get(MESSAGE_ID_ENV_VAR)
129
+ if env_id is not None and env_id.strip():
130
+ return env_id.strip()
131
+ return None
132
+
133
+
134
+ def resolve_chat_id(explicit: str | None = None) -> str | None:
135
+ """Resolve chat ID with the same file-then-env precedence as message ID.
136
+
137
+ Explicit flag wins. The turn-identity file is consulted next (and fails
138
+ loudly when configured but unusable). Falls back to ``$UNIQUE_CHAT_ID``.
139
+ """
140
+ if explicit is not None and str(explicit).strip():
141
+ return str(explicit).strip()
142
+
143
+ identity = read_turn_identity()
144
+ if identity is not None and identity.chat_id is not None:
145
+ return identity.chat_id
146
+
147
+ env_id = os.environ.get(CHAT_ID_ENV_VAR)
148
+ if env_id is not None and env_id.strip():
149
+ return env_id.strip()
150
+ return None
@@ -5,8 +5,9 @@ description: >-
5
5
  question in free-form chat -- for clarifications, confirmations
6
6
  (especially destructive actions), missing parameters, multiple-choice
7
7
  decisions, or structured form input. Elicitations are routed through
8
- the Unique AI Platform UI via `unique-cli elicit ask` so the user gets
9
- a proper structured prompt and you get a structured answer back.
8
+ the Unique AI Platform UI via `unique-cli elicit create` + `elicit wait`
9
+ (or `elicit ask` outside an agent harness) so the user gets a proper
10
+ structured prompt and you get a structured answer back.
10
11
  Do NOT ask the user in plain chat when you can use this skill instead.
11
12
  ---
12
13
 
@@ -14,21 +15,82 @@ description: >-
14
15
 
15
16
  Use this skill **whenever you need input from the user** -- a clarifying question, a confirmation before a destructive action, a choice between options, or a structured form. Elicitations create a first-class UI prompt on the Unique AI Platform and return the answer as structured JSON.
16
17
 
17
- > **Rule of thumb:** if you catch yourself about to write "Could you clarify…?" or "Do you want me to…?" or "Which one should I pick?" in chat, stop and call `unique-cli elicit ask` instead.
18
+ > **Rule of thumb:** if you catch yourself about to write "Could you clarify…?" or "Do you want me to…?" or "Which one should I pick?" in chat, stop and use `unique-cli elicit create` + `elicit wait` instead (see "The pattern" below).
18
19
 
19
20
  !!! danger "`--visible` is currently MANDATORY"
20
- You **must always run `elicit ask` with visibility on** (it is on by default — just don't pass `--no-visible`). Until the UN-19815 UI fix ships in your environment, an elicitation created without the visibility workaround is stored by the backend but **never rendered in the chat UI** — the user simply never sees the question and you will wait forever. There is no situation in which you should disable it today.
21
-
22
- ## The only command you need: `elicit ask`
23
-
24
- `elicit ask` is a **single call that creates the elicitation, shows it to the user, and blocks until the user answers** (or declines / cancels / expires). This is the command you reach for every time. You do not need to learn `elicit create`, `elicit wait`, `elicit pending`, `elicit get`, or `elicit respond` — they exist for scripting and tests, not for agent workflows.
21
+ You **must always create elicitations with visibility on** (it is on by default for both `elicit create` and `elicit ask` — just don't pass `--no-visible`). Until the UN-19815 UI fix ships in your environment, an elicitation created without the visibility workaround is stored by the backend but **never rendered in the chat UI** — the user simply never sees the question and you will wait forever. There is no situation in which you should disable it today.
22
+
23
+ ## The pattern: `elicit create` + short polling loop with `elicit wait`
24
+
25
+ Do **not** call `elicit ask` from inside an agent harness (Claude Code, Codex,
26
+ or any environment where your Bash/shell tool has its own foreground-wait
27
+ timeout — commonly ~2 minutes). `elicit ask` blocks synchronously for up to
28
+ `--timeout` seconds (default 2 hours) waiting for the human to answer, but a
29
+ human reading and answering a prompt routinely takes longer than a typical
30
+ Bash tool timeout. If that timeout fires first, your harness will silently
31
+ detach the process to the background and hand you a "running in background"
32
+ stub instead of the real answer — you will not see the user's response, and
33
+ the chat may appear stuck to the user.
34
+
35
+ Instead, use `elicit create` (returns immediately) followed by a short
36
+ polling loop with `elicit wait`, where **every individual command finishes
37
+ comfortably under your harness's Bash foreground timeout** (90s is a safe
38
+ default — comfortably under Claude Code's ~120s):
39
+
40
+ 1. **Create** the elicitation:
41
+
42
+ ```bash
43
+ create_output=$(unique-cli elicit create "<question>" \
44
+ --mode FORM \
45
+ --tool-name "<tool_name>" \
46
+ --chat-id "$UNIQUE_CHAT_ID" \
47
+ --expires-in 7200 \
48
+ --schema '<json schema>')
49
+ elicitation_id=$(echo "$create_output" | awk '/^Created elicitation/{print $3}')
50
+ ```
51
+
52
+ 2. **Poll in short bursts**, each well under your harness's Bash foreground
53
+ timeout:
54
+
55
+ ```bash
56
+ status="PENDING"
57
+ elapsed=0
58
+ total_timeout=7200 # match --expires-in above
59
+ chunk=90
60
+ while [ "$elapsed" -lt "$total_timeout" ]; do
61
+ result=$(unique-cli elicit wait "$elicitation_id" --timeout "$chunk" --poll-interval 3)
62
+ status=$(echo "$result" | awk -F': *' '/^Status:/{print $2}')
63
+ case "$status" in
64
+ RESPONDED|ACCEPTED|DECLINED|CANCELLED|REJECTED|EXPIRED|COMPLETED) break ;;
65
+ esac
66
+ elapsed=$((elapsed + chunk))
67
+ done
68
+ ```
69
+
70
+ Each `elicit wait ... --timeout 90` call is a normal, short-lived Bash
71
+ invocation that always returns within ~90 seconds — either with a
72
+ terminal status (done) or the current non-terminal status (loop again).
73
+ Your harness never sees a single call run long enough to background it.
74
+
75
+ 3. Parse `Response:` from the final `$result` exactly as you would with
76
+ `elicit ask`'s output — the format is identical (see "Reading the
77
+ response" below).
78
+
79
+ `elicit ask` remains the right choice for **non-agent, scripted, or
80
+ human-operated CLI usage** where a single blocking call is expected and
81
+ there is no surrounding tool-timeout concern (tests, ops scripts, manual CLI
82
+ use). Do not reach for it from inside an agent turn.
25
83
 
26
84
  ```bash
27
85
  unique-cli elicit ask "<question>" [options]
28
86
  ```
29
87
 
30
- !!! danger "`--chat-id` and `--message-id` are MANDATORY"
31
- You **must always pass both** `--chat-id "$UNIQUE_CHAT_ID"` **and** `--message-id "$UNIQUE_MESSAGE_ID"` on every `elicit ask` call. These environment variables are always available in the agent environment. Without both flags the elicitation is not correctly anchored to the current conversation and the user will not see it.
88
+ !!! danger "`--chat-id` is MANDATORY"
89
+ You **must always pass** `--chat-id "$UNIQUE_CHAT_ID"` on every
90
+ `elicit create`/`elicit ask` call. Omit `--message-id`: the CLI resolves
91
+ the current turn's assistant message ID from `$UNIQUE_TURN_IDENTITY_FILE`
92
+ (preferred) or `$UNIQUE_MESSAGE_ID`. Do **not** pass a stale
93
+ `$UNIQUE_MESSAGE_ID` from a persistent process environment.
32
94
 
33
95
  ## When to use
34
96
 
@@ -43,12 +105,16 @@ unique-cli elicit ask "<question>" [options]
43
105
 
44
106
  ## Examples
45
107
 
108
+ > The examples below use `elicit ask` for brevity to show the schema shapes.
109
+ > From an agent harness, use the same `--schema`/`--message`/`--tool-name`
110
+ > arguments with `elicit create` instead, then poll with `elicit wait` as
111
+ > shown in "The pattern" above.
112
+
46
113
  ### Minimal — free-text answer
47
114
 
48
115
  ```bash
49
116
  unique-cli elicit ask "Which quarter should I report on?" \
50
- --chat-id "$UNIQUE_CHAT_ID" \
51
- --message-id "$UNIQUE_MESSAGE_ID"
117
+ --chat-id "$UNIQUE_CHAT_ID"
52
118
  ```
53
119
 
54
120
  Under the hood this creates a form with a single required string field `answer`. The reply you receive will look like:
@@ -71,7 +137,6 @@ Provide an explicit JSON schema so the user sees proper UI controls instead of a
71
137
  ```bash
72
138
  unique-cli elicit ask "Which report format do you want?" \
73
139
  --chat-id "$UNIQUE_CHAT_ID" \
74
- --message-id "$UNIQUE_MESSAGE_ID" \
75
140
  --schema '{
76
141
  "type": "object",
77
142
  "properties": {
@@ -92,7 +157,6 @@ Always use this before `rm`, `rmdir -r`, mass uploads, or anything irreversible.
92
157
  ```bash
93
158
  unique-cli elicit ask "Confirm deleting /Archive/2024 and everything inside it" \
94
159
  --chat-id "$UNIQUE_CHAT_ID" \
95
- --message-id "$UNIQUE_MESSAGE_ID" \
96
160
  --schema '{
97
161
  "type": "object",
98
162
  "properties": {
@@ -112,7 +176,6 @@ Proceed **only** if the response contains `"confirm": true`. Treat `DECLINED`, `
112
176
  ```bash
113
177
  unique-cli elicit ask "Please provide report settings" \
114
178
  --chat-id "$UNIQUE_CHAT_ID" \
115
- --message-id "$UNIQUE_MESSAGE_ID" \
116
179
  --schema '{
117
180
  "type": "object",
118
181
  "properties": {
@@ -127,10 +190,16 @@ unique-cli elicit ask "Please provide report settings" \
127
190
 
128
191
  ## Options
129
192
 
193
+ The table below documents `elicit ask`'s flags. `elicit create` takes the
194
+ same `--chat-id`, `--message-id`, `--tool-name`, `--schema`, `--metadata`,
195
+ and `--assistant-id` flags, but uses `--expires-in <seconds>` instead of
196
+ `--timeout`/`--poll-interval` (those two apply only to `elicit wait`, which
197
+ you call separately in the polling pattern).
198
+
130
199
  | Option | Short | Default | Description |
131
200
  |--------|-------|---------|-------------|
132
201
  | `--chat-id` | `-c` | none | **MANDATORY.** Chat to show the question in. Always pass `"$UNIQUE_CHAT_ID"`. Without it the visibility workaround cannot run and the user will not see the elicitation. |
133
- | `--message-id` | `-m` | none | **MANDATORY.** The current assistant message ID. Always pass `"$UNIQUE_MESSAGE_ID"`. Anchors the elicitation to the correct message in the conversation thread. |
202
+ | `--message-id` | `-m` | auto | Optional. Prefer omitting this flag — the CLI resolves the current turn's message ID from `$UNIQUE_TURN_IDENTITY_FILE` (preferred) or `$UNIQUE_MESSAGE_ID`. Do not pass a stale env value from a persistent process. |
134
203
  | `--tool-name` | `-t` | `agent_question` | Short snake_case label shown to the user (e.g. `clarify`, `confirm_delete`, `choose_report`). |
135
204
  | `--schema` | | single `answer` string | JSON Schema for the form body. |
136
205
  | `--timeout` | | `7200` | Max seconds to block locally before giving up. This is the single knob for `ask`: it also sets when the request expires on the platform, so the prompt expires exactly when you stop waiting and the chat UI can offer the user a way to continue. |
@@ -187,14 +256,20 @@ If the exact structured response is useful for auditing or debugging, put it beh
187
256
 
188
257
  Do not expose raw JSON by default when a natural-language confirmation would be clearer.
189
258
 
190
- ## Scripting pattern
259
+ ## Scripting pattern (non-agent-harness only)
260
+
261
+ This one-shot pattern is for **scripts, tests, or manual CLI use outside an
262
+ agent harness** — i.e. contexts with no Bash-tool foreground timeout to worry
263
+ about. From inside an agent harness, use the `elicit create` + `elicit wait`
264
+ polling pattern from "The pattern" section above instead; the output parsing
265
+ below (pulling `Response:` out of the text) is identical either way, only the
266
+ command(s) producing that output differ.
191
267
 
192
268
  In a shell script or agent tool wrapper, capture the output and pull out the `Response:` line:
193
269
 
194
270
  ```bash
195
271
  result=$(unique-cli elicit ask "Which region?" \
196
272
  --chat-id "$UNIQUE_CHAT_ID" \
197
- --message-id "$UNIQUE_MESSAGE_ID" \
198
273
  --schema '{
199
274
  "type":"object",
200
275
  "properties":{"region":{"type":"string","enum":["EU","US","APAC"]}},
@@ -220,9 +295,9 @@ esac
220
295
 
221
296
  ## Agent workflow rules
222
297
 
223
- 1. **Default to `elicit ask`.** If you need an answer from the user, use this command, not a chat message. Do not use any of the other `elicit *` subcommands from an agent.
298
+ 1. **Default to `elicit create` + `elicit wait` polling.** If you need an answer from the user, use this pattern, not a chat message and not a single blocking `elicit ask` call. See "The pattern" above.
224
299
  2. **Always pass `--chat-id "$UNIQUE_CHAT_ID"`.** Without it the elicitation is not attached to a chat and the user will not see it.
225
- 3. **Always pass `--message-id "$UNIQUE_MESSAGE_ID"`.** This anchors the elicitation to the current message in the conversation. Both `$UNIQUE_CHAT_ID` and `$UNIQUE_MESSAGE_ID` are always available as environment variables — never omit either.
300
+ 3. **Omit `--message-id`.** The CLI resolves the current turn's assistant message ID from `$UNIQUE_TURN_IDENTITY_FILE` (preferred) or `$UNIQUE_MESSAGE_ID`. Do not pass a stale `$UNIQUE_MESSAGE_ID` from a persistent process environment.
226
301
  4. **Never pass `--no-visible`.** See the warning above. The visibility workaround is mandatory today.
227
302
  5. **Never run destructive CLI commands without a confirmation elicitation.** This includes `rm`, `rmdir -r`, bulk renames, large uploads, schedule deletion, etc.
228
303
  6. **Pick a meaningful `--tool-name`.** `confirm_delete`, `choose_region`, `pick_report` -- short snake_case describing the intent.
@@ -231,7 +306,7 @@ esac
231
306
  9. **Handle non-`RESPONDED` outcomes explicitly.** If the status is `DECLINED` / `CANCELLED` / `EXPIRED`, tell the user you stopped and ask what they want to do next instead of silently proceeding.
232
307
  10. **Don't spam elicitations.** One well-designed form with a few related fields is better than five sequential yes/no questions.
233
308
  11. **Cap each elicitation at 5 questions.** If you need more than 5 answers, split them into multiple focused elicitations so the user can respond confidently.
234
- 12. **Respect timeouts.** The default `--timeout` is 2 hours -- override it only when the task needs a shorter or longer wait.
309
+ 12. **Never make a single blocking call longer than your harness's Bash foreground timeout.** Use `--expires-in` on `elicit create` to set the real, human-scale deadline (e.g. 7200s / 2 hours), but keep each individual `elicit wait --timeout N` call short (≈90s) and loop until a terminal status or the overall deadline is reached. Never call `elicit ask` with a multi-minute `--timeout` from an agent harness.
235
310
 
236
311
  ## Prerequisites
237
312
 
@@ -243,7 +318,8 @@ UNIQUE_COMPANY_ID # Company ID (required)
243
318
  UNIQUE_API_KEY # API key -- optional on localhost / secured cluster
244
319
  UNIQUE_APP_ID # App ID -- optional on localhost / secured cluster
245
320
  UNIQUE_CHAT_ID # Current chat ID -- always pass as --chat-id (required)
246
- UNIQUE_MESSAGE_ID # Current message ID -- always pass as --message-id (required)
321
+ UNIQUE_TURN_IDENTITY_FILE # Per-turn identity JSON — CLI resolves message ID from here
322
+ UNIQUE_MESSAGE_ID # Fallback message ID when no turn-identity file is present
247
323
  ```
248
324
 
249
325
  Install: `pip install unique-sdk`
@@ -77,7 +77,7 @@ unique-cli mcp [--chat-id <id>] [--message-id <id>] [PAYLOAD | --file <path> | -
77
77
  | Option | Short | Required | Description |
78
78
  |--------|-------|----------|-------------|
79
79
  | `--chat-id` | `-c` | Yes | Chat ID for the conversation context |
80
- | `--message-id` | `-m` | Yes | Message ID for the conversation context |
80
+ | `--message-id` | `-m` | No | Message ID for the conversation context. Optional — resolved from `$UNIQUE_TURN_IDENTITY_FILE` (preferred) or `$UNIQUE_MESSAGE_ID` when omitted. |
81
81
  | `PAYLOAD` | | One of three | Inline JSON string |
82
82
  | `--file` | `-f` | One of three | Path to a JSON file |
83
83
  | `--stdin` | | One of three | Read JSON from stdin |
@@ -17,10 +17,12 @@ the tool name shown in the generated connected-space skill.
17
17
  ```bash
18
18
  unique-cli subagent "<tool_name>" "<message>" \
19
19
  --chat-id "$UNIQUE_CHAT_ID" \
20
- --message-id "$UNIQUE_MESSAGE_ID" \
21
20
  --assistant-id "$UNIQUE_ASSISTANT_ID"
22
21
  ```
23
22
 
23
+ Omit `--message-id`: the CLI resolves the current turn's assistant message ID
24
+ from `$UNIQUE_TURN_IDENTITY_FILE` automatically.
25
+
24
26
  ## Rules
25
27
 
26
28
  1. Use the exact tool name from the connected-space skill or from
@@ -49,7 +51,7 @@ The platform sets these environment variables automatically:
49
51
  UNIQUE_USER_ID
50
52
  UNIQUE_COMPANY_ID
51
53
  UNIQUE_CHAT_ID
52
- UNIQUE_MESSAGE_ID
54
+ UNIQUE_TURN_IDENTITY_FILE
53
55
  UNIQUE_ASSISTANT_ID
54
56
  UNIQUE_API_KEY
55
57
  UNIQUE_APP_ID