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.
- opscloud/__init__.py +5 -0
- opscloud/__main__.py +6 -0
- opscloud/_constants.py +53 -0
- opscloud/_debug.py +145 -0
- opscloud/_debug_buffer.py +250 -0
- opscloud/_textual_patches.py +367 -0
- opscloud/_version.py +15 -0
- opscloud/agent/__init__.py +27 -0
- opscloud/agent/config.py +168 -0
- opscloud/agent/factory.py +1103 -0
- opscloud/approval_mode.py +44 -0
- opscloud/backend/__init__.py +49 -0
- opscloud/backend/composite.py +89 -0
- opscloud/backend/local.py +183 -0
- opscloud/backend/registry.py +79 -0
- opscloud/backend/sandbox_config.py +107 -0
- opscloud/backend/sandbox_factory.py +432 -0
- opscloud/built_in_skills/remember/SKILL.md +124 -0
- opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
- opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
- opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
- opscloud/cli/__init__.py +1 -0
- opscloud/cli/commands/__init__.py +15 -0
- opscloud/cli/commands/auth.py +45 -0
- opscloud/cli/commands/doctor.py +100 -0
- opscloud/cli/commands/mcp.py +137 -0
- opscloud/cli/commands/threads.py +51 -0
- opscloud/cli/interactive.py +154 -0
- opscloud/cli/main.py +903 -0
- opscloud/cli/non_interactive.py +425 -0
- opscloud/cli/server_manager.py +58 -0
- opscloud/commands/__init__.py +19 -0
- opscloud/commands/_base.py +110 -0
- opscloud/commands/_guards.py +52 -0
- opscloud/commands/_router.py +146 -0
- opscloud/commands/_types.py +36 -0
- opscloud/commands/core/__init__.py +47 -0
- opscloud/commands/core/auth.py +94 -0
- opscloud/commands/core/bug.py +46 -0
- opscloud/commands/core/clear.py +106 -0
- opscloud/commands/core/cloud.py +185 -0
- opscloud/commands/core/compact.py +128 -0
- opscloud/commands/core/config_cmd.py +120 -0
- opscloud/commands/core/context.py +220 -0
- opscloud/commands/core/cost.py +44 -0
- opscloud/commands/core/doctor.py +535 -0
- opscloud/commands/core/effort.py +108 -0
- opscloud/commands/core/exit_cmd.py +41 -0
- opscloud/commands/core/fast.py +148 -0
- opscloud/commands/core/help_cmd.py +94 -0
- opscloud/commands/core/mcp.py +134 -0
- opscloud/commands/core/model.py +109 -0
- opscloud/commands/core/permissions.py +196 -0
- opscloud/commands/core/plugins.py +92 -0
- opscloud/commands/core/pool.py +108 -0
- opscloud/commands/core/resume.py +97 -0
- opscloud/commands/core/skills.py +178 -0
- opscloud/commands/power/__init__.py +51 -0
- opscloud/commands/power/agents.py +98 -0
- opscloud/commands/power/btw.py +85 -0
- opscloud/commands/power/copy.py +97 -0
- opscloud/commands/power/goal.py +548 -0
- opscloud/commands/power/loop.py +295 -0
- opscloud/commands/power/memory.py +287 -0
- opscloud/commands/power/review.py +152 -0
- opscloud/commands/power/rubric.py +287 -0
- opscloud/commands/power/runtime.py +321 -0
- opscloud/commands/power/skill_creator.py +70 -0
- opscloud/commands/power/skill_invoke.py +165 -0
- opscloud/commands/power/tasks.py +209 -0
- opscloud/commands/power/trace.py +117 -0
- opscloud/commands/power/ui_toggles.py +135 -0
- opscloud/commands/power/version.py +52 -0
- opscloud/commands/registry.py +176 -0
- opscloud/config/__init__.py +114 -0
- opscloud/config/adapters/__init__.py +5 -0
- opscloud/config/adapters/postgres.py +776 -0
- opscloud/config/adapters/sqlite.py +769 -0
- opscloud/config/aws.py +248 -0
- opscloud/config/cloud_profiles.py +437 -0
- opscloud/config/env_vars.py +81 -0
- opscloud/config/langsmith.py +319 -0
- opscloud/config/manifest.py +817 -0
- opscloud/config/metadata.py +205 -0
- opscloud/config/paths.py +604 -0
- opscloud/config/plugins.py +130 -0
- opscloud/config/settings.py +834 -0
- opscloud/config/store.py +453 -0
- opscloud/config/store_factory.py +97 -0
- opscloud/config/toml_config.py +562 -0
- opscloud/editor.py +173 -0
- opscloud/exceptions.py +65 -0
- opscloud/file_ops.py +217 -0
- opscloud/hooks/__init__.py +139 -0
- opscloud/hooks/env.py +44 -0
- opscloud/hooks/interrupt.py +121 -0
- opscloud/hooks/models/__init__.py +109 -0
- opscloud/hooks/models/adapters.py +19 -0
- opscloud/hooks/models/config.py +79 -0
- opscloud/hooks/models/domain.py +434 -0
- opscloud/hooks/models/transport.py +37 -0
- opscloud/hooks/tools.py +231 -0
- opscloud/input.py +48 -0
- opscloud/integrations/__init__.py +80 -0
- opscloud/integrations/base.py +153 -0
- opscloud/integrations/content_filter.py +119 -0
- opscloud/integrations/event_bus.py +480 -0
- opscloud/integrations/hooks.py +238 -0
- opscloud/integrations/identity.py +181 -0
- opscloud/integrations/notifications.py +134 -0
- opscloud/integrations/stream_bridge.py +397 -0
- opscloud/integrations/thread_store.py +81 -0
- opscloud/json_types.py +20 -0
- opscloud/mcp/__init__.py +57 -0
- opscloud/mcp/config.py +203 -0
- opscloud/mcp/discovery.py +229 -0
- opscloud/mcp/mcp_info.py +109 -0
- opscloud/mcp/middleware.py +102 -0
- opscloud/mcp/preload.py +505 -0
- opscloud/mcp/raw_config.py +149 -0
- opscloud/mcp/semantic_profiler.py +305 -0
- opscloud/mcp/session_manager.py +727 -0
- opscloud/mcp/trust.py +105 -0
- opscloud/memory/__init__.py +27 -0
- opscloud/memory/branch.py +61 -0
- opscloud/memory/guard.py +19 -0
- opscloud/memory/onboarding.py +120 -0
- opscloud/memory/registry.py +134 -0
- opscloud/memory/store.py +223 -0
- opscloud/middleware/__init__.py +106 -0
- opscloud/middleware/_repository_bounds.py +459 -0
- opscloud/middleware/ask_user.py +152 -0
- opscloud/middleware/auto_mode.py +60 -0
- opscloud/middleware/auto_mode_hitl.py +2434 -0
- opscloud/middleware/compaction.py +566 -0
- opscloud/middleware/configurable_model.py +233 -0
- opscloud/middleware/cost_tracking.py +1021 -0
- opscloud/middleware/glm_stall_recovery.py +109 -0
- opscloud/middleware/goal_criteria.py +1234 -0
- opscloud/middleware/goal_state_notice.py +712 -0
- opscloud/middleware/goal_tools.py +407 -0
- opscloud/middleware/headless_mcp_guard.py +88 -0
- opscloud/middleware/jev_model_router.py +1018 -0
- opscloud/middleware/local_context.py +460 -0
- opscloud/middleware/mcp_context.py +151 -0
- opscloud/middleware/mcp_middleware.py +146 -0
- opscloud/middleware/memory_guard.py +393 -0
- opscloud/middleware/model_retry.py +205 -0
- opscloud/middleware/registry.py +137 -0
- opscloud/middleware/reliable_rubric.py +709 -0
- opscloud/middleware/resume_state.py +91 -0
- opscloud/middleware/server_hooks.py +1189 -0
- opscloud/middleware/shell_allow_list.py +90 -0
- opscloud/middleware/skills.py +382 -0
- opscloud/middleware/subagent_artifacts.py +84 -0
- opscloud/middleware/subagent_telemetry.py +261 -0
- opscloud/middleware/subagents.py +243 -0
- opscloud/middleware/tool_filter.py +384 -0
- opscloud/middleware/unified_system_message.py +62 -0
- opscloud/model/__init__.py +37 -0
- opscloud/model/config.py +2992 -0
- opscloud/model/factory.py +477 -0
- opscloud/model/pool.py +459 -0
- opscloud/model/reasoning.py +282 -0
- opscloud/offload.py +173 -0
- opscloud/output.py +64 -0
- opscloud/plugins/__init__.py +189 -0
- opscloud/plugins/_json.py +22 -0
- opscloud/plugins/adapters/__init__.py +13 -0
- opscloud/plugins/adapters/agents.py +112 -0
- opscloud/plugins/adapters/commands.py +190 -0
- opscloud/plugins/adapters/mcp.py +363 -0
- opscloud/plugins/adapters/skills.py +132 -0
- opscloud/plugins/commands_cli.py +541 -0
- opscloud/plugins/discovery.py +512 -0
- opscloud/plugins/manifest.py +335 -0
- opscloud/plugins/marketplace.py +893 -0
- opscloud/plugins/models.py +334 -0
- opscloud/plugins/project_plugins.py +317 -0
- opscloud/plugins/protocol.py +31 -0
- opscloud/plugins/store.py +905 -0
- opscloud/plugins/substitution.py +122 -0
- opscloud/project_utils.py +145 -0
- opscloud/prompts/__init__.py +37 -0
- opscloud/prompts/system.py +483 -0
- opscloud/prompts/templates/system_prompt.md +166 -0
- opscloud/prompts/types.py +54 -0
- opscloud/rubrics/__init__.py +42 -0
- opscloud/rubrics/evaluator.py +404 -0
- opscloud/rubrics/evidence_extractor.py +229 -0
- opscloud/rubrics/generator.py +239 -0
- opscloud/rubrics/jev_compiler.py +175 -0
- opscloud/rubrics/jev_grader.py +297 -0
- opscloud/rubrics/middleware.py +8 -0
- opscloud/schema/__init__.py +107 -0
- opscloud/schema/interrupts.py +981 -0
- opscloud/security/__init__.py +91 -0
- opscloud/security/approval_mode.py +528 -0
- opscloud/security/approval_mode_source.py +230 -0
- opscloud/security/cli_ast_evaluator.py +552 -0
- opscloud/security/jev_classifier.py +651 -0
- opscloud/security/shell_safety.py +340 -0
- opscloud/security/unicode_security.py +439 -0
- opscloud/security/url_validation.py +160 -0
- opscloud/server/__init__.py +13 -0
- opscloud/server/_server_config.py +158 -0
- opscloud/server/server.py +242 -0
- opscloud/server/server_graph.py +93 -0
- opscloud/skills/__init__.py +91 -0
- opscloud/skills/commands.py +1099 -0
- opscloud/skills/invocation.py +200 -0
- opscloud/skills/load.py +388 -0
- opscloud/skills/loader.py +23 -0
- opscloud/skills/merge.py +65 -0
- opscloud/skills/registry.py +263 -0
- opscloud/skills/sources.py +174 -0
- opscloud/skills/trust.py +417 -0
- opscloud/state/__init__.py +142 -0
- opscloud/state/base.py +154 -0
- opscloud/state/cloud_context.py +262 -0
- opscloud/state/goal_channels.py +124 -0
- opscloud/state/goal_state_limits.py +175 -0
- opscloud/state/resume_state.py +58 -0
- opscloud/state/service_context.py +281 -0
- opscloud/state/session.py +957 -0
- opscloud/state/state_migration.py +86 -0
- opscloud/subagents/__init__.py +30 -0
- opscloud/subagents/loader.py +331 -0
- opscloud/subagents/subagents_parser.py +117 -0
- opscloud/subagents/types.py +25 -0
- opscloud/tools/__init__.py +36 -0
- opscloud/tools/catalog.py +37 -0
- opscloud/tools/display.py +24 -0
- opscloud/tools/fetch_url.py +176 -0
- opscloud/tools/goal_tools.py +423 -0
- opscloud/tools/registry.py +105 -0
- opscloud/tools/thread.py +32 -0
- opscloud/tools/web_search.py +215 -0
- opscloud/ui/__init__.py +119 -0
- opscloud/ui/app.py +5400 -0
- opscloud/ui/app.tcss +479 -0
- opscloud/ui/clipboard.py +147 -0
- opscloud/ui/command_registry.py +482 -0
- opscloud/ui/message_store.py +152 -0
- opscloud/ui/permission_store.py +360 -0
- opscloud/ui/preferences.py +113 -0
- opscloud/ui/prompt_manager.py +96 -0
- opscloud/ui/remote_client.py +505 -0
- opscloud/ui/textual_adapter.py +1269 -0
- opscloud/ui/theme.py +728 -0
- opscloud/ui/ui_help.py +415 -0
- opscloud/ui/widgets/__init__.py +383 -0
- opscloud/ui/widgets/_ask_user_types.py +81 -0
- opscloud/ui/widgets/_copy_spans.py +52 -0
- opscloud/ui/widgets/_inline_prompt.py +290 -0
- opscloud/ui/widgets/_js_eval_display.py +139 -0
- opscloud/ui/widgets/_links.py +175 -0
- opscloud/ui/widgets/_tool_stream.py +308 -0
- opscloud/ui/widgets/agent_selector.py +297 -0
- opscloud/ui/widgets/approval.py +741 -0
- opscloud/ui/widgets/ask_user.py +539 -0
- opscloud/ui/widgets/auth.py +695 -0
- opscloud/ui/widgets/auth_manager.py +214 -0
- opscloud/ui/widgets/auto_mode_notice.py +146 -0
- opscloud/ui/widgets/autocomplete.py +709 -0
- opscloud/ui/widgets/chat_input.py +805 -0
- opscloud/ui/widgets/cloud_selector.py +528 -0
- opscloud/ui/widgets/config_manager.py +652 -0
- opscloud/ui/widgets/debug_console.py +1269 -0
- opscloud/ui/widgets/devops_renderers.py +184 -0
- opscloud/ui/widgets/diff.py +237 -0
- opscloud/ui/widgets/effort_selector.py +166 -0
- opscloud/ui/widgets/goal_review.py +612 -0
- opscloud/ui/widgets/goal_status.py +57 -0
- opscloud/ui/widgets/infra_panel.py +141 -0
- opscloud/ui/widgets/install_confirm.py +114 -0
- opscloud/ui/widgets/loading.py +170 -0
- opscloud/ui/widgets/mcp_viewer.py +1204 -0
- opscloud/ui/widgets/messages.py +4461 -0
- opscloud/ui/widgets/model_selector.py +863 -0
- opscloud/ui/widgets/notification_center.py +108 -0
- opscloud/ui/widgets/notification_settings.py +119 -0
- opscloud/ui/widgets/operation_card.py +106 -0
- opscloud/ui/widgets/permissions_manager.py +667 -0
- opscloud/ui/widgets/plugin_manager.py +1212 -0
- opscloud/ui/widgets/pool_selector.py +376 -0
- opscloud/ui/widgets/preamble.py +53 -0
- opscloud/ui/widgets/skills_viewer.py +432 -0
- opscloud/ui/widgets/status.py +1037 -0
- opscloud/ui/widgets/subagent_panel.py +927 -0
- opscloud/ui/widgets/theme_selector.py +232 -0
- opscloud/ui/widgets/thread_selector.py +358 -0
- opscloud/ui/widgets/toast.py +31 -0
- opscloud/ui/widgets/tool_display.py +325 -0
- opscloud/ui/widgets/tool_renderers.py +234 -0
- opscloud/ui/widgets/tool_widgets.py +278 -0
- opscloud/ui/widgets/welcome.py +421 -0
- opscloud/ui/widgets/welcome_popup.py +102 -0
- opscloud/utils/__init__.py +24 -0
- opscloud/utils/cost_estimation.py +169 -0
- opscloud/utils/git.py +390 -0
- opscloud/utils/logger.py +571 -0
- opscloud/utils/session_stats.py +115 -0
- opscloud/utils/startup_error.py +37 -0
- talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
- talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
- talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
- talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|