talkops-opscloud 0.3.0b1__py3-none-any.whl

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 (309) hide show
  1. opscloud/__init__.py +5 -0
  2. opscloud/__main__.py +6 -0
  3. opscloud/_constants.py +53 -0
  4. opscloud/_debug.py +145 -0
  5. opscloud/_debug_buffer.py +250 -0
  6. opscloud/_textual_patches.py +367 -0
  7. opscloud/_version.py +15 -0
  8. opscloud/agent/__init__.py +27 -0
  9. opscloud/agent/config.py +168 -0
  10. opscloud/agent/factory.py +1103 -0
  11. opscloud/approval_mode.py +44 -0
  12. opscloud/backend/__init__.py +49 -0
  13. opscloud/backend/composite.py +89 -0
  14. opscloud/backend/local.py +183 -0
  15. opscloud/backend/registry.py +79 -0
  16. opscloud/backend/sandbox_config.py +107 -0
  17. opscloud/backend/sandbox_factory.py +432 -0
  18. opscloud/built_in_skills/remember/SKILL.md +124 -0
  19. opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
  20. opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
  21. opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
  22. opscloud/cli/__init__.py +1 -0
  23. opscloud/cli/commands/__init__.py +15 -0
  24. opscloud/cli/commands/auth.py +45 -0
  25. opscloud/cli/commands/doctor.py +100 -0
  26. opscloud/cli/commands/mcp.py +137 -0
  27. opscloud/cli/commands/threads.py +51 -0
  28. opscloud/cli/interactive.py +154 -0
  29. opscloud/cli/main.py +903 -0
  30. opscloud/cli/non_interactive.py +425 -0
  31. opscloud/cli/server_manager.py +58 -0
  32. opscloud/commands/__init__.py +19 -0
  33. opscloud/commands/_base.py +110 -0
  34. opscloud/commands/_guards.py +52 -0
  35. opscloud/commands/_router.py +146 -0
  36. opscloud/commands/_types.py +36 -0
  37. opscloud/commands/core/__init__.py +47 -0
  38. opscloud/commands/core/auth.py +94 -0
  39. opscloud/commands/core/bug.py +46 -0
  40. opscloud/commands/core/clear.py +106 -0
  41. opscloud/commands/core/cloud.py +185 -0
  42. opscloud/commands/core/compact.py +128 -0
  43. opscloud/commands/core/config_cmd.py +120 -0
  44. opscloud/commands/core/context.py +220 -0
  45. opscloud/commands/core/cost.py +44 -0
  46. opscloud/commands/core/doctor.py +535 -0
  47. opscloud/commands/core/effort.py +108 -0
  48. opscloud/commands/core/exit_cmd.py +41 -0
  49. opscloud/commands/core/fast.py +148 -0
  50. opscloud/commands/core/help_cmd.py +94 -0
  51. opscloud/commands/core/mcp.py +134 -0
  52. opscloud/commands/core/model.py +109 -0
  53. opscloud/commands/core/permissions.py +196 -0
  54. opscloud/commands/core/plugins.py +92 -0
  55. opscloud/commands/core/pool.py +108 -0
  56. opscloud/commands/core/resume.py +97 -0
  57. opscloud/commands/core/skills.py +178 -0
  58. opscloud/commands/power/__init__.py +51 -0
  59. opscloud/commands/power/agents.py +98 -0
  60. opscloud/commands/power/btw.py +85 -0
  61. opscloud/commands/power/copy.py +97 -0
  62. opscloud/commands/power/goal.py +548 -0
  63. opscloud/commands/power/loop.py +295 -0
  64. opscloud/commands/power/memory.py +287 -0
  65. opscloud/commands/power/review.py +152 -0
  66. opscloud/commands/power/rubric.py +287 -0
  67. opscloud/commands/power/runtime.py +321 -0
  68. opscloud/commands/power/skill_creator.py +70 -0
  69. opscloud/commands/power/skill_invoke.py +165 -0
  70. opscloud/commands/power/tasks.py +209 -0
  71. opscloud/commands/power/trace.py +117 -0
  72. opscloud/commands/power/ui_toggles.py +135 -0
  73. opscloud/commands/power/version.py +52 -0
  74. opscloud/commands/registry.py +176 -0
  75. opscloud/config/__init__.py +114 -0
  76. opscloud/config/adapters/__init__.py +5 -0
  77. opscloud/config/adapters/postgres.py +776 -0
  78. opscloud/config/adapters/sqlite.py +769 -0
  79. opscloud/config/aws.py +248 -0
  80. opscloud/config/cloud_profiles.py +437 -0
  81. opscloud/config/env_vars.py +81 -0
  82. opscloud/config/langsmith.py +319 -0
  83. opscloud/config/manifest.py +817 -0
  84. opscloud/config/metadata.py +205 -0
  85. opscloud/config/paths.py +604 -0
  86. opscloud/config/plugins.py +130 -0
  87. opscloud/config/settings.py +834 -0
  88. opscloud/config/store.py +453 -0
  89. opscloud/config/store_factory.py +97 -0
  90. opscloud/config/toml_config.py +562 -0
  91. opscloud/editor.py +173 -0
  92. opscloud/exceptions.py +65 -0
  93. opscloud/file_ops.py +217 -0
  94. opscloud/hooks/__init__.py +139 -0
  95. opscloud/hooks/env.py +44 -0
  96. opscloud/hooks/interrupt.py +121 -0
  97. opscloud/hooks/models/__init__.py +109 -0
  98. opscloud/hooks/models/adapters.py +19 -0
  99. opscloud/hooks/models/config.py +79 -0
  100. opscloud/hooks/models/domain.py +434 -0
  101. opscloud/hooks/models/transport.py +37 -0
  102. opscloud/hooks/tools.py +231 -0
  103. opscloud/input.py +48 -0
  104. opscloud/integrations/__init__.py +80 -0
  105. opscloud/integrations/base.py +153 -0
  106. opscloud/integrations/content_filter.py +119 -0
  107. opscloud/integrations/event_bus.py +480 -0
  108. opscloud/integrations/hooks.py +238 -0
  109. opscloud/integrations/identity.py +181 -0
  110. opscloud/integrations/notifications.py +134 -0
  111. opscloud/integrations/stream_bridge.py +397 -0
  112. opscloud/integrations/thread_store.py +81 -0
  113. opscloud/json_types.py +20 -0
  114. opscloud/mcp/__init__.py +57 -0
  115. opscloud/mcp/config.py +203 -0
  116. opscloud/mcp/discovery.py +229 -0
  117. opscloud/mcp/mcp_info.py +109 -0
  118. opscloud/mcp/middleware.py +102 -0
  119. opscloud/mcp/preload.py +505 -0
  120. opscloud/mcp/raw_config.py +149 -0
  121. opscloud/mcp/semantic_profiler.py +305 -0
  122. opscloud/mcp/session_manager.py +727 -0
  123. opscloud/mcp/trust.py +105 -0
  124. opscloud/memory/__init__.py +27 -0
  125. opscloud/memory/branch.py +61 -0
  126. opscloud/memory/guard.py +19 -0
  127. opscloud/memory/onboarding.py +120 -0
  128. opscloud/memory/registry.py +134 -0
  129. opscloud/memory/store.py +223 -0
  130. opscloud/middleware/__init__.py +106 -0
  131. opscloud/middleware/_repository_bounds.py +459 -0
  132. opscloud/middleware/ask_user.py +152 -0
  133. opscloud/middleware/auto_mode.py +60 -0
  134. opscloud/middleware/auto_mode_hitl.py +2434 -0
  135. opscloud/middleware/compaction.py +566 -0
  136. opscloud/middleware/configurable_model.py +233 -0
  137. opscloud/middleware/cost_tracking.py +1021 -0
  138. opscloud/middleware/glm_stall_recovery.py +109 -0
  139. opscloud/middleware/goal_criteria.py +1234 -0
  140. opscloud/middleware/goal_state_notice.py +712 -0
  141. opscloud/middleware/goal_tools.py +407 -0
  142. opscloud/middleware/headless_mcp_guard.py +88 -0
  143. opscloud/middleware/jev_model_router.py +1018 -0
  144. opscloud/middleware/local_context.py +460 -0
  145. opscloud/middleware/mcp_context.py +151 -0
  146. opscloud/middleware/mcp_middleware.py +146 -0
  147. opscloud/middleware/memory_guard.py +393 -0
  148. opscloud/middleware/model_retry.py +205 -0
  149. opscloud/middleware/registry.py +137 -0
  150. opscloud/middleware/reliable_rubric.py +709 -0
  151. opscloud/middleware/resume_state.py +91 -0
  152. opscloud/middleware/server_hooks.py +1189 -0
  153. opscloud/middleware/shell_allow_list.py +90 -0
  154. opscloud/middleware/skills.py +382 -0
  155. opscloud/middleware/subagent_artifacts.py +84 -0
  156. opscloud/middleware/subagent_telemetry.py +261 -0
  157. opscloud/middleware/subagents.py +243 -0
  158. opscloud/middleware/tool_filter.py +384 -0
  159. opscloud/middleware/unified_system_message.py +62 -0
  160. opscloud/model/__init__.py +37 -0
  161. opscloud/model/config.py +2992 -0
  162. opscloud/model/factory.py +477 -0
  163. opscloud/model/pool.py +459 -0
  164. opscloud/model/reasoning.py +282 -0
  165. opscloud/offload.py +173 -0
  166. opscloud/output.py +64 -0
  167. opscloud/plugins/__init__.py +189 -0
  168. opscloud/plugins/_json.py +22 -0
  169. opscloud/plugins/adapters/__init__.py +13 -0
  170. opscloud/plugins/adapters/agents.py +112 -0
  171. opscloud/plugins/adapters/commands.py +190 -0
  172. opscloud/plugins/adapters/mcp.py +363 -0
  173. opscloud/plugins/adapters/skills.py +132 -0
  174. opscloud/plugins/commands_cli.py +541 -0
  175. opscloud/plugins/discovery.py +512 -0
  176. opscloud/plugins/manifest.py +335 -0
  177. opscloud/plugins/marketplace.py +893 -0
  178. opscloud/plugins/models.py +334 -0
  179. opscloud/plugins/project_plugins.py +317 -0
  180. opscloud/plugins/protocol.py +31 -0
  181. opscloud/plugins/store.py +905 -0
  182. opscloud/plugins/substitution.py +122 -0
  183. opscloud/project_utils.py +145 -0
  184. opscloud/prompts/__init__.py +37 -0
  185. opscloud/prompts/system.py +483 -0
  186. opscloud/prompts/templates/system_prompt.md +166 -0
  187. opscloud/prompts/types.py +54 -0
  188. opscloud/rubrics/__init__.py +42 -0
  189. opscloud/rubrics/evaluator.py +404 -0
  190. opscloud/rubrics/evidence_extractor.py +229 -0
  191. opscloud/rubrics/generator.py +239 -0
  192. opscloud/rubrics/jev_compiler.py +175 -0
  193. opscloud/rubrics/jev_grader.py +297 -0
  194. opscloud/rubrics/middleware.py +8 -0
  195. opscloud/schema/__init__.py +107 -0
  196. opscloud/schema/interrupts.py +981 -0
  197. opscloud/security/__init__.py +91 -0
  198. opscloud/security/approval_mode.py +528 -0
  199. opscloud/security/approval_mode_source.py +230 -0
  200. opscloud/security/cli_ast_evaluator.py +552 -0
  201. opscloud/security/jev_classifier.py +651 -0
  202. opscloud/security/shell_safety.py +340 -0
  203. opscloud/security/unicode_security.py +439 -0
  204. opscloud/security/url_validation.py +160 -0
  205. opscloud/server/__init__.py +13 -0
  206. opscloud/server/_server_config.py +158 -0
  207. opscloud/server/server.py +242 -0
  208. opscloud/server/server_graph.py +93 -0
  209. opscloud/skills/__init__.py +91 -0
  210. opscloud/skills/commands.py +1099 -0
  211. opscloud/skills/invocation.py +200 -0
  212. opscloud/skills/load.py +388 -0
  213. opscloud/skills/loader.py +23 -0
  214. opscloud/skills/merge.py +65 -0
  215. opscloud/skills/registry.py +263 -0
  216. opscloud/skills/sources.py +174 -0
  217. opscloud/skills/trust.py +417 -0
  218. opscloud/state/__init__.py +142 -0
  219. opscloud/state/base.py +154 -0
  220. opscloud/state/cloud_context.py +262 -0
  221. opscloud/state/goal_channels.py +124 -0
  222. opscloud/state/goal_state_limits.py +175 -0
  223. opscloud/state/resume_state.py +58 -0
  224. opscloud/state/service_context.py +281 -0
  225. opscloud/state/session.py +957 -0
  226. opscloud/state/state_migration.py +86 -0
  227. opscloud/subagents/__init__.py +30 -0
  228. opscloud/subagents/loader.py +331 -0
  229. opscloud/subagents/subagents_parser.py +117 -0
  230. opscloud/subagents/types.py +25 -0
  231. opscloud/tools/__init__.py +36 -0
  232. opscloud/tools/catalog.py +37 -0
  233. opscloud/tools/display.py +24 -0
  234. opscloud/tools/fetch_url.py +176 -0
  235. opscloud/tools/goal_tools.py +423 -0
  236. opscloud/tools/registry.py +105 -0
  237. opscloud/tools/thread.py +32 -0
  238. opscloud/tools/web_search.py +215 -0
  239. opscloud/ui/__init__.py +119 -0
  240. opscloud/ui/app.py +5400 -0
  241. opscloud/ui/app.tcss +479 -0
  242. opscloud/ui/clipboard.py +147 -0
  243. opscloud/ui/command_registry.py +482 -0
  244. opscloud/ui/message_store.py +152 -0
  245. opscloud/ui/permission_store.py +360 -0
  246. opscloud/ui/preferences.py +113 -0
  247. opscloud/ui/prompt_manager.py +96 -0
  248. opscloud/ui/remote_client.py +505 -0
  249. opscloud/ui/textual_adapter.py +1269 -0
  250. opscloud/ui/theme.py +728 -0
  251. opscloud/ui/ui_help.py +415 -0
  252. opscloud/ui/widgets/__init__.py +383 -0
  253. opscloud/ui/widgets/_ask_user_types.py +81 -0
  254. opscloud/ui/widgets/_copy_spans.py +52 -0
  255. opscloud/ui/widgets/_inline_prompt.py +290 -0
  256. opscloud/ui/widgets/_js_eval_display.py +139 -0
  257. opscloud/ui/widgets/_links.py +175 -0
  258. opscloud/ui/widgets/_tool_stream.py +308 -0
  259. opscloud/ui/widgets/agent_selector.py +297 -0
  260. opscloud/ui/widgets/approval.py +741 -0
  261. opscloud/ui/widgets/ask_user.py +539 -0
  262. opscloud/ui/widgets/auth.py +695 -0
  263. opscloud/ui/widgets/auth_manager.py +214 -0
  264. opscloud/ui/widgets/auto_mode_notice.py +146 -0
  265. opscloud/ui/widgets/autocomplete.py +709 -0
  266. opscloud/ui/widgets/chat_input.py +805 -0
  267. opscloud/ui/widgets/cloud_selector.py +528 -0
  268. opscloud/ui/widgets/config_manager.py +652 -0
  269. opscloud/ui/widgets/debug_console.py +1269 -0
  270. opscloud/ui/widgets/devops_renderers.py +184 -0
  271. opscloud/ui/widgets/diff.py +237 -0
  272. opscloud/ui/widgets/effort_selector.py +166 -0
  273. opscloud/ui/widgets/goal_review.py +612 -0
  274. opscloud/ui/widgets/goal_status.py +57 -0
  275. opscloud/ui/widgets/infra_panel.py +141 -0
  276. opscloud/ui/widgets/install_confirm.py +114 -0
  277. opscloud/ui/widgets/loading.py +170 -0
  278. opscloud/ui/widgets/mcp_viewer.py +1204 -0
  279. opscloud/ui/widgets/messages.py +4461 -0
  280. opscloud/ui/widgets/model_selector.py +863 -0
  281. opscloud/ui/widgets/notification_center.py +108 -0
  282. opscloud/ui/widgets/notification_settings.py +119 -0
  283. opscloud/ui/widgets/operation_card.py +106 -0
  284. opscloud/ui/widgets/permissions_manager.py +667 -0
  285. opscloud/ui/widgets/plugin_manager.py +1212 -0
  286. opscloud/ui/widgets/pool_selector.py +376 -0
  287. opscloud/ui/widgets/preamble.py +53 -0
  288. opscloud/ui/widgets/skills_viewer.py +432 -0
  289. opscloud/ui/widgets/status.py +1037 -0
  290. opscloud/ui/widgets/subagent_panel.py +927 -0
  291. opscloud/ui/widgets/theme_selector.py +232 -0
  292. opscloud/ui/widgets/thread_selector.py +358 -0
  293. opscloud/ui/widgets/toast.py +31 -0
  294. opscloud/ui/widgets/tool_display.py +325 -0
  295. opscloud/ui/widgets/tool_renderers.py +234 -0
  296. opscloud/ui/widgets/tool_widgets.py +278 -0
  297. opscloud/ui/widgets/welcome.py +421 -0
  298. opscloud/ui/widgets/welcome_popup.py +102 -0
  299. opscloud/utils/__init__.py +24 -0
  300. opscloud/utils/cost_estimation.py +169 -0
  301. opscloud/utils/git.py +390 -0
  302. opscloud/utils/logger.py +571 -0
  303. opscloud/utils/session_stats.py +115 -0
  304. opscloud/utils/startup_error.py +37 -0
  305. talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
  306. talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
  307. talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
  308. talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
  309. talkops_opscloud-0.3.0b1.dist-info/licenses/LICENSE +175 -0
@@ -0,0 +1,282 @@
1
+ """Reasoning effort support and provider-native thinking configuration.
2
+
3
+ Translates standard `reasoning_effort` ('low', 'medium', 'high', 'max')
4
+ into provider-native request shapes:
5
+ - Google GenAI (Gemini): `include_thoughts=True`, `thinking_level="HIGH"`, `thinking_budget=8192`
6
+ - Anthropic (Claude): `thinking={"type": "enabled", "budget_tokens": 8192}`
7
+ - OpenAI: `reasoning={"effort": "high"}`
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Mapping
13
+ from typing import Any
14
+
15
+ from opscloud.model.config import (
16
+ MODEL_PROFILES,
17
+ ModelSpec,
18
+ detect_provider,
19
+ get_model_profile as _config_get_model_profile,
20
+ get_model_profiles,
21
+ normalize_model_spec,
22
+ )
23
+ from opscloud.utils.logger import get_logger
24
+
25
+ logger = get_logger(__name__)
26
+
27
+ SUPPORTED_EFFORT_LEVELS = ("none", "off", "minimal", "low", "medium", "high", "xhigh", "max")
28
+
29
+
30
+ def _resolve_provider(model_spec: str | None) -> str:
31
+ """Resolve provider name from model spec string, supporting bare or prefixed formats."""
32
+ if not model_spec:
33
+ return ""
34
+ parsed = ModelSpec.try_parse(model_spec)
35
+ if parsed is not None and parsed.provider:
36
+ return parsed.provider
37
+ return detect_provider(model_spec) or ""
38
+
39
+
40
+ def get_model_profile(spec: str) -> dict[str, Any] | None:
41
+ """Return model profile for spec."""
42
+ if not spec:
43
+ return None
44
+ entry = _config_get_model_profile(spec)
45
+ if entry and entry.get("profile"):
46
+ return dict(entry["profile"])
47
+ return None
48
+
49
+
50
+ def _model_profile(
51
+ model_spec: str | None, *, cli_override: dict[str, Any] | None = None
52
+ ) -> Mapping[str, Any] | None:
53
+ """Return the reasoning-capable profile for `model_spec`."""
54
+ if not model_spec:
55
+ return None
56
+ entry = get_model_profiles(cli_override=cli_override).get(model_spec)
57
+ if entry is None:
58
+ entry = _config_get_model_profile(model_spec)
59
+ profile = cli_override if entry is None else entry.get("profile")
60
+ if profile is None:
61
+ return None
62
+ if not isinstance(profile, Mapping):
63
+ logger.warning(
64
+ "Ignoring model profile for %s with unexpected type %s",
65
+ model_spec,
66
+ type(profile).__name__,
67
+ )
68
+ return None
69
+ reasoning_output = profile.get("reasoning_output")
70
+ if reasoning_output is not None and not isinstance(reasoning_output, bool):
71
+ logger.warning(
72
+ "Ignoring reasoning_output for %s with unexpected type %s",
73
+ model_spec,
74
+ type(reasoning_output).__name__,
75
+ )
76
+ return None
77
+ if reasoning_output is not True:
78
+ return None
79
+ return profile
80
+
81
+
82
+ def supported_efforts_for_model(
83
+ model_spec: str | None, *, cli_override: dict[str, Any] | None = None
84
+ ) -> tuple[str, ...]:
85
+ """Return the ordered reasoning effort levels supported by `model_spec`."""
86
+ profile = _model_profile(model_spec, cli_override=cli_override)
87
+ if profile is None or "reasoning_effort_levels" not in profile:
88
+ return ()
89
+ levels = profile["reasoning_effort_levels"]
90
+ if not isinstance(levels, list):
91
+ return ()
92
+ for level in levels:
93
+ if not isinstance(level, str):
94
+ return ()
95
+ return tuple(levels)
96
+
97
+
98
+ def default_effort_for_model(
99
+ model_spec: str | None, *, cli_override: dict[str, Any] | None = None
100
+ ) -> str | None:
101
+ """Return the profile's reasoning effort default independently of its levels."""
102
+ profile = _model_profile(model_spec, cli_override=cli_override)
103
+ if profile is None or "reasoning_effort_default" not in profile:
104
+ return None
105
+ default = profile.get("reasoning_effort_default")
106
+ if default is not None:
107
+ return str(default)
108
+ return None
109
+
110
+
111
+ def is_effort_supported_for_model(
112
+ model_spec: str, effort: str, *, cli_override: dict[str, Any] | None = None
113
+ ) -> bool:
114
+ """Check whether a given reasoning effort level is supported by the specified model."""
115
+ if not model_spec:
116
+ return False
117
+ supported = supported_efforts_for_model(model_spec, cli_override=cli_override)
118
+ return any(s.casefold() == effort.casefold() for s in supported)
119
+
120
+
121
+ def _str_or_none(value: object) -> str | None:
122
+ if value is None:
123
+ return None
124
+ if isinstance(value, str):
125
+ return value
126
+ return None
127
+
128
+
129
+ def _effort_paths(provider: str) -> tuple[tuple[str, ...], ...]:
130
+ if provider in ("openai", "openai_codex", "azure_openai"):
131
+ return (("reasoning", "effort"), ("reasoning_effort",))
132
+ if provider == "anthropic":
133
+ return (("effort",), ("reasoning_effort",), ("output_config", "effort"), ("thinking",))
134
+ if provider in ("google_genai", "google", "google_vertexai"):
135
+ return (("thinking_level",), ("reasoning_effort",), ("thinking_config", "thinking_level"), ("include_thoughts",))
136
+ return (("reasoning_effort",),)
137
+
138
+
139
+ def _path_is_present(model_params: Mapping[str, Any], path: tuple[str, ...]) -> bool:
140
+ if len(path) == 1:
141
+ return path[0] in model_params
142
+ nested = model_params.get(path[0])
143
+ return isinstance(nested, Mapping) and path[1] in nested
144
+
145
+
146
+ def has_explicit_effort_model_params(model_spec: str | None, model_params: dict[str, Any] | None) -> bool:
147
+ """Return whether canonical or native effort parameters are present."""
148
+ if not model_spec or not model_params:
149
+ return False
150
+ provider = _resolve_provider(model_spec)
151
+ return any(_path_is_present(model_params, path) for path in _effort_paths(provider))
152
+
153
+
154
+ def current_effort_from_model_params(model_spec: str | None, model_params: dict[str, Any] | None) -> str | None:
155
+ """Read canonical or native effort settings using integration precedence."""
156
+ if not model_spec or not model_params:
157
+ return None
158
+ provider = _resolve_provider(model_spec)
159
+
160
+ if provider in {"openai", "openai_codex", "azure_openai"}:
161
+ reasoning = model_params.get("reasoning")
162
+ if isinstance(reasoning, Mapping) and "effort" in reasoning:
163
+ return _str_or_none(reasoning["effort"])
164
+ elif provider == "anthropic":
165
+ if "effort" in model_params and model_params["effort"] is not None:
166
+ return _str_or_none(model_params["effort"])
167
+ output_config = model_params.get("output_config")
168
+ if isinstance(output_config, Mapping) and "effort" in output_config:
169
+ return _str_or_none(output_config["effort"])
170
+ thinking = model_params.get("thinking")
171
+ if isinstance(thinking, Mapping):
172
+ budget = thinking.get("budget_tokens")
173
+ if budget:
174
+ budget_map = {1024: "low", 2048: "low", 4096: "medium", 8192: "high", 16384: "max"}
175
+ if budget in budget_map:
176
+ return budget_map[budget]
177
+ elif provider in {"google_genai", "google", "google_vertexai"}:
178
+ if model_params.get("thinking_budget") == 0 or model_params.get("include_thoughts") is False:
179
+ return "off"
180
+ if "reasoning_effort" in model_params and model_params["reasoning_effort"] is not None:
181
+ return _str_or_none(model_params["reasoning_effort"])
182
+ if "thinking_level" in model_params and model_params["thinking_level"] is not None:
183
+ return _str_or_none(model_params["thinking_level"])
184
+ thinking_config = model_params.get("thinking_config")
185
+ if isinstance(thinking_config, Mapping) and "thinking_level" in thinking_config:
186
+ return _str_or_none(thinking_config["thinking_level"])
187
+ elif provider == "fireworks":
188
+ mk = model_params.get("model_kwargs")
189
+ if isinstance(mk, Mapping) and "reasoning_effort" in mk:
190
+ return _str_or_none(mk["reasoning_effort"])
191
+ elif provider == "xai":
192
+ eb = model_params.get("extra_body")
193
+ if isinstance(eb, Mapping) and "reasoning_effort" in eb:
194
+ return _str_or_none(eb["reasoning_effort"])
195
+
196
+ return _str_or_none(model_params.get("reasoning_effort"))
197
+
198
+
199
+ def _remove_nested_key(params: dict[str, Any], container: str, key: str) -> None:
200
+ nested = params.get(container)
201
+ if not isinstance(nested, Mapping):
202
+ return
203
+ remaining = dict(nested)
204
+ remaining.pop(key, None)
205
+ if remaining:
206
+ params[container] = remaining
207
+ else:
208
+ params.pop(container, None)
209
+
210
+
211
+ def without_effort_model_params(model_spec: str | None, existing: dict[str, Any] | None) -> dict[str, Any] | None:
212
+ """Remove canonical and native effort settings without changing siblings."""
213
+ if not existing:
214
+ return None
215
+ cleaned = dict(existing)
216
+ cleaned.pop("reasoning_effort", None)
217
+
218
+ provider = _resolve_provider(model_spec)
219
+ if provider in ("openai", "openai_codex", "azure_openai"):
220
+ _remove_nested_key(cleaned, "reasoning", "effort")
221
+ elif provider == "anthropic":
222
+ cleaned.pop("effort", None)
223
+ _remove_nested_key(cleaned, "output_config", "effort")
224
+ cleaned.pop("thinking", None)
225
+ elif provider in ("google_genai", "google", "google_vertexai"):
226
+ cleaned.pop("thinking_level", None)
227
+ cleaned.pop("thinking_budget", None)
228
+ cleaned.pop("include_thoughts", None)
229
+ _remove_nested_key(cleaned, "thinking_config", "thinking_level")
230
+ _remove_nested_key(cleaned, "thinking_config", "thinking_budget")
231
+ elif provider == "fireworks":
232
+ _remove_nested_key(cleaned, "model_kwargs", "reasoning_effort")
233
+ elif provider == "xai":
234
+ _remove_nested_key(cleaned, "extra_body", "reasoning_effort")
235
+ return cleaned or None
236
+
237
+
238
+ def with_effort_model_params(model_spec: str | None, existing: dict[str, Any] | None, effort: str) -> dict[str, Any]:
239
+ """Replace existing effort settings with the standard flat parameter or provider-native parameter.
240
+
241
+ If effort is off/none/clear/0/reset, or if the model does not support reasoning effort,
242
+ returns a cleaned parameter dictionary with all reasoning and thinking keys stripped.
243
+ """
244
+ updated = without_effort_model_params(model_spec, existing) or {}
245
+ eff_lower = effort.lower()
246
+
247
+ # Disabling / clearing reasoning
248
+ if eff_lower in ("off", "none", "clear", "0", "reset", ""):
249
+ return updated
250
+
251
+ # If model is specified and does not support this effort level, do not inject effort
252
+ if model_spec and not is_effort_supported_for_model(model_spec, effort):
253
+ return updated
254
+
255
+ provider = _resolve_provider(model_spec)
256
+ if provider in ("google_genai", "google", "google_vertexai"):
257
+ valid_level = eff_lower if eff_lower in ("minimal", "low", "medium", "high") else "medium"
258
+ updated["reasoning_effort"] = valid_level
259
+ updated["include_thoughts"] = True
260
+ elif provider == "anthropic":
261
+ updated["reasoning_effort"] = effort
262
+ budget = {
263
+ "low": 1024,
264
+ "medium": 4096,
265
+ "high": 8192,
266
+ "max": 16384,
267
+ }.get(eff_lower, 4096)
268
+ updated["thinking"] = {"type": "enabled", "budget_tokens": budget}
269
+ elif provider in ("openai", "openai_codex", "azure_openai"):
270
+ updated["reasoning_effort"] = effort
271
+ elif provider == "fireworks":
272
+ mk = dict(updated.get("model_kwargs", {}))
273
+ mk["reasoning_effort"] = effort
274
+ updated["model_kwargs"] = mk
275
+ elif provider == "xai":
276
+ eb = dict(updated.get("extra_body", {}))
277
+ eb["reasoning_effort"] = effort
278
+ updated["extra_body"] = eb
279
+ else:
280
+ updated["reasoning_effort"] = effort
281
+
282
+ return updated
opscloud/offload.py ADDED
@@ -0,0 +1,173 @@
1
+ """Storage paths for offloaded conversation history in OpsCode."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import stat
7
+ import tempfile
8
+ from dataclasses import dataclass
9
+ from pathlib import Path, PurePath
10
+
11
+ from opscloud.config.paths import DATA_DIR
12
+ from opscloud.utils.logger import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+
16
+ _FALLBACK_ARTIFACTS_ROOT = "/opscloud-artifacts-fallback"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class _ArtifactsStorage:
21
+ """Agent-visible artifacts root and optional routed large-result directory."""
22
+
23
+ root: str
24
+ large_results_dir: Path | None = None
25
+
26
+
27
+ def _filesystem_tool_path(path: PurePath) -> str:
28
+ """Represent an absolute host path in the filesystem tool path format."""
29
+ normalized = path.as_posix()
30
+ if path.drive and not path.drive.startswith("\\\\"):
31
+ return f"//?/{normalized}"
32
+ return normalized
33
+
34
+
35
+ _EPHEMERAL_OFFLOAD_STORAGE = False
36
+ _UNIQUE_OFFLOAD_FALLBACK_ROOT: Path | None = None
37
+
38
+
39
+ def offload_storage_is_ephemeral() -> bool:
40
+ """Return whether offload history is routed to non-persistent storage."""
41
+ return _EPHEMERAL_OFFLOAD_STORAGE
42
+
43
+
44
+ def _harden_dir(path: Path) -> None:
45
+ """Create `path` if needed and restrict it to the current user."""
46
+ path.mkdir(mode=0o700, parents=True, exist_ok=True)
47
+ info = path.lstat()
48
+ if not stat.S_ISDIR(info.st_mode):
49
+ msg = f"Path is not a directory: {path}"
50
+ raise OSError(msg)
51
+ getuid = getattr(os, "getuid", None)
52
+ if getuid is not None and info.st_uid != getuid():
53
+ msg = f"Directory is owned by another user: {path}"
54
+ raise PermissionError(msg)
55
+ path.chmod(0o700)
56
+
57
+
58
+ def _probe_writable(path: Path) -> None:
59
+ """Confirm `path` accepts new files (catches read-only mounts)."""
60
+ with tempfile.NamedTemporaryFile(dir=path, prefix=".write-test-"):
61
+ pass
62
+
63
+
64
+ def _artifacts_root() -> _ArtifactsStorage:
65
+ """Return storage configuration for offloaded artifacts."""
66
+ getuid = getattr(os, "getuid", None)
67
+ suffix = str(getuid()) if getuid is not None else str(os.getpid())
68
+ temp_root = Path(tempfile.gettempdir())
69
+ root = temp_root / f"opscloud-artifacts-{suffix}"
70
+ try:
71
+ _harden_dir(root)
72
+ _probe_writable(root)
73
+ except (OSError, RuntimeError):
74
+ logger.warning(
75
+ "Predictable per-user artifacts directory is unavailable; routing "
76
+ "large results from a stable virtual prefix to private temporary storage",
77
+ exc_info=True,
78
+ )
79
+ unique = Path(
80
+ tempfile.mkdtemp(prefix=f"opscloud-artifacts-{suffix}-", dir=temp_root)
81
+ )
82
+ _harden_dir(unique)
83
+ _probe_writable(unique)
84
+ return _ArtifactsStorage(
85
+ root=_FALLBACK_ARTIFACTS_ROOT,
86
+ large_results_dir=unique,
87
+ )
88
+ return _ArtifactsStorage(root=_filesystem_tool_path(root))
89
+
90
+
91
+ def _offload_fallback_root() -> Path:
92
+ """Return a writable base directory for offloaded conversation history."""
93
+
94
+ def _prepare_user_dir() -> Path:
95
+ base = DATA_DIR
96
+ base.mkdir(parents=True, exist_ok=True)
97
+ archive_dir = base / "conversation_history"
98
+ _harden_dir(archive_dir)
99
+ _probe_writable(archive_dir)
100
+ return base
101
+
102
+ def _prepare_temp_dir(path: Path) -> Path:
103
+ _harden_dir(path)
104
+ _probe_writable(path)
105
+ return path
106
+
107
+ global _EPHEMERAL_OFFLOAD_STORAGE, _UNIQUE_OFFLOAD_FALLBACK_ROOT # noqa: PLW0603
108
+ if _UNIQUE_OFFLOAD_FALLBACK_ROOT is not None:
109
+ _EPHEMERAL_OFFLOAD_STORAGE = True
110
+ return _UNIQUE_OFFLOAD_FALLBACK_ROOT
111
+ try:
112
+ root = _prepare_user_dir()
113
+ except (RuntimeError, OSError):
114
+ logger.warning(
115
+ "User data directory is not writable; falling back to temporary "
116
+ "offload storage, which may not persist across restarts",
117
+ exc_info=True,
118
+ )
119
+ else:
120
+ _EPHEMERAL_OFFLOAD_STORAGE = False
121
+ return root
122
+
123
+ _EPHEMERAL_OFFLOAD_STORAGE = True
124
+ getuid = getattr(os, "getuid", None)
125
+ suffix = str(getuid()) if getuid is not None else str(os.getpid())
126
+ temp_root = Path(tempfile.gettempdir())
127
+ path = temp_root / f"opscloud-{suffix}"
128
+ try:
129
+ return _prepare_temp_dir(path)
130
+ except (OSError, RuntimeError):
131
+ logger.warning(
132
+ "Per-user temporary offload directory is unavailable; creating "
133
+ "a private unique directory",
134
+ exc_info=True,
135
+ )
136
+ unique = Path(tempfile.mkdtemp(prefix=f"opscloud-{suffix}-", dir=temp_root))
137
+ _UNIQUE_OFFLOAD_FALLBACK_ROOT = _prepare_temp_dir(unique)
138
+ return _UNIQUE_OFFLOAD_FALLBACK_ROOT
139
+
140
+
141
+ def delete_offloaded_history(thread_id: str) -> bool:
142
+ """Remove a thread's offloaded conversation-history archive."""
143
+ if not thread_id:
144
+ return False
145
+ try:
146
+ archive_dir = _offload_fallback_root() / "conversation_history"
147
+ except (OSError, RuntimeError):
148
+ logger.warning(
149
+ "Could not resolve offload root to clean history for thread %s",
150
+ thread_id,
151
+ exc_info=True,
152
+ )
153
+ return False
154
+ archive_path = archive_dir / f"{thread_id}.md"
155
+ if archive_path.parent != archive_dir:
156
+ logger.warning(
157
+ "Refusing to delete offloaded history for suspicious thread id %r",
158
+ thread_id,
159
+ )
160
+ return False
161
+ try:
162
+ archive_path.unlink()
163
+ except FileNotFoundError:
164
+ return False
165
+ except OSError:
166
+ logger.warning(
167
+ "Failed to delete offloaded conversation history for thread %s",
168
+ thread_id,
169
+ exc_info=True,
170
+ )
171
+ return False
172
+ logger.debug("Deleted offloaded conversation history for thread %s", thread_id)
173
+ return True
opscloud/output.py ADDED
@@ -0,0 +1,64 @@
1
+ """Machine-readable JSON output helpers for CLI subcommands.
2
+
3
+ This module deliberately stays stdlib-only so it can be imported from CLI
4
+ startup paths without pulling in unnecessary dependency trees.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+ from typing import Any, Literal
13
+
14
+ OutputFormat = Literal["text", "json"]
15
+ """Accepted internal output modes for CLI subcommands."""
16
+
17
+
18
+ def add_json_output_arg(
19
+ parser: argparse.ArgumentParser, *, default: OutputFormat | None = None
20
+ ) -> None:
21
+ """Add a `--json` flag to an argparse parser.
22
+
23
+ Args:
24
+ parser: Parser to update.
25
+ default: Default output format for this parser.
26
+ Pass `None` for subparsers so parent parser values are preserved.
27
+ """
28
+ if default is None:
29
+ parser.add_argument(
30
+ "--json",
31
+ dest="output_format",
32
+ action="store_const",
33
+ const="json",
34
+ default=argparse.SUPPRESS,
35
+ help="Emit machine-readable JSON for this command",
36
+ )
37
+ else:
38
+ parser.add_argument(
39
+ "--json",
40
+ dest="output_format",
41
+ action="store_const",
42
+ const="json",
43
+ default=default,
44
+ help="Emit machine-readable JSON for this command",
45
+ )
46
+
47
+
48
+ def write_json(command: str, data: list[Any] | dict[str, Any]) -> None:
49
+ """Write a JSON envelope to stdout and flush.
50
+
51
+ The envelope is a single-line JSON object with a stable schema:
52
+
53
+ ```json
54
+ {"schema_version": 1, "command": "...", "data": ...}
55
+ ```
56
+
57
+ Args:
58
+ command: Self-documenting command name (e.g. `'list'`, `'threads list'`).
59
+ data: Payload — typically a list for listing commands or a dict
60
+ for action/info commands.
61
+ """
62
+ envelope = {"schema_version": 1, "command": command, "data": data}
63
+ sys.stdout.write(json.dumps(envelope, default=str) + "\n")
64
+ sys.stdout.flush()