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,1103 @@
1
+ """Core Agent Factory for OpsCloud.
2
+
3
+ Produces a compiled LangGraph ``Pregel`` graph wired with the complete
4
+ middleware stack, composite backend, dynamic subagents, skills, MCP servers,
5
+ and security controls with Database as Source of Truth.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from collections.abc import Callable, Mapping, Sequence
12
+ import concurrent.futures
13
+ import fnmatch
14
+ from pathlib import Path
15
+ from typing import Any, cast
16
+
17
+ from deepagents import create_deep_agent
18
+ from deepagents.backends import FilesystemBackend
19
+ from deepagents.backends.protocol import BackendProtocol
20
+ from deepagents.middleware import MemoryMiddleware
21
+ from deepagents.middleware.async_subagents import AsyncSubAgent
22
+ from langchain.agents.middleware.types import AgentMiddleware
23
+ from langchain_core.language_models import BaseChatModel
24
+ from langchain_core.tools import BaseTool
25
+ from langgraph.graph.state import CompiledStateGraph
26
+
27
+ from opscloud.agent.config import AgentContextSchema, CLIContextSchema
28
+ from opscloud.backend.composite import OpsCloudCompositeBackend
29
+ from opscloud.backend.local import LocalShellBackend
30
+ from opscloud.config import paths
31
+ from opscloud.config.settings import get_settings
32
+ from opscloud.project_utils import ProjectContext, get_server_project_context
33
+ from opscloud.mcp.discovery import discover_mcp_configs
34
+ from opscloud.memory.registry import MemoryRegistry
35
+ from opscloud.middleware.ask_user import AskUserMiddleware
36
+ from opscloud.middleware.auto_mode import (
37
+ AsyncApprovalHITLMiddleware,
38
+ AutoModeHITLMiddleware,
39
+ )
40
+ from opscloud.middleware.compaction import (
41
+ _create_cli_compaction_middleware,
42
+ )
43
+ from opscloud.middleware.configurable_model import ConfigurableModelMiddleware
44
+ from opscloud.middleware.jev_model_router import JevDynamicModelRouterMiddleware
45
+ from opscloud.middleware.cost_tracking import CostTrackingMiddleware
46
+ from opscloud.middleware.glm_stall_recovery import (
47
+ GlmTerminalStallRecoveryMiddleware,
48
+ )
49
+ from opscloud.middleware.goal_criteria import (
50
+ GoalCriteriaMiddleware,
51
+ _WebSearchBudgetMiddleware,
52
+ create_goal_criteria_agent,
53
+ create_goal_criteria_fallback_agent,
54
+ )
55
+ from opscloud.middleware.goal_tools import GoalToolsMiddleware
56
+ from opscloud.middleware.headless_mcp_guard import (
57
+ HeadlessMCPGuardMiddleware,
58
+ gated_mcp_tool_names,
59
+ )
60
+ from opscloud.middleware.local_context import LocalContextMiddleware
61
+ from opscloud.middleware.mcp_middleware import MCPToolMiddleware
62
+ from opscloud.middleware.memory_guard import ManagedMemoryGuardMiddleware
63
+ from opscloud.middleware.model_retry import CodeModelRetryMiddleware
64
+ from opscloud.middleware.reliable_rubric import ReliableRubricMiddleware
65
+ from opscloud.middleware.resume_state import ResumeStateMiddleware
66
+ from opscloud.middleware.server_hooks import ServerHooksMiddleware
67
+ from opscloud.middleware.shell_allow_list import ShellAllowListMiddleware
68
+ from opscloud.middleware.skills import (
69
+ PluginSkillsMiddleware,
70
+ discover_skill_dirs,
71
+ )
72
+ from opscloud.middleware.subagents import SubagentsMiddleware
73
+ from opscloud.middleware.subagent_telemetry import SubagentTelemetryMiddleware
74
+ from opscloud.middleware.tool_filter import ToolFilterMiddleware
75
+ from opscloud.middleware.unified_system_message import (
76
+ UnifiedSystemMessageMiddleware,
77
+ )
78
+ from opscloud.model.factory import create_model
79
+ from opscloud.prompts import (
80
+ get_base_system_prompt,
81
+ get_memory_system_prompt,
82
+ )
83
+ from opscloud.rubrics.evaluator import (
84
+ _RUBRIC_GRADER_SYSTEM_PROMPT,
85
+ _create_rubric_grader_tools,
86
+ )
87
+ from opscloud.skills.registry import SkillRegistry, SkillSourceTuple
88
+ from opscloud.subagents.loader import list_subagents
89
+ from opscloud.subagents.types import SubagentMetadata
90
+ from opscloud.tools.catalog import register_all_tools
91
+ from opscloud.tools.registry import ToolRegistry
92
+ from opscloud.utils.logger import get_logger
93
+
94
+ logger = get_logger(__name__)
95
+
96
+
97
+ def _build_hitl_interrupt_config(auto_approve: bool) -> dict[str, bool]:
98
+ """Build HITL interrupt configuration for tools."""
99
+ if auto_approve:
100
+ return {}
101
+ return {
102
+ "execute": True,
103
+ "delete_file": True,
104
+ "edit_file": True,
105
+ "write_file": True,
106
+ }
107
+
108
+
109
+ def _format_description(tool_call: Any = None, *args: Any, **kwargs: Any) -> str:
110
+ if isinstance(tool_call, str) and args and isinstance(args[0], dict):
111
+ name = tool_call
112
+ tool_args = args[0]
113
+ elif isinstance(tool_call, dict):
114
+ name = tool_call.get("name") or tool_call.get("tool_name") or ""
115
+ tool_args = tool_call.get("args") or tool_call.get("parameters") or {}
116
+ else:
117
+ name = getattr(tool_call, "name", None) or getattr(tool_call, "tool_name", None) or ""
118
+ tool_args = getattr(tool_call, "args", {}) or getattr(tool_call, "parameters", {}) or {}
119
+
120
+ if not isinstance(tool_args, dict):
121
+ tool_args = {}
122
+
123
+ if name in {"execute", "run_command"}:
124
+ cmd = tool_args.get("command") or tool_args.get("CommandLine") or ""
125
+ return f"Execute shell command: {cmd}"
126
+ if name in {"write_file", "write_to_file"}:
127
+ path = tool_args.get("file_path") or tool_args.get("TargetFile") or ""
128
+ return f"Write file: {path}"
129
+ if name in {"edit_file", "replace_file_content", "multi_replace_file_content"}:
130
+ path = tool_args.get("file_path") or tool_args.get("TargetFile") or ""
131
+ return f"Edit file: {path}"
132
+ if name in {"task", "subagent"}:
133
+ sub_type = tool_args.get("subagent_type") or tool_args.get("name") or tool_args.get("agent") or "subagent"
134
+ desc = tool_args.get("description") or tool_args.get("prompt") or tool_args.get("task") or ""
135
+ return f"Spawn {sub_type} subagent: {desc}" if desc else f"Spawn {sub_type} subagent"
136
+ if name == "js_eval":
137
+ code_snippet = str(tool_args.get("code") or "").strip()
138
+ first_line = code_snippet.split("\n")[0][:80]
139
+ return (
140
+ f"Evaluate script / dispatch subagents: {first_line}"
141
+ if first_line
142
+ else "Evaluate script / dispatch subagents"
143
+ )
144
+ if ":" in name:
145
+ srv, tname = name.split(":", 1)
146
+ args_str = ", ".join(f"{k}={v}" for k, v in list(tool_args.items())[:3])
147
+ return f"Execute {srv} action '{tname}' ({args_str})" if args_str else f"Execute {srv} action '{tname}'"
148
+ args_str = ", ".join(f"{k}={v}" for k, v in list(tool_args.items())[:3])
149
+ return f"Execute tool '{name}' ({args_str})" if args_str else f"Execute tool '{name}'"
150
+
151
+
152
+ def _should_interrupt_tool_call(
153
+ request: Any = None,
154
+ *args: Any,
155
+ auto_mode_enabled: bool = True,
156
+ **kwargs: Any,
157
+ ) -> bool:
158
+ """Decide whether stock HITL should pause for a gated tool call.
159
+
160
+ Follows the unified approval mode hierarchy:
161
+ 1. If server hooks already decided permission -> False (never interrupt).
162
+ 2. If mode in ("auto", "smart") -> False when classifier is enabled, True if classifier disabled.
163
+ 3. Internal AGENTS.md writes or read-only filesystem tools -> False.
164
+ 4. In "manual" mode -> True (require human confirmation).
165
+ """
166
+ from opscloud._constants import READONLY_FS_TOOLS
167
+ from opscloud.middleware.auto_mode import _async_routing_mode
168
+ from opscloud.middleware.server_hooks import hook_decided_permission
169
+ from opscloud.security.approval_mode import ApprovalMode, coerce_approval_mode
170
+ from opscloud.security.approval_mode_source import _resolve_approval_mode
171
+
172
+ state = (
173
+ getattr(request, "state", None)
174
+ or (request.get("state") if isinstance(request, dict) else None)
175
+ or (args[0] if args and isinstance(args[0], dict) else None)
176
+ )
177
+ tool_call = (
178
+ getattr(request, "tool_call", None)
179
+ or getattr(request, "action", None)
180
+ or (request.get("tool_call") if isinstance(request, dict) else None)
181
+ or (request.get("action") if isinstance(request, dict) else None)
182
+ or request
183
+ )
184
+ tool_id = str(tool_call.get("id") or "") if isinstance(tool_call, dict) else getattr(tool_call, "id", "")
185
+ if hook_decided_permission(state, tool_id):
186
+ return False
187
+
188
+ mode = _async_routing_mode(state)
189
+ if mode is None and isinstance(state, Mapping) and state.get("approval_mode"):
190
+ mode = coerce_approval_mode(state.get("approval_mode"))
191
+ if mode is None:
192
+ runtime = (
193
+ getattr(request, "runtime", None)
194
+ or (request.get("runtime") if isinstance(request, dict) else None)
195
+ or kwargs.get("runtime")
196
+ )
197
+ ctx = getattr(runtime, "context", None) if runtime else None
198
+ if not ctx and hasattr(runtime, "config"):
199
+ ctx = getattr(runtime, "config", None)
200
+ if not ctx and isinstance(state, dict):
201
+ ctx = state
202
+ mode = _resolve_approval_mode(ctx, getattr(runtime, "store", None) if runtime else None)
203
+
204
+ if mode in (ApprovalMode.AUTO, ApprovalMode.SMART, "auto", "smart"):
205
+ return not auto_mode_enabled
206
+
207
+ # Never interrupt internal memory file writes (e.g. AGENTS.md) or read-only tools
208
+ tool_name = str(tool_call.get("name") or tool_call.get("action") or "") if isinstance(tool_call, dict) else getattr(tool_call, "name", "")
209
+ tool_args = tool_call.get("args") or tool_call.get("parameters") or {} if isinstance(tool_call, dict) else getattr(tool_call, "args", {})
210
+ path_str = str(tool_args.get("path") or tool_args.get("TargetFile") or tool_args.get("file_path") or "")
211
+ if tool_name in READONLY_FS_TOOLS or "AGENTS.md" in path_str:
212
+ return False
213
+
214
+ return True
215
+
216
+
217
+ def _interrupt_predicate(
218
+ *,
219
+ auto_mode_enabled: bool,
220
+ ) -> Callable[[Any], bool]:
221
+ """Bind runtime eligibility into a stock-HITL predicate.
222
+
223
+ Returns a predicate suitable for ``InterruptOnConfig.when`` that closes
224
+ over ``auto_mode_enabled``.
225
+ """
226
+
227
+ def should_interrupt(request: Any) -> bool:
228
+ """Evaluate whether the given tool call request warrants an interrupt.
229
+
230
+ Args:
231
+ request: The tool execution request to check.
232
+
233
+ Returns:
234
+ bool: True if execution should pause for human approval.
235
+ """
236
+ return _should_interrupt_tool_call(request, auto_mode_enabled=auto_mode_enabled)
237
+
238
+ return should_interrupt
239
+
240
+
241
+ INTERPRETER_PTC_SAFE_PRESET: frozenset[str] = frozenset({"read_file", "glob", "grep"})
242
+ _INTERPRETER_WRITE_TOOLS: frozenset[str] = frozenset(
243
+ {
244
+ "execute",
245
+ "write_file",
246
+ "edit_file",
247
+ "delete_file",
248
+ "replace_file_content",
249
+ "multi_replace_file_content",
250
+ "write_to_file",
251
+ "write_todos",
252
+ "task",
253
+ "subagent",
254
+ }
255
+ )
256
+
257
+
258
+ def _resolve_ptc_option(
259
+ ptc: str | bool | list[str] | None,
260
+ *,
261
+ tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]],
262
+ acknowledge_unsafe: bool,
263
+ auto_approve: bool,
264
+ ) -> list[str] | None:
265
+ if ptc is False or ptc is None or ptc == []:
266
+ return None
267
+
268
+ live_names: list[str] = []
269
+ for candidate in tools:
270
+ if isinstance(candidate, BaseTool):
271
+ name = candidate.name
272
+ if isinstance(name, str):
273
+ live_names.append(name)
274
+ elif isinstance(candidate, dict):
275
+ raw_name = candidate.get("name")
276
+ if isinstance(raw_name, str):
277
+ live_names.append(raw_name)
278
+ else:
279
+ attr = getattr(candidate, "name", None)
280
+ if isinstance(attr, str):
281
+ live_names.append(attr)
282
+ live_set: set[str] = set(live_names)
283
+
284
+ if isinstance(ptc, str):
285
+ normalized = ptc.strip().lower()
286
+ if normalized == "safe":
287
+ return sorted(INTERPRETER_PTC_SAFE_PRESET)
288
+ if normalized == "all":
289
+ if not auto_approve and not acknowledge_unsafe:
290
+ msg = (
291
+ "interpreter_ptc='all' exposes every host tool to PTC "
292
+ "calls that bypass HITL approval. Set "
293
+ "interpreter_ptc_acknowledge_unsafe=True (or use "
294
+ "auto_approve=True) to opt in."
295
+ )
296
+ raise ValueError(msg)
297
+ included = sorted(live_set)
298
+ write_included = sorted(_INTERPRETER_WRITE_TOOLS & live_set)
299
+ if write_included:
300
+ logger.info(
301
+ "interpreter_ptc='all' includes write/shell tools: %s",
302
+ write_included,
303
+ )
304
+ return included
305
+ msg = f"Invalid interpreter_ptc preset {ptc!r}. Must be 'safe', 'all', or a list of tool names."
306
+ raise ValueError(msg)
307
+
308
+ if isinstance(ptc, list):
309
+ for item in ptc:
310
+ if not isinstance(item, str):
311
+ msg = "interpreter_ptc list must contain only strings."
312
+ raise TypeError(msg)
313
+ configured_set = set(ptc)
314
+ unacknowledged = configured_set & _INTERPRETER_WRITE_TOOLS
315
+ if unacknowledged and not auto_approve and not acknowledge_unsafe:
316
+ unack_str = ", ".join(sorted(unacknowledged))
317
+ msg = (
318
+ f"interpreter_ptc contains write/shell tools ({unack_str}) "
319
+ "that bypass HITL approval. Set "
320
+ "interpreter_ptc_acknowledge_unsafe=True (or use "
321
+ "auto_approve=True) to opt in."
322
+ )
323
+ raise ValueError(msg)
324
+ return sorted(configured_set)
325
+
326
+ if ptc:
327
+ return sorted(INTERPRETER_PTC_SAFE_PRESET)
328
+
329
+ msg = "interpreter_ptc must be a string, boolean, list of strings, or None."
330
+ raise TypeError(msg)
331
+
332
+
333
+ def _subagent_cli_middleware(
334
+ *,
335
+ has_explicit_model: bool,
336
+ assistant_id: str,
337
+ subagent_name: str,
338
+ subagent_meta: SubagentMetadata | dict[str, Any] | None = None,
339
+ allowed_tools: Sequence[str] | None = None,
340
+ allowed_skills: Sequence[str] | None = None,
341
+ capabilities: Sequence[dict[str, Any]] | None = None,
342
+ interactive: bool = True,
343
+ auto_approve: bool = False,
344
+ shell_allow_list: list[str] | None = None,
345
+ interrupt_on: dict[str, Any] | None = None,
346
+ worktree_root: str | Path | None = None,
347
+ subagent_path: str | None = None,
348
+ custom_middleware: Sequence[Any] | None = None,
349
+ mcp_server_info: Sequence[Any] | None = None,
350
+ mcp_config: dict[str, Any] | None = None,
351
+ mcp_tools: list[Any] | None = None,
352
+ backend: BackendProtocol | None = None,
353
+ model: str | BaseChatModel | None = None,
354
+ ) -> list[Any]:
355
+ middleware: list[AgentMiddleware[Any, Any]] = []
356
+ subagent_cwd = Path(worktree_root) if worktree_root is not None else Path.cwd()
357
+
358
+ if interrupt_on is not None and interactive and not auto_approve:
359
+ middleware.append(
360
+ AutoModeHITLMiddleware(
361
+ interrupt_on,
362
+ worktree_root=subagent_cwd,
363
+ shell_allow_list=list(shell_allow_list or []),
364
+ )
365
+ )
366
+ elif interrupt_on is not None:
367
+ middleware.append(AsyncApprovalHITLMiddleware(interrupt_on))
368
+
369
+ if not has_explicit_model:
370
+ middleware.append(ConfigurableModelMiddleware(persist_model_state=False))
371
+
372
+ middleware.append(
373
+ JevDynamicModelRouterMiddleware(
374
+ subagent_name=subagent_name,
375
+ capabilities=capabilities,
376
+ )
377
+ )
378
+
379
+ if backend is not None and model is not None:
380
+ try:
381
+ middleware.append(_create_cli_compaction_middleware(model, backend))
382
+ except Exception as exc:
383
+ logger.warning(
384
+ "Failed to initialize compaction middleware for subagent %s: %s",
385
+ subagent_name,
386
+ exc,
387
+ )
388
+
389
+ middleware.append(CodeModelRetryMiddleware())
390
+ middleware.append(CostTrackingMiddleware(nested=True))
391
+ middleware.append(SubagentTelemetryMiddleware(subagent_name=subagent_name))
392
+
393
+ if not interactive:
394
+ middleware.append(GlmTerminalStallRecoveryMiddleware())
395
+
396
+ if shell_allow_list:
397
+ middleware.append(ShellAllowListMiddleware(shell_allow_list))
398
+
399
+ middleware.append(
400
+ ServerHooksMiddleware(
401
+ cwd=subagent_cwd,
402
+ emit_stop=False,
403
+ mcp_tools=mcp_tools or (),
404
+ )
405
+ )
406
+
407
+ # MCP Context Middleware — injects MCP server inventory into system prompt
408
+ if mcp_server_info or mcp_config:
409
+ from opscloud.middleware.mcp_context import MCPContextMiddleware
410
+
411
+ middleware.append(MCPContextMiddleware(mcp_server_info=mcp_server_info, mcp_config=mcp_config))
412
+
413
+ if mcp_server_info or mcp_config or mcp_tools:
414
+ middleware.append(MCPToolMiddleware())
415
+
416
+ server_keys: list[str] = []
417
+ if mcp_config:
418
+ server_keys.extend(mcp_config.keys())
419
+ if mcp_server_info:
420
+ server_keys.extend(getattr(s, "name", str(s)) for s in mcp_server_info)
421
+
422
+ if allowed_tools or capabilities:
423
+ middleware.append(
424
+ ToolFilterMiddleware(
425
+ allowed_patterns=allowed_tools,
426
+ capabilities=capabilities,
427
+ subagent_name=subagent_name,
428
+ mcp_tools=mcp_tools,
429
+ known_mcp_servers=server_keys or None,
430
+ )
431
+ )
432
+
433
+ from opscloud.plugins.adapters.agents import get_subagent_skills_source
434
+ from opscloud.skills.sources import CodeSkillSource, get_skill_sources
435
+
436
+ skill_sources: list[CodeSkillSource] = []
437
+ meta_for_skills = subagent_meta or {"path": subagent_path, "name": subagent_name}
438
+ bundled_source = get_subagent_skills_source(meta_for_skills)
439
+ if bundled_source and Path(bundled_source[0]).is_dir():
440
+ skill_sources.append(bundled_source)
441
+
442
+ if allowed_skills is not None:
443
+ global_sources = get_skill_sources(
444
+ project_root=Path(worktree_root) if worktree_root else None,
445
+ include_subagent_skills=True,
446
+ )
447
+ backend_fs = FilesystemBackend(virtual_mode=False)
448
+ for src in global_sources:
449
+ src_path = src[0]
450
+ try:
451
+ found_dirs = discover_skill_dirs(backend_fs, src_path)
452
+ for dir_path, _ in found_dirs:
453
+ skill_name = Path(dir_path).name
454
+ if any(
455
+ fnmatch.fnmatch(skill_name, pat) or fnmatch.fnmatch(skill_name.lower(), pat.lower())
456
+ for pat in allowed_skills
457
+ ):
458
+ if not any(s[0] == src[0] for s in skill_sources):
459
+ skill_sources.append(src)
460
+ break
461
+ except Exception as exc:
462
+ logger.debug("Skill discovery failed for source %s: %s", src_path, exc)
463
+ elif not skill_sources:
464
+ # If not a bundled subagent and no allowed_skills filter, inherit global sources
465
+ skill_sources.extend(
466
+ get_skill_sources(
467
+ project_root=Path(worktree_root) if worktree_root else None,
468
+ include_subagent_skills=True,
469
+ )
470
+ )
471
+
472
+ if skill_sources:
473
+ middleware.append(
474
+ PluginSkillsMiddleware(
475
+ backend=FilesystemBackend(virtual_mode=False),
476
+ sources=skill_sources,
477
+ allowed_skills=allowed_skills,
478
+ project_root=Path(worktree_root) if worktree_root else None,
479
+ )
480
+ )
481
+
482
+ if custom_middleware:
483
+ for extra_mw in custom_middleware:
484
+ middleware.append(extra_mw)
485
+
486
+ from opscloud.middleware.ask_user import AskUserMiddleware
487
+ from opscloud.middleware.subagent_artifacts import SubagentArtifactsMiddleware
488
+
489
+ middleware.append(AskUserMiddleware())
490
+
491
+ middleware.append(ManagedMemoryGuardMiddleware())
492
+ middleware.append(SubagentArtifactsMiddleware())
493
+ middleware.append(UnifiedSystemMessageMiddleware())
494
+ return middleware
495
+
496
+
497
+ def create_opscloud_agent(
498
+ model: str | BaseChatModel | None = None,
499
+ *,
500
+ assistant_id: str = "opscloud",
501
+ cloud_provider: str | None = None,
502
+ tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,
503
+ mcp_tools: Sequence[BaseTool] | None = None,
504
+ system_prompt: str | None = None,
505
+ interactive: bool = True,
506
+ auto_approve: bool = False,
507
+ enable_shell: bool = True,
508
+ enable_interpreter: bool = True,
509
+ enable_ask_user: bool = True,
510
+ checkpointer: Any | None = None,
511
+ store: Any | None = None,
512
+ config_store: Any | None = None,
513
+ cwd: str | Path | None = None,
514
+ project_context: ProjectContext | None = None,
515
+ thread_id: str | None = None,
516
+ reasoning_effort: str | None = None,
517
+ extra_kwargs: dict[str, Any] | None = None,
518
+ goal_criteria_tools: Sequence[BaseTool | Callable[..., Any]] | None = None,
519
+ async_subagents: Sequence[AsyncSubAgent] | None = None,
520
+ fs_tools: Sequence[str] | None = None,
521
+ **kwargs: Any,
522
+ ) -> tuple[CompiledStateGraph[Any, Any, Any, Any], OpsCloudCompositeBackend]:
523
+ """Create the OpsCloud agent graph with DB-backed resources.
524
+
525
+ Returns:
526
+ tuple[Pregel, OpsCloudCompositeBackend]: Compiled Pregel graph and backend.
527
+ """
528
+ settings = get_settings()
529
+
530
+ # 1. Resolve working directory following dcode conventions
531
+ resolved_context = project_context or get_server_project_context()
532
+ effective_cwd: Path
533
+ if cwd is not None:
534
+ effective_cwd = Path(cwd).resolve()
535
+ elif resolved_context is not None:
536
+ effective_cwd = resolved_context.user_cwd
537
+ elif settings.cwd is not None:
538
+ effective_cwd = settings.cwd.resolve()
539
+ else:
540
+ effective_cwd = Path.cwd().resolve()
541
+
542
+ if resolved_context and resolved_context.project_root:
543
+ effective_project_root = resolved_context.project_root
544
+ elif cwd is not None:
545
+ from opscloud.project_utils import find_project_root
546
+
547
+ effective_project_root = find_project_root(effective_cwd) or effective_cwd
548
+ else:
549
+ effective_project_root = settings.effective_project_root
550
+ project_scope_root = effective_project_root or effective_cwd
551
+
552
+ # 2. Create composite backend
553
+ default_backend = LocalShellBackend(root_dir=effective_cwd)
554
+ composite_backend = OpsCloudCompositeBackend(default=default_backend)
555
+
556
+ # 3. Resolve active chat model
557
+ eff = (
558
+ reasoning_effort
559
+ or (extra_kwargs.get("reasoning_effort") if extra_kwargs else None)
560
+ or getattr(settings, "reasoning_effort", None)
561
+ )
562
+ model_provider: str | None = None
563
+ model_context_limit: int | None = None
564
+ model_unsupported_modalities: frozenset[str] = frozenset()
565
+
566
+ if isinstance(model, str) or model is None:
567
+ merged_kwargs = dict(extra_kwargs or {})
568
+ if eff:
569
+ merged_kwargs["reasoning_effort"] = eff
570
+ model_res = create_model(model, extra_kwargs=merged_kwargs if merged_kwargs else None)
571
+ model_res.apply_to_settings()
572
+ active_model = model_res.model
573
+ model_name = getattr(active_model, "model_name", model or "default-model")
574
+ model_provider = model_res.provider
575
+ model_context_limit = model_res.context_limit
576
+ model_unsupported_modalities = model_res.unsupported_modalities
577
+ else:
578
+ active_model = model
579
+ model_name = getattr(active_model, "model_name", "custom-model")
580
+ model_provider = getattr(active_model, "_llm_type", None)
581
+
582
+ # 4. Register catalog tools and build tool list
583
+ register_all_tools()
584
+ tool_registry = ToolRegistry.get_instance()
585
+
586
+ all_tools: list[BaseTool] = []
587
+ if tools:
588
+ for t in tools:
589
+ if isinstance(t, BaseTool):
590
+ all_tools.append(t)
591
+
592
+ # Append default tools if not already present (core web exploration)
593
+ # Following dcode conventions: web_search is only bound when Tavily is configured
594
+ default_tool_names = ["fetch_url"]
595
+ if getattr(settings, "has_tavily", False):
596
+ default_tool_names.insert(0, "web_search")
597
+ for tool_name in default_tool_names:
598
+ if not any(getattr(t, "name", None) == tool_name for t in all_tools):
599
+ try:
600
+ all_tools.append(tool_registry.build_tool(tool_name, backend=composite_backend))
601
+ except Exception as e:
602
+ logger.warning("Failed to build registered tool %s: %s", tool_name, e)
603
+
604
+ # 5. MCP Discovery
605
+ mcp_tools_list: list[BaseTool] = []
606
+ cached_infos: list[Any] | None = None
607
+ mcp_configs: dict[str, Any] | None = None
608
+ if mcp_tools is not None:
609
+ mcp_tools_list = list(mcp_tools)
610
+ all_tools.extend(mcp_tools)
611
+ else:
612
+ mcp_configs = discover_mcp_configs(project_scope_root)
613
+ from opscloud.mcp.preload import (
614
+ get_cached_mcp_server_infos,
615
+ preload_mcp_metadata,
616
+ )
617
+ from opscloud.mcp.session_manager import (
618
+ MCPSessionManager,
619
+ build_mcp_tools_from_server_infos,
620
+ )
621
+
622
+ mcp_manager = MCPSessionManager.get_instance(mcp_configs or {})
623
+ if mcp_configs:
624
+ try:
625
+ cached_infos = get_cached_mcp_server_infos()
626
+ cached_by_name = {info.name: info for info in cached_infos}
627
+ missing_configs = {
628
+ k: v
629
+ for k, v in mcp_configs.items()
630
+ if k not in cached_by_name or not cached_by_name[k].enabled or cached_by_name[k].status != "ok"
631
+ }
632
+
633
+ if missing_configs:
634
+ try:
635
+ loop = asyncio.get_running_loop()
636
+ except RuntimeError:
637
+ loop = None
638
+
639
+ if loop is not None and loop.is_running():
640
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
641
+ new_infos = pool.submit(asyncio.run, preload_mcp_metadata(missing_configs)).result()
642
+ else:
643
+ new_infos = asyncio.run(preload_mcp_metadata(missing_configs))
644
+ cached_infos = get_cached_mcp_server_infos()
645
+
646
+ coordinator_infos = [
647
+ info for info in cached_infos if info.name in mcp_configs and info.enabled and info.status == "ok"
648
+ ]
649
+ list(coordinator_infos)
650
+
651
+ discovered_tools = build_mcp_tools_from_server_infos(coordinator_infos, mcp_manager)
652
+ if discovered_tools:
653
+ mcp_tools_list.extend(discovered_tools)
654
+ all_tools.extend(discovered_tools)
655
+ logger.info("Active MCP servers loaded: %d (tools: %d)", len(mcp_configs), len(discovered_tools))
656
+ except Exception as e:
657
+ logger.warning("Failed to initialize MCP tools in agent factory: %s", e)
658
+
659
+ # 6. Discover memory sources (ensuring ~/.opscloud/AGENTS.md exists)
660
+ from opscloud.config.paths import ensure_user_agent_md
661
+
662
+ ensure_user_agent_md(assistant_id)
663
+ memory_registry = MemoryRegistry.get_instance(store=config_store)
664
+ memory_sources_str = memory_registry.get_all_memory_sources(project_root=project_scope_root)
665
+
666
+ # 7. Discover skills
667
+ skill_registry = SkillRegistry.get_instance(store=config_store)
668
+ skill_registry.discover_skills(project_scope_root, force=True)
669
+ skill_registry.get_sources_for_middleware()
670
+
671
+ # 8. Define approval policies and subagents
672
+ interrupt_on: Any | None = None
673
+ if auto_approve:
674
+ interrupt_on = {}
675
+ else:
676
+ from langchain.agents.middleware.human_in_the_loop import InterruptOnConfig
677
+
678
+ from opscloud.middleware.auto_mode import DynamicInterruptMapping
679
+
680
+ default_tool_hitl_config: InterruptOnConfig = {
681
+ "allowed_decisions": ["approve", "reject"],
682
+ "description": _format_description,
683
+ "when": _should_interrupt_tool_call,
684
+ }
685
+ interrupt_on = DynamicInterruptMapping(default_config=default_tool_hitl_config)
686
+
687
+ user_agents_dir = paths.user_agents_dir(assistant_id)
688
+ project_agents_dir = paths.project_agents_dir(project_scope_root)
689
+ if not project_agents_dir.is_dir() and (project_scope_root / "agents").is_dir():
690
+ project_agents_dir = project_scope_root / "agents"
691
+
692
+ subagent_metas = list_subagents(
693
+ user_agents_dir=user_agents_dir if user_agents_dir.is_dir() else None,
694
+ project_agents_dir=project_agents_dir if project_agents_dir.is_dir() else None,
695
+ project_root=project_scope_root,
696
+ include_plugins=True,
697
+ )
698
+
699
+ subagent_by_name: dict[str, SubagentMetadata] = {}
700
+ for meta in subagent_metas:
701
+ name = meta.get("name")
702
+ if name:
703
+ subagent_by_name[name] = meta
704
+
705
+ allow_list = getattr(settings, "shell_allow_list", None) or []
706
+
707
+ from deepagents.middleware.subagents import GENERAL_PURPOSE_SUBAGENT, SubAgent
708
+
709
+ compiled_subagents: list[SubAgent] = []
710
+ base_subagent_tools = [t for t in all_tools if t not in mcp_tools_list]
711
+ for name, subagent_meta in subagent_by_name.items():
712
+ model_spec = subagent_meta.get("model")
713
+ sub_prompt = subagent_meta.get("system_prompt") or ""
714
+ subagent_dict: dict[str, Any] = {
715
+ "name": subagent_meta.get("name") or name,
716
+ "description": subagent_meta.get("description") or "",
717
+ "system_prompt": sub_prompt,
718
+ }
719
+ if model_spec:
720
+ try:
721
+ sub_model_res = create_model(model_spec)
722
+ subagent_dict["model"] = sub_model_res.model
723
+ except Exception as e:
724
+ logger.warning("Failed to resolve model %s for subagent %s: %s", model_spec, name, e)
725
+
726
+ subagent_name = subagent_meta.get("name") or name
727
+ subagent_path = subagent_meta.get("path")
728
+ custom_mw = subagent_meta.get("middleware")
729
+
730
+ # Build per-subagent MCP config and tools for this subagent
731
+ from opscloud.plugins.adapters.mcp import prepare_subagent_mcp
732
+
733
+ servers, subagent_mcp_tools, sub_mcp_server_infos = prepare_subagent_mcp(
734
+ subagent_name, subagent_meta, project_scope_root
735
+ )
736
+
737
+ if subagent_mcp_tools:
738
+ subagent_dict["tools"] = [*base_subagent_tools, *subagent_mcp_tools]
739
+ else:
740
+ subagent_dict["tools"] = list(base_subagent_tools)
741
+
742
+ sub_middleware = _subagent_cli_middleware(
743
+ has_explicit_model=bool(model_spec),
744
+ assistant_id=assistant_id,
745
+ subagent_name=subagent_name,
746
+ subagent_meta=subagent_meta,
747
+ allowed_tools=subagent_meta.get("tools"),
748
+ allowed_skills=subagent_meta.get("skills"),
749
+ capabilities=subagent_meta.get("capabilities"),
750
+ interactive=interactive,
751
+ auto_approve=auto_approve,
752
+ shell_allow_list=allow_list,
753
+ interrupt_on=interrupt_on,
754
+ worktree_root=effective_cwd,
755
+ subagent_path=subagent_path if subagent_path else None,
756
+ custom_middleware=custom_mw if isinstance(custom_mw, list) else None,
757
+ mcp_server_info=sub_mcp_server_infos or None,
758
+ mcp_config=servers or None,
759
+ mcp_tools=subagent_mcp_tools or None,
760
+ backend=composite_backend,
761
+ model=subagent_dict.get("model") or active_model,
762
+ )
763
+ if sub_middleware:
764
+ subagent_dict["middleware"] = sub_middleware
765
+ if interrupt_on is not None:
766
+ subagent_dict["interrupt_on"] = {}
767
+
768
+ compiled_subagents.append(cast(SubAgent, subagent_dict))
769
+ logger.info(
770
+ "Compiled subagent '%s' (tools: %d)",
771
+ subagent_name,
772
+ len(subagent_dict.get("tools", [])),
773
+ extra={"subagent": subagent_name, "tools_count": len(subagent_dict.get("tools", []))},
774
+ )
775
+
776
+ if not any(sub.get("name") == GENERAL_PURPOSE_SUBAGENT["name"] for sub in compiled_subagents):
777
+ gp_name = GENERAL_PURPOSE_SUBAGENT["name"]
778
+ gp_description = GENERAL_PURPOSE_SUBAGENT["description"]
779
+ gp_system_prompt = GENERAL_PURPOSE_SUBAGENT.get("system_prompt") or ""
780
+ gp_middleware = _subagent_cli_middleware(
781
+ has_explicit_model=False,
782
+ assistant_id=assistant_id,
783
+ subagent_name=gp_name,
784
+ interactive=interactive,
785
+ auto_approve=auto_approve,
786
+ shell_allow_list=allow_list,
787
+ interrupt_on=interrupt_on,
788
+ worktree_root=effective_cwd,
789
+ backend=composite_backend,
790
+ model=active_model,
791
+ )
792
+ gp_subagent: dict[str, Any] = {
793
+ "name": gp_name,
794
+ "description": gp_description,
795
+ "system_prompt": gp_system_prompt,
796
+ "tools": list(base_subagent_tools),
797
+ "middleware": gp_middleware,
798
+ }
799
+ if interrupt_on is not None:
800
+ gp_subagent["interrupt_on"] = {}
801
+ compiled_subagents.append(cast(SubAgent, gp_subagent))
802
+ if gp_name not in subagent_by_name:
803
+ subagent_by_name[gp_name] = cast(
804
+ SubagentMetadata,
805
+ {
806
+ "name": gp_name,
807
+ "description": gp_description,
808
+ "system_prompt": gp_system_prompt,
809
+ "is_plugin": False,
810
+ },
811
+ )
812
+ logger.info(
813
+ "Compiled subagent '%s'",
814
+ gp_name,
815
+ extra={"subagent": gp_name},
816
+ )
817
+
818
+
819
+ # 9. Build system prompt
820
+ effective_prompt = system_prompt or get_base_system_prompt(
821
+ assistant_id=assistant_id,
822
+ interactive=interactive,
823
+ cwd=effective_cwd,
824
+ fs_tools=fs_tools,
825
+ model_name=model_name,
826
+ model_provider=model_provider,
827
+ model_context_limit=model_context_limit,
828
+ model_unsupported_modalities=model_unsupported_modalities,
829
+ cloud_provider=cloud_provider or getattr(settings, "cloud_provider", "aws"),
830
+ sandbox_type=getattr(settings, "sandbox_provider", "local"),
831
+ has_search=any(getattr(t, "name", None) == "web_search" for t in all_tools),
832
+ )
833
+
834
+ # 10. Build ordered middleware stack (matching OpsCode execution order)
835
+ agent_middleware: list[AgentMiddleware[Any, Any]] = []
836
+
837
+ # 10.1 ConfigurableModelMiddleware, Jev Dynamic Model Router & Model Retry
838
+ agent_middleware.append(ConfigurableModelMiddleware())
839
+ agent_middleware.append(JevDynamicModelRouterMiddleware(subagent_name=None))
840
+ agent_middleware.append(CodeModelRetryMiddleware())
841
+
842
+ # 10.2 Non-interactive guards
843
+ if not interactive:
844
+ agent_middleware.append(GlmTerminalStallRecoveryMiddleware())
845
+ if mcp_tools_list and (gated_names := gated_mcp_tool_names(mcp_tools_list)):
846
+ agent_middleware.append(HeadlessMCPGuardMiddleware(gated_names))
847
+
848
+ # 10.3 ResumeState, CostTracking, GoalTools
849
+ agent_middleware.extend([ResumeStateMiddleware(), CostTrackingMiddleware(), GoalToolsMiddleware()])
850
+
851
+ # 10.4 AskUserMiddleware
852
+ if enable_ask_user and interactive:
853
+ agent_middleware.append(AskUserMiddleware())
854
+
855
+ # 10.5 MCP tool arguments and error handling
856
+ if mcp_tools_list:
857
+ if cached_infos or mcp_configs:
858
+ from opscloud.middleware.mcp_context import MCPContextMiddleware
859
+
860
+ agent_middleware.append(
861
+ MCPContextMiddleware(mcp_server_info=cached_infos, mcp_config=mcp_configs)
862
+ )
863
+ agent_middleware.append(MCPToolMiddleware())
864
+
865
+ # 10.5 MemoryMiddleware & ManagedMemoryGuardMiddleware
866
+ if memory_sources_str:
867
+ agent_middleware.append(
868
+ MemoryMiddleware(
869
+ backend=FilesystemBackend(virtual_mode=False),
870
+ sources=memory_sources_str,
871
+ system_prompt=get_memory_system_prompt(headless=not interactive),
872
+ )
873
+ )
874
+ agent_middleware.append(ManagedMemoryGuardMiddleware(guarded_paths=memory_sources_str))
875
+
876
+ # 10.6 PluginSkillsMiddleware (resolved via canonical dcode-aligned get_skill_sources)
877
+ from opscloud.skills.sources import get_skill_sources
878
+
879
+ coord_skill_sources = get_skill_sources(
880
+ assistant_id=assistant_id,
881
+ project_root=project_scope_root,
882
+ project_context=project_context,
883
+ include_subagent_skills=False,
884
+ store=config_store or store,
885
+ )
886
+ if coord_skill_sources:
887
+ agent_middleware.append(
888
+ PluginSkillsMiddleware(
889
+ backend=composite_backend,
890
+ sources=coord_skill_sources,
891
+ project_root=project_scope_root,
892
+ )
893
+ )
894
+
895
+ # 10.7 CodeInterpreterMiddleware (if enabled)
896
+ if enable_interpreter:
897
+ try:
898
+ from langchain_core._api import suppress_langchain_beta_warning
899
+ from langchain_quickjs import CodeInterpreterMiddleware, PTCOption
900
+
901
+ ptc_names = _resolve_ptc_option(
902
+ getattr(settings, "interpreter_ptc", "safe"),
903
+ tools=all_tools,
904
+ acknowledge_unsafe=getattr(settings, "interpreter_ptc_acknowledge_unsafe", False),
905
+ auto_approve=auto_approve,
906
+ )
907
+ ptc_option: PTCOption | None = cast(PTCOption, list(ptc_names)) if ptc_names is not None else None
908
+
909
+ with suppress_langchain_beta_warning():
910
+ agent_middleware.append(
911
+ CodeInterpreterMiddleware(
912
+ tool_name="js_eval",
913
+ timeout=getattr(settings, "interpreter_timeout_seconds", 30),
914
+ memory_limit=getattr(settings, "interpreter_memory_limit_mb", 64) * 1024 * 1024,
915
+ max_ptc_calls=getattr(settings, "interpreter_max_ptc_calls", 50),
916
+ max_result_chars=getattr(settings, "interpreter_max_result_chars", 50000),
917
+ ptc=ptc_option,
918
+ subagents=True,
919
+ )
920
+ )
921
+ except ImportError:
922
+ logger.warning("langchain-quickjs is not installed. CodeInterpreterMiddleware disabled.")
923
+
924
+ # 10.8 LocalContextMiddleware
925
+ from opscloud.config.langsmith import get_langsmith_project_name
926
+
927
+ tracing_project_name = get_langsmith_project_name()
928
+ agent_middleware.append(
929
+ LocalContextMiddleware(
930
+ backend=composite_backend,
931
+ working_dir=effective_cwd,
932
+ tracing_project=tracing_project_name,
933
+ user_tracing_project=getattr(settings, "user_langchain_project", tracing_project_name),
934
+ )
935
+ )
936
+
937
+ # 10.9 ShellAllowListMiddleware
938
+ allow_list = getattr(settings, "shell_allow_list", None) or []
939
+ if allow_list:
940
+ agent_middleware.append(ShellAllowListMiddleware(allow_list))
941
+
942
+ # 10.10 HITL approval (AutoModeHITLMiddleware / AsyncApprovalHITLMiddleware)
943
+ auto_mode_config: tuple[Path, list[str]] | None = None
944
+ if interrupt_on is not None and interactive and not auto_approve:
945
+ auto_mode_config = (Path(effective_cwd), list(allow_list))
946
+
947
+ if auto_mode_config is not None and interrupt_on is not None:
948
+ agent_middleware.append(
949
+ AutoModeHITLMiddleware(
950
+ interrupt_on,
951
+ worktree_root=auto_mode_config[0],
952
+ shell_allow_list=auto_mode_config[1],
953
+ )
954
+ )
955
+ elif interrupt_on is not None:
956
+ agent_middleware.append(AsyncApprovalHITLMiddleware(interrupt_on))
957
+
958
+ # 10.11 ServerHooksMiddleware
959
+ hooks_cwd = Path(effective_cwd) if effective_cwd else Path.cwd()
960
+ agent_middleware.append(ServerHooksMiddleware(cwd=hooks_cwd, mcp_tools=mcp_tools_list))
961
+
962
+ # 10.12 GoalCriteriaMiddleware
963
+ criteria_agent = None
964
+ fallback_agent = None
965
+ criteria_context_tools: list[Any] = list(goal_criteria_tools or ())
966
+ if not criteria_context_tools and mcp_tools_list:
967
+ criteria_context_tools = [
968
+ t
969
+ for t in mcp_tools_list
970
+ if (
971
+ isinstance(getattr(t, "metadata", None), dict)
972
+ and getattr(t, "metadata", {}).get("readOnlyHint") is True
973
+ )
974
+ ]
975
+
976
+ try:
977
+ from opscloud.skills.sources import get_skill_sources
978
+
979
+ criteria_skill_sources = get_skill_sources(
980
+ assistant_id=assistant_id,
981
+ project_root=project_scope_root,
982
+ project_context=project_context,
983
+ include_subagent_skills=True,
984
+ subagents=list(subagent_by_name.values()),
985
+ store=config_store or store,
986
+ )
987
+ criteria_backend = getattr(composite_backend, "default", composite_backend)
988
+ criteria_agent = create_goal_criteria_agent(
989
+ model=active_model,
990
+ repository_backend=criteria_backend,
991
+ repository_root=str(project_scope_root),
992
+ context_tools=criteria_context_tools,
993
+ subagent_metas=list(subagent_by_name.values()),
994
+ skill_sources=criteria_skill_sources,
995
+ )
996
+ except Exception as exc:
997
+ logger.warning(
998
+ "Failed to create goal criteria agent: %s; using fallback agent",
999
+ exc,
1000
+ )
1001
+
1002
+ try:
1003
+ fallback_agent = create_goal_criteria_fallback_agent(
1004
+ model=active_model,
1005
+ )
1006
+ except Exception as exc:
1007
+ logger.warning("Failed to create goal criteria fallback agent: %s", exc)
1008
+
1009
+ from opscloud.tools.goal_tools import set_active_criteria_agent
1010
+
1011
+ set_active_criteria_agent(criteria_agent, fallback_agent)
1012
+
1013
+ agent_middleware.append(
1014
+ GoalCriteriaMiddleware(
1015
+ criteria_agent=criteria_agent,
1016
+ fallback_agent=fallback_agent,
1017
+ )
1018
+ )
1019
+
1020
+ # 10.13 CLICompactionMiddleware
1021
+ agent_middleware.append(_create_cli_compaction_middleware(active_model, composite_backend))
1022
+
1023
+ # 10.14 ReliableRubricMiddleware
1024
+ rubric_grader_tools = _create_rubric_grader_tools(composite_backend, repository_root=str(project_scope_root))
1025
+ grader_middleware: list[AgentMiddleware[Any, Any]] = [
1026
+ _WebSearchBudgetMiddleware(),
1027
+ ]
1028
+ agent_middleware.append(
1029
+ ReliableRubricMiddleware(
1030
+ model=active_model,
1031
+ system_prompt=_RUBRIC_GRADER_SYSTEM_PROMPT,
1032
+ tools=rubric_grader_tools,
1033
+ grader_middleware=grader_middleware,
1034
+ grader_context_schema=CLIContextSchema,
1035
+ )
1036
+ )
1037
+
1038
+ # 10.15 SubagentsMiddleware
1039
+ agent_middleware.append(SubagentsMiddleware(subagent_metas=list(subagent_by_name.values())))
1040
+
1041
+ # 10.16 UnifiedSystemMessageMiddleware
1042
+ agent_middleware.append(UnifiedSystemMessageMiddleware())
1043
+
1044
+ # Ensure all middleware trace policies record full input state schemas in LangSmith
1045
+ from opscloud.config.langsmith import enable_full_middleware_tracing
1046
+
1047
+ enable_full_middleware_tracing()
1048
+
1049
+ # 11. Compile the agent graph
1050
+ from opscloud.subagents.loader import load_async_subagents
1051
+
1052
+ resolved_async_subagents: list[AsyncSubAgent] = []
1053
+ if async_subagents is not None:
1054
+ resolved_async_subagents = list(async_subagents)
1055
+ else:
1056
+ try:
1057
+ resolved_async_subagents = load_async_subagents(store=config_store or store)
1058
+ except Exception as exc:
1059
+ logger.warning("Failed to load async subagents: %s", exc)
1060
+ resolved_async_subagents = []
1061
+
1062
+ all_subagents: list[Any] = list(compiled_subagents)
1063
+ if resolved_async_subagents:
1064
+ all_subagents.extend(resolved_async_subagents)
1065
+
1066
+ from opscloud.state import OpsCloudAgentState
1067
+
1068
+ graph = create_deep_agent(
1069
+ model=active_model,
1070
+ system_prompt=effective_prompt,
1071
+ tools=all_tools,
1072
+ backend=composite_backend,
1073
+ subagents=all_subagents,
1074
+ middleware=agent_middleware,
1075
+ checkpointer=checkpointer,
1076
+ store=store,
1077
+ name=assistant_id,
1078
+ context_schema=CLIContextSchema,
1079
+ state_schema=OpsCloudAgentState,
1080
+ )
1081
+
1082
+ return graph, composite_backend
1083
+
1084
+
1085
+ __all__ = [
1086
+ "AgentContextSchema",
1087
+ "CLIContextSchema",
1088
+ "_resolve_ptc_option",
1089
+ "_subagent_cli_middleware",
1090
+ "create_opscloud_agent",
1091
+ ]
1092
+
1093
+
1094
+ create_agent = create_opscloud_agent
1095
+ create_k8s_autopilot_agent = create_opscloud_agent
1096
+
1097
+ __all__ = [
1098
+ 'create_opscloud_agent',
1099
+ 'create_agent',
1100
+ 'create_k8s_autopilot_agent',
1101
+ 'CLIContextSchema',
1102
+ 'AgentContextSchema',
1103
+ ]