unique-sdk 2026.24.0.dev0__tar.gz → 2026.24.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 (79) hide show
  1. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/PKG-INFO +1 -1
  2. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/pyproject.toml +1 -1
  3. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/cli.py +86 -0
  4. unique_sdk-2026.24.0.dev1/unique_sdk/cli/commands/subagent.py +298 -0
  5. unique_sdk-2026.24.0.dev1/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +56 -0
  6. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/README.md +0 -0
  7. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/__init__.py +0 -0
  8. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_api_requestor.py +0 -0
  9. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_api_resource.py +0 -0
  10. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_api_version.py +0 -0
  11. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_error.py +0 -0
  12. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_http_client.py +0 -0
  13. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_list_object.py +0 -0
  14. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_object_classes.py +0 -0
  15. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_request_options.py +0 -0
  16. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_unique_object.py +0 -0
  17. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_unique_ql.py +0 -0
  18. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_unique_response.py +0 -0
  19. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_util.py +0 -0
  20. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_version.py +0 -0
  21. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/_webhook.py +0 -0
  22. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/__init__.py +0 -0
  23. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_acronyms.py +0 -0
  24. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_agentic_table.py +0 -0
  25. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_analytics_order.py +0 -0
  26. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_benchmarking.py +0 -0
  27. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_briefing.py +0 -0
  28. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_chat_completion.py +0 -0
  29. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_content.py +0 -0
  30. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_elicitation.py +0 -0
  31. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_embedding.py +0 -0
  32. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_event.py +0 -0
  33. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_folder.py +0 -0
  34. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_group.py +0 -0
  35. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_integrated.py +0 -0
  36. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_llm_models.py +0 -0
  37. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_mcp.py +0 -0
  38. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_message.py +0 -0
  39. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_message_assessment.py +0 -0
  40. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_message_execution.py +0 -0
  41. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_message_log.py +0 -0
  42. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_message_tool.py +0 -0
  43. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_module.py +0 -0
  44. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  45. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_search.py +0 -0
  46. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_search_string.py +0 -0
  47. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  48. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_space.py +0 -0
  49. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_user.py +0 -0
  50. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/api_resources/_web_search.py +0 -0
  51. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/__init__.py +0 -0
  52. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/__main__.py +0 -0
  53. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/commands/__init__.py +0 -0
  54. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/commands/elicitation.py +0 -0
  55. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/commands/files.py +0 -0
  56. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/commands/folders.py +0 -0
  57. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/commands/mcp.py +0 -0
  58. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/commands/navigation.py +0 -0
  59. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  60. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/commands/search.py +0 -0
  61. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/commands/web_search.py +0 -0
  62. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/commands/web_search_config.py +0 -0
  63. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/config.py +0 -0
  64. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/formatting.py +0 -0
  65. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/shell.py +0 -0
  66. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  67. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
  68. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  69. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  70. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  71. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  72. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/cli/state.py +0 -0
  73. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/utils/analytics_order_run.py +0 -0
  74. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/utils/benchmarking_run.py +0 -0
  75. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/utils/chat_history.py +0 -0
  76. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/utils/chat_in_space.py +0 -0
  77. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/utils/file_io.py +0 -0
  78. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.0.dev1}/unique_sdk/utils/sources.py +0 -0
  79. {unique_sdk-2026.24.0.dev0 → unique_sdk-2026.24.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.24.0.dev0
3
+ Version: 2026.24.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.24.0.dev0"
3
+ version = "2026.24.0.dev1"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -27,6 +27,10 @@ from unique_sdk.cli.commands.scheduled_tasks import (
27
27
  cmd_schedule_update,
28
28
  )
29
29
  from unique_sdk.cli.commands.search import cmd_search
30
+ from unique_sdk.cli.commands.subagent import cmd_subagent
31
+ from unique_sdk.cli.commands.subagent import (
32
+ is_error_output as _is_subagent_error_output,
33
+ )
30
34
  from unique_sdk.cli.commands.web_search import (
31
35
  cmd_web_crawl,
32
36
  cmd_web_search,
@@ -82,6 +86,7 @@ Examples:
82
86
  unique-cli upload ./file.pdf Upload to current folder
83
87
  unique-cli download cont_abc123 Download by content ID
84
88
  unique-cli elicit ask "Which?" Ask the user a question synchronously
89
+ unique-cli subagent Legal "Review" Invoke a connected space/subagent
85
90
  unique-cli web-search search "x" Search the web via the public API
86
91
  unique-cli web-search crawl URL Crawl a URL via the public API
87
92
  """
@@ -465,6 +470,87 @@ def mcp(
465
470
  )
466
471
 
467
472
 
473
+ @main.command()
474
+ @click.argument("tool_name")
475
+ @click.argument("message")
476
+ @click.option(
477
+ "--config",
478
+ "config_path",
479
+ default=None,
480
+ type=click.Path(exists=True),
481
+ help="Path to .unique-subagents.json. Defaults to $UNIQUE_SUBAGENTS_CONFIG or cwd.",
482
+ )
483
+ @click.option(
484
+ "--chat-id",
485
+ "parent_chat_id",
486
+ default=None,
487
+ envvar="UNIQUE_CHAT_ID",
488
+ help="Parent chat ID for message correlation.",
489
+ )
490
+ @click.option(
491
+ "--message-id",
492
+ "parent_message_id",
493
+ default=None,
494
+ envvar="UNIQUE_MESSAGE_ID",
495
+ help="Parent message ID for message correlation.",
496
+ )
497
+ @click.option(
498
+ "--assistant-id",
499
+ "parent_assistant_id",
500
+ default=None,
501
+ envvar="UNIQUE_ASSISTANT_ID",
502
+ help="Parent assistant ID for message correlation.",
503
+ )
504
+ @click.option(
505
+ "--reset-chat",
506
+ is_flag=True,
507
+ help="Ignore any saved reusable chat for this subagent call.",
508
+ )
509
+ @click.option(
510
+ "--json",
511
+ "output_json",
512
+ is_flag=True,
513
+ help="Print the raw response JSON instead of a human-readable response.",
514
+ )
515
+ @click.pass_context
516
+ def subagent(
517
+ ctx: click.Context,
518
+ tool_name: str,
519
+ message: str,
520
+ config_path: str | None,
521
+ parent_chat_id: str | None,
522
+ parent_message_id: str | None,
523
+ parent_assistant_id: str | None,
524
+ reset_chat: bool,
525
+ output_json: bool,
526
+ ) -> None:
527
+ """Invoke a configured connected-space subagent.
528
+
529
+ \b
530
+ TOOL_NAME must match an entry in .unique-subagents.json. The command sends
531
+ MESSAGE to that connected assistant and waits for the assistant response.
532
+
533
+ \b
534
+ Examples:
535
+ unique-cli subagent LegalReview "Review this contract clause"
536
+ unique-cli subagent Finance "Summarize Q4 revenue" --reset-chat
537
+ """
538
+ output = cmd_subagent(
539
+ LazyState.get(ctx),
540
+ tool_name=tool_name,
541
+ message=message,
542
+ config_path=config_path,
543
+ parent_chat_id=parent_chat_id,
544
+ parent_message_id=parent_message_id,
545
+ parent_assistant_id=parent_assistant_id,
546
+ reset_chat=reset_chat,
547
+ output_json=output_json,
548
+ )
549
+ click.echo(output)
550
+ if _is_subagent_error_output(output):
551
+ ctx.exit(1)
552
+
553
+
468
554
  # -- Scheduled Tasks -------------------------------------------------------
469
555
 
470
556
 
@@ -0,0 +1,298 @@
1
+ """Subagent command: invoke configured connected spaces via the Unique platform."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Any, Literal, TypedDict, cast
10
+
11
+ from unique_sdk._error import APIError
12
+ from unique_sdk.api_resources._space import Space
13
+ from unique_sdk.cli.state import ShellState
14
+ from unique_sdk.utils.chat_in_space import send_message_and_wait_for_completion
15
+
16
+ CONFIG_FILENAME = ".unique-subagents.json"
17
+ STATE_FILENAME = ".unique-subagent-chats.json"
18
+ ENV_CONFIG_PATH = "UNIQUE_SUBAGENTS_CONFIG"
19
+ SUBAGENT_ERROR_PREFIX = "subagent:"
20
+
21
+
22
+ class SubagentDefinition(TypedDict, total=False):
23
+ name: str
24
+ displayName: str
25
+ configuration: dict[str, Any]
26
+ assistantId: str
27
+ chatId: str | None
28
+ reuseChat: bool
29
+ forcedTools: list[str]
30
+ pollInterval: float
31
+ maxWait: float
32
+ stopCondition: Literal["stoppedStreamingAt", "completedAt"]
33
+
34
+
35
+ def is_error_output(output: str) -> bool:
36
+ """Return ``True`` when ``output`` is a CLI error message."""
37
+ return output.startswith(SUBAGENT_ERROR_PREFIX)
38
+
39
+
40
+ def resolve_config_path(config_path: str | None = None) -> Path:
41
+ """Resolve the subagent config file path."""
42
+ if config_path:
43
+ return Path(config_path).expanduser()
44
+ env_path = os.environ.get(ENV_CONFIG_PATH)
45
+ if env_path:
46
+ return Path(env_path).expanduser()
47
+ return Path.cwd() / CONFIG_FILENAME
48
+
49
+
50
+ def _load_config(config_path: Path) -> list[SubagentDefinition]:
51
+ try:
52
+ raw = json.loads(config_path.read_text(encoding="utf-8"))
53
+ except FileNotFoundError as exc:
54
+ raise ValueError(f"config file not found: {config_path}") from exc
55
+ except json.JSONDecodeError as exc:
56
+ raise ValueError(f"config file is not valid JSON: {exc}") from exc
57
+
58
+ if not isinstance(raw, dict):
59
+ raise ValueError("config root must be a JSON object")
60
+
61
+ subagents = raw.get("subagents")
62
+ if not isinstance(subagents, list):
63
+ raise ValueError('config must contain a "subagents" array')
64
+
65
+ validated: list[SubagentDefinition] = []
66
+ for item in subagents:
67
+ if isinstance(item, dict):
68
+ validated.append(cast(SubagentDefinition, cast(object, item)))
69
+ return validated
70
+
71
+
72
+ def _find_subagent(
73
+ subagents: list[SubagentDefinition],
74
+ tool_name: str,
75
+ ) -> SubagentDefinition:
76
+ for subagent in subagents:
77
+ if subagent.get("name") == tool_name:
78
+ return subagent
79
+ available = sorted(name for subagent in subagents if (name := subagent.get("name")))
80
+ suffix = f" Available: {', '.join(available)}." if available else ""
81
+ raise ValueError(f"unknown subagent tool {tool_name!r}.{suffix}")
82
+
83
+
84
+ def _get_str(
85
+ subagent: SubagentDefinition,
86
+ *keys: str,
87
+ required: bool = False,
88
+ ) -> str | None:
89
+ value = _get_value(subagent, *keys)
90
+ if isinstance(value, str) and value.strip():
91
+ return value
92
+ if required:
93
+ key_list = " / ".join(keys)
94
+ raise ValueError(f"subagent {subagent.get('name')!r} is missing {key_list}")
95
+ return None
96
+
97
+
98
+ def _get_value(subagent: SubagentDefinition, *keys: str) -> Any:
99
+ subagent_data = cast(dict[str, Any], cast(object, subagent))
100
+ for key in keys:
101
+ if key in subagent_data:
102
+ return subagent_data[key]
103
+ configuration = subagent.get("configuration")
104
+ if isinstance(configuration, dict):
105
+ for key in keys:
106
+ if key in configuration:
107
+ return configuration[key]
108
+ return None
109
+
110
+
111
+ def _get_bool(
112
+ subagent: SubagentDefinition,
113
+ *keys: str,
114
+ default: bool,
115
+ ) -> bool:
116
+ value = _get_value(subagent, *keys)
117
+ return value if isinstance(value, bool) else default
118
+
119
+
120
+ def _get_float(
121
+ subagent: SubagentDefinition,
122
+ *keys: str,
123
+ default: float,
124
+ ) -> float:
125
+ value = _get_value(subagent, *keys)
126
+ if isinstance(value, int | float) and not isinstance(value, bool):
127
+ return float(value)
128
+ return default
129
+
130
+
131
+ def _get_forced_tools(subagent: SubagentDefinition) -> list[str] | None:
132
+ value = _get_value(subagent, "forcedTools", "forced_tools")
133
+ if isinstance(value, list) and all(isinstance(item, str) for item in value):
134
+ return value or None
135
+ return None
136
+
137
+
138
+ def _get_stop_condition(
139
+ subagent: SubagentDefinition,
140
+ ) -> Literal["stoppedStreamingAt", "completedAt"]:
141
+ value = _get_value(subagent, "stopCondition", "stop_condition")
142
+ if value in ("stoppedStreamingAt", "completedAt"):
143
+ return value
144
+ return "completedAt"
145
+
146
+
147
+ def _load_chat_state(state_path: Path) -> dict[str, str]:
148
+ if not state_path.is_file():
149
+ return {}
150
+ try:
151
+ raw = json.loads(state_path.read_text(encoding="utf-8"))
152
+ except (OSError, json.JSONDecodeError):
153
+ return {}
154
+ if not isinstance(raw, dict):
155
+ return {}
156
+ return {
157
+ key: value
158
+ for key, value in raw.items()
159
+ if isinstance(key, str) and isinstance(value, str)
160
+ }
161
+
162
+
163
+ def _save_chat_state(state_path: Path, state: dict[str, str]) -> None:
164
+ state_path.write_text(json.dumps(state, indent=2), encoding="utf-8")
165
+
166
+
167
+ def _format_response(
168
+ *,
169
+ tool_name: str,
170
+ display_name: str,
171
+ response: Space.Message,
172
+ output_json: bool,
173
+ ) -> str:
174
+ if output_json:
175
+ return json.dumps(response, indent=2, default=str)
176
+
177
+ text = response.get("text")
178
+ if text is None:
179
+ raise ValueError(f"subagent {tool_name!r} returned no text")
180
+
181
+ lines = [f"Subagent: {display_name} ({tool_name})"]
182
+ chat_id = response.get("chatId")
183
+ if chat_id:
184
+ lines.append(f"Chat: {chat_id}")
185
+ lines.append("")
186
+ lines.append(text)
187
+ return "\n".join(lines)
188
+
189
+
190
+ async def _send_to_subagent(
191
+ *,
192
+ state: ShellState,
193
+ subagent: SubagentDefinition,
194
+ message: str,
195
+ chat_id: str | None,
196
+ parent_chat_id: str | None,
197
+ parent_message_id: str | None,
198
+ parent_assistant_id: str | None,
199
+ ) -> Space.Message:
200
+ assistant_id = _get_str(
201
+ subagent,
202
+ "assistantId",
203
+ "assistant_id",
204
+ required=True,
205
+ )
206
+ assert assistant_id is not None
207
+
208
+ correlation: Space.Correlation | None = None
209
+ if parent_chat_id and parent_message_id and parent_assistant_id:
210
+ correlation = {
211
+ "parentMessageId": parent_message_id,
212
+ "parentChatId": parent_chat_id,
213
+ "parentAssistantId": parent_assistant_id,
214
+ }
215
+
216
+ return await send_message_and_wait_for_completion(
217
+ user_id=state.config.user_id,
218
+ company_id=state.config.company_id,
219
+ assistant_id=assistant_id,
220
+ text=message,
221
+ tool_choices=_get_forced_tools(subagent),
222
+ chat_id=chat_id,
223
+ poll_interval=_get_float(
224
+ subagent,
225
+ "pollInterval",
226
+ "poll_interval",
227
+ default=1.0,
228
+ ),
229
+ max_wait=_get_float(
230
+ subagent,
231
+ "maxWait",
232
+ "max_wait",
233
+ default=120.0,
234
+ ),
235
+ stop_condition=_get_stop_condition(subagent),
236
+ correlation=correlation,
237
+ )
238
+
239
+
240
+ def cmd_subagent(
241
+ state: ShellState,
242
+ tool_name: str,
243
+ message: str,
244
+ *,
245
+ config_path: str | None = None,
246
+ parent_chat_id: str | None = None,
247
+ parent_message_id: str | None = None,
248
+ parent_assistant_id: str | None = None,
249
+ reset_chat: bool = False,
250
+ output_json: bool = False,
251
+ ) -> str:
252
+ """Invoke one configured connected-space subagent."""
253
+ try:
254
+ resolved_config_path = resolve_config_path(config_path)
255
+ subagent = _find_subagent(
256
+ _load_config(resolved_config_path),
257
+ tool_name=tool_name,
258
+ )
259
+ display_name = _get_str(subagent, "displayName") or tool_name
260
+ configured_chat_id = _get_str(subagent, "chatId", "chat_id")
261
+ reuse_chat = _get_bool(subagent, "reuseChat", "reuse_chat", default=True)
262
+
263
+ state_path = resolved_config_path.parent / STATE_FILENAME
264
+ chat_id = configured_chat_id
265
+ chat_state: dict[str, str] = {}
266
+ if chat_id is None and reuse_chat and not reset_chat:
267
+ chat_state = _load_chat_state(state_path)
268
+ chat_id = chat_state.get(tool_name)
269
+
270
+ response = asyncio.run(
271
+ _send_to_subagent(
272
+ state=state,
273
+ subagent=subagent,
274
+ message=message,
275
+ chat_id=chat_id,
276
+ parent_chat_id=parent_chat_id,
277
+ parent_message_id=parent_message_id,
278
+ parent_assistant_id=parent_assistant_id,
279
+ )
280
+ )
281
+
282
+ response_chat_id = response.get("chatId")
283
+ if configured_chat_id is None and reuse_chat and response_chat_id:
284
+ chat_state = chat_state or _load_chat_state(state_path)
285
+ chat_state[tool_name] = response_chat_id
286
+ try:
287
+ _save_chat_state(state_path, chat_state)
288
+ except OSError:
289
+ pass
290
+
291
+ return _format_response(
292
+ tool_name=tool_name,
293
+ display_name=display_name,
294
+ response=response,
295
+ output_json=output_json,
296
+ )
297
+ except (ValueError, OSError, TimeoutError, APIError) as exc:
298
+ return f"{SUBAGENT_ERROR_PREFIX} {exc}"
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: unique-cli-subagent
3
+ description: >-
4
+ Invoke connected Unique spaces/subagents through the unique-cli subagent
5
+ command. Use when the workspace exposes connected-space tools and you need
6
+ to delegate a question or task to one of those configured assistants.
7
+ ---
8
+
9
+ # Unique CLI -- Connected Spaces / Subagents
10
+
11
+ Use this skill to call a connected Unique space as a tool. Each connected
12
+ space is configured by the platform in `.unique-subagents.json`; call it by
13
+ the tool name shown in the generated connected-space skill.
14
+
15
+ ## Usage
16
+
17
+ ```bash
18
+ unique-cli subagent "<tool_name>" "<message>" \
19
+ --chat-id "$UNIQUE_CHAT_ID" \
20
+ --message-id "$UNIQUE_MESSAGE_ID" \
21
+ --assistant-id "$UNIQUE_ASSISTANT_ID"
22
+ ```
23
+
24
+ ## Rules
25
+
26
+ 1. Use the exact tool name from the connected-space skill or from
27
+ `.unique-subagents.json`.
28
+ 2. Send a focused message that contains the subagent-specific task and the
29
+ relevant context.
30
+ 3. Treat the returned text as the connected space's answer. Do not invent
31
+ details that are not in the response.
32
+ 4. If the connected space has references in its text, preserve them exactly.
33
+
34
+ ## Options
35
+
36
+ | Option | Default | Description |
37
+ |--------|---------|-------------|
38
+ | `<tool_name>` | required | Name of the configured connected-space tool. |
39
+ | `<message>` | required | Prompt sent to the connected space. |
40
+ | `--reset-chat` | off | Start from a fresh subagent chat instead of reusing the saved chat. |
41
+ | `--json` | off | Print the raw response JSON. |
42
+ | `--config` | `.unique-subagents.json` | Override the config path. |
43
+
44
+ ## Prerequisites
45
+
46
+ The platform sets these environment variables automatically:
47
+
48
+ ```bash
49
+ UNIQUE_USER_ID
50
+ UNIQUE_COMPANY_ID
51
+ UNIQUE_CHAT_ID
52
+ UNIQUE_MESSAGE_ID
53
+ UNIQUE_ASSISTANT_ID
54
+ UNIQUE_API_KEY
55
+ UNIQUE_APP_ID
56
+ ```