Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
6baa994
fix: video gen exclude edit_file
Feb 6, 2026
9c8205c
Merge branch 'main' of https://github.com/modelscope/ms-agent
Feb 9, 2026
94b833f
Merge branch 'main' of https://github.com/modelscope/ms-agent
Mar 5, 2026
11bcf06
enhance deep research v2
Mar 11, 2026
dd3f185
Merge branch 'main' of https://github.com/modelscope/ms-agent
Mar 12, 2026
693d9e9
feat: support search local paths through sirchmunk
Mar 15, 2026
05cb676
refactor: optimize architecture, restrict researcher report edits, up…
Mar 15, 2026
2977ba0
Update ms_agent/agent/llm_agent.py
suluyana Mar 16, 2026
879dba4
Apply suggestion from @gemini-code-assist[bot]
suluyana Mar 16, 2026
1dd49b6
fix local code executor; refine workflow and prompt (03)
Mar 17, 2026
c27347f
fix timeout; support for running subagent in process; support for pos…
Mar 19, 2026
5920dc4
refine readme for deep research; add run_benchmark.sh; fix counting c…
Mar 20, 2026
14cb873
Merge branch 'main' of https://github.com/modelscope/ms-agent into fe…
Mar 20, 2026
5202823
full modify?
Mar 20, 2026
24eb500
fix lint
Mar 20, 2026
986b34f
enrich researcher's reflection strategy to enhance stability
Mar 23, 2026
e6ad3e9
Merge branch 'main' of https://github.com/modelscope/ms-agent
Mar 24, 2026
88aaae6
Merge remote-tracking branch 'origin' into feat/tools
Mar 24, 2026
fec8bfe
mv localsearch to tools
Mar 24, 2026
b437bdc
thinking support beta; search_file_content fix
Mar 25, 2026
86a3ba8
support API key pool construction; support for reasoning model
Mar 27, 2026
8a32ce5
fix lint
Mar 27, 2026
393f23c
support for vertex type anthropic llm; refine reasoning output
Mar 27, 2026
49919ab
Merge branch 'main' of https://github.com/modelscope/ms-agent
Mar 31, 2026
e00c296
feat: add snapshot/rollback system to agent runtime
Apr 2, 2026
f08a15e
support for response api; optimize log output formatting
Apr 2, 2026
6bfb262
feat: merge SplitTask into AgentTool, add TaskManager infrastructure
Apr 2, 2026
db45457
feat: add run_in_background support to AgentTool
Apr 2, 2026
47bf056
feat: add TaskControlTool for LLM-accessible task management
Apr 3, 2026
e3b62dc
fix: hold strong reference to background watcher asyncio.Task to prev…
Apr 3, 2026
6286732
fix: cancel watcher tasks on AgentTool cleanup; export TaskControlToo…
Apr 3, 2026
c326139
fix: correct malformed XML in TaskManager._format_notification
Apr 3, 2026
b103ffa
test: add smoke tests for TaskManager, AgentTool dynamic spec, TaskCo…
Apr 3, 2026
7a96f8e
feat: stream sub-agent execution trace to file in real time
Apr 9, 2026
5825111
Merge branch 'main' of https://github.com/modelscope/ms-agent into fe…
Apr 9, 2026
6a1ba9f
fix lint
Apr 9, 2026
c6b1e3b
Merge branch 'main' of https://github.com/modelscope/ms-agent
Apr 10, 2026
2f2dbb0
Merge branch 'main' of https://github.com/modelscope/ms-agent
Apr 13, 2026
193e4a1
Merge remote-tracking branch 'origin' into feat/git
Apr 13, 2026
b3feb1a
feat: workspace policy, shell artifacts, TaskManager, grep/glob tools
Apr 13, 2026
ebbc72a
chore(projects): align configs with filesystem and workspace search t…
Apr 13, 2026
94ba0c3
refactor(tools): merge grep/glob into FileSystemTool
Apr 13, 2026
9841444
Merge branch 'feat/dr_reasoning' of https://github.com/alcholiclg/ms-…
Apr 13, 2026
ec5fb1a
feat(search): add Tavily engine, extract fetcher, and dr_bench wiring
Apr 20, 2026
cb75af3
fix(jina): align reader with websearch (meta fetch + playwright fallb…
Apr 20, 2026
87ae4ef
Merge origin/feat/git into bench/tavily-0413 (snapshot utils; keep be…
Apr 21, 2026
18a5683
chore(tools): use origin/feat/git filesystem_tool on bench/tavily-0413
Apr 21, 2026
e59618d
Merge feat/git into bench/tavily-0413; resolve prompts/reporter_callb…
Apr 22, 2026
ecf350b
fix(dr v2): align tavily yaml file_system include with grep/glob/edit…
Apr 22, 2026
7b49a52
Merge feat/agent-tool-overhaul into bench/tavily-0413
Apr 23, 2026
a5d4267
fix(filesystem): drop read-cache staleness gate for writes
Apr 23, 2026
00c8e86
fix(deep-research): harden tools, snapshots, and reporter/searcher co…
Apr 27, 2026
e5890f6
Merge branch 'bench/tavily-0413' into feat/tools
Apr 27, 2026
67668e9
fix lint
Apr 28, 2026
2d96611
Revert "fix lint"
Apr 28, 2026
34d5e92
fix lint
Apr 28, 2026
9e3585e
fix timeout & local code exec
May 28, 2026
fb5be5c
fix: make MCP import conditional for Docker environments
May 28, 2026
2c7b450
fix: guard reindex_tool against None servers when MCP unavailable
May 28, 2026
d893b71
feat: add dual-layer permission control with unified workspace root
Jun 11, 2026
cad1074
feat: add shell-based hooks system with multi-platform config loading
Jun 15, 2026
770baa1
feat: add MCP runtime management with layered config and ToolManager …
Jun 16, 2026
d08c1f7
Merge upstream/main into feat/new_playground_part
Jun 16, 2026
c4de21a
Merge upstream/main into feat/new_playground_part
Jun 18, 2026
3328ec0
feat(plugins): add community plugin system with hookify E2E valid…
Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,419 changes: 2,419 additions & 0 deletions docs/zh/design/hooks-design.md

Large diffs are not rendered by default.

1,101 changes: 1,101 additions & 0 deletions docs/zh/design/mcp_runtime_management.md

Large diffs are not rendered by default.

1,584 changes: 1,584 additions & 0 deletions docs/zh/design/permission-design.md

Large diffs are not rendered by default.

1,763 changes: 1,763 additions & 0 deletions docs/zh/design/plugins-design.md

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions ms_agent/agent/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

from ms_agent.llm import Message
from ms_agent.utils import read_history, save_history
from ms_agent.utils.constants import DEFAULT_OUTPUT_DIR, DEFAULT_RETRY_COUNT
from ms_agent.utils.constants import DEFAULT_RETRY_COUNT
from ms_agent.utils.workspace_context import resolve_workspace_root


class Agent(ABC):
Expand Down Expand Up @@ -42,8 +43,14 @@ def __init__(self,
self.trust_remote_code = trust_remote_code
self.config.tag = tag
self.config.trust_remote_code = trust_remote_code
self.output_dir = getattr(self.config, 'output_dir',
DEFAULT_OUTPUT_DIR)
workspace_root = resolve_workspace_root(self.config)
self.output_dir = str(workspace_root)
try:
from omegaconf import open_dict
with open_dict(self.config):
self.config.output_dir = self.output_dir
except Exception:
pass

@abstractmethod
async def run(
Expand Down
230 changes: 222 additions & 8 deletions ms_agent/agent/llm_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import inspect
import json
import os.path
from pathlib import Path
import sys
import threading
import uuid
Expand Down Expand Up @@ -148,6 +149,7 @@ def __init__(
self.mcp_config: Dict[str, Any] = self.parse_mcp_servers(
kwargs.get('mcp_config', {}))
self.mcp_client = kwargs.get('mcp_client', None)
self.mcp_runtime = kwargs.get('mcp_runtime', None)
self.config_handler = self.register_config_handler()

# Skill system (initialized in prepare_skills)
Expand All @@ -157,6 +159,7 @@ def __init__(

# Skill runtime (initialized in prepare_skills)
self._skill_runtime: Optional[SkillRuntime] = None
self._plugin_runtime = None

# Slash-command router for interactive input (lazily built)
self._command_router = None
Expand Down Expand Up @@ -227,6 +230,9 @@ async def prepare_skills(self):
self._skill_runtime.set_system_content_builder(
self._build_system_content
)
if getattr(self, '_plugin_runtime', None) is not None:
self._plugin_runtime.skill_runtime = self._skill_runtime
self._plugin_runtime._sync_skill_runtime(self.config)

def _build_system_content(self) -> str:
"""Build the full system prompt content.
Expand Down Expand Up @@ -488,7 +494,32 @@ async def on_tool_call(self, messages: List[Message]):
await self.loop_callback('on_tool_call', messages)

async def after_tool_call(self, messages: List[Message]):
if messages[-1].role == 'assistant' and not messages[-1].tool_calls:
assistant = messages[-1]
would_stop = assistant.role == 'assistant' and not assistant.tool_calls

hook_runtime = getattr(self, '_hook_runtime', None)
if would_stop and hook_runtime is not None and not hook_runtime.is_empty:
from ms_agent.hooks.context import (
append_stop_blocking_feedback,
apply_hook_result_to_messages,
)

last_text = assistant.content if isinstance(assistant.content, str) else ''
stop = await hook_runtime.run_stop(
reason='no_tool_calls',
last_assistant_message=last_text,
stop_hook_active=getattr(self.runtime, 'stop_hook_active', False),
)
if stop.action in ('block', 'deny'):
append_stop_blocking_feedback(messages, stop.reason)
self.runtime.should_stop = False
self.runtime.stop_hook_active = True
await self.loop_callback('after_tool_call', messages)
return
apply_hook_result_to_messages(
messages, stop, hook_event='Stop')

if would_stop:
self.runtime.should_stop = True
await self.loop_callback('after_tool_call', messages)

Expand Down Expand Up @@ -527,6 +558,7 @@ async def parallel_tool_call(self,
name=tool_call_query['tool_name'],
resources=tool_call_result_format.resources,
tool_detail=tool_call_result_format.tool_detail,
hook_attachments=tool_call_result_format.hook_attachments,
)

if _new_message.tool_call_id is None:
Expand All @@ -537,16 +569,132 @@ async def parallel_tool_call(self,
self.log_output(_new_message.content)
return messages

def _build_permission_objects(self):
"""Create SafetyGuard and PermissionEnforcer from config if configured."""
from ms_agent.permission import (
AutoPermissionHandler,
PermissionConfig,
PermissionEnforcer,
PermissionMemory,
SafetyGuard,
)
from ms_agent.permission.config import SafetyConfig

raw = {}
if hasattr(self.config, 'permission'):
raw = dict(self.config.permission) if self.config.permission else {}

from ms_agent.utils.workspace_context import resolve_workspace_root

workspace_root = str(resolve_workspace_root(self.config))
perm_config = PermissionConfig.from_dict(raw, project_root=workspace_root)

allowed_dirs = [workspace_root]
for directory in perm_config.safety.allowed_directories:
if directory not in allowed_dirs:
allowed_dirs.append(directory)
read_only_dirs = list(perm_config.safety.read_only_directories)
safety_guard = SafetyGuard(
config=perm_config.safety,
allowed_dirs=allowed_dirs,
read_only_dirs=read_only_dirs,
workspace_root=workspace_root,
)

handler = AutoPermissionHandler()
memory = PermissionMemory(project_path=workspace_root)
enforcer = PermissionEnforcer(config=perm_config, handler=handler, memory=memory)

return safety_guard, enforcer, perm_config

async def prepare_tools(self):
"""Initialize and connect the tool manager."""
import uuid

from ms_agent.hooks.bridge import CallbackToHookBridge
from ms_agent.hooks.factory import build_hook_runtime
from ms_agent.plugins.runtime import PluginRuntime
from ms_agent.utils.workspace_context import resolve_workspace_root

self.task_manager = TaskManager()

safety_guard, permission_enforcer, perm_config = self._build_permission_objects()
session_id = (
self.runtime.session_id
or getattr(self, 'tag', None)
or str(uuid.uuid4())
)
raw_hooks = {}
if hasattr(self.config, 'hooks') and self.config.hooks:
raw_hooks = OmegaConf.to_container(self.config.hooks, resolve=True) or {}
enabled_executors = frozenset(
raw_hooks.get('enabled_executors', ['command']) or ['command'])
self._plugin_runtime = PluginRuntime()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The PluginRuntime is initialized without passing the existing _skill_runtime or mcp_runtime. If prepare_skills runs before prepare_tools, the plugin runtime's skill synchronization will be skipped because self.skill_runtime is still None when start_sync is called. Pass skill_runtime=self._skill_runtime and mcp_runtime=self.mcp_runtime to ensure proper synchronization.

        self._plugin_runtime = PluginRuntime(
            skill_runtime=self._skill_runtime,
            mcp_runtime=self.mcp_runtime,
        )

self._plugin_runtime.start_sync(
str(resolve_workspace_root(self.config)),
session_id,
config=self.config,
enabled_executors=enabled_executors,
)
self._register_plugin_commands()
plugin_mcp_servers = self._plugin_runtime.load_result.mcp_servers
if plugin_mcp_servers:
from ms_agent.plugins.runtime import dedupe_mcp_server_names
plugin_mcp_servers = dedupe_mcp_server_names(
plugin_mcp_servers,
set(self.mcp_config.setdefault('mcpServers', {}).keys()),
)
self._plugin_runtime.load_result.mcp_servers = plugin_mcp_servers
self.mcp_config['mcpServers'].update(plugin_mcp_servers)
hook_runtime = build_hook_runtime(
self.config,
session_id=session_id,
plugin_hook_registries=self._plugin_runtime.load_result.hook_registries,
)
mcp_rt = self.mcp_runtime
if mcp_rt is not None and plugin_mcp_servers:
from ms_agent.config.mcp_schema import ResolvedMCPConfig
merged_servers = {
state.name: dict(state.config)
for state in mcp_rt.list_servers()
}
merged_servers.update(plugin_mcp_servers)
await mcp_rt.apply_config(
ResolvedMCPConfig(mcp_servers=merged_servers))

self.tool_manager = ToolManager(
self.config,
self.mcp_config,
self.mcp_config if mcp_rt is None else {},
self.mcp_client,
permission_enforcer=permission_enforcer,
safety_guard=safety_guard,
permission_mode=perm_config.mode,
read_policy=perm_config.safety.read_policy,
hook_runtime=hook_runtime,
permission_config=perm_config,
trust_remote_code=self.trust_remote_code,
mcp_callable_check=mcp_rt.is_callable if mcp_rt else None,
mcp_failure_handler=mcp_rt.record_failure if mcp_rt else None,
mcp_unavailable_detail=mcp_rt.unavailable_detail if mcp_rt else None,
mcp_success_handler=mcp_rt.record_success if mcp_rt else None,
)
if mcp_rt is not None:
self.tool_manager._skip_mcp_reindex = True
if self._plugin_runtime.agent_registry.has_agents():
self.tool_manager.ensure_plugin_agent_tools(
self._plugin_runtime.agent_registry,
)
if hook_runtime.has_session_handlers:
self.register_callback(CallbackToHookBridge(self.config, hook_runtime))
self._hook_runtime = hook_runtime
if not self.runtime.session_id:
self.runtime.session_id = hook_runtime.session_id
if mcp_rt is not None and not mcp_rt.is_started:
await mcp_rt.start()
await self.tool_manager.connect()
if mcp_rt is not None:
mcp_rt.bind_tool_manager(self.tool_manager)
await mcp_rt.sync_tools()
for tool in self.tool_manager.extra_tools:
if hasattr(tool, 'set_task_manager'):
tool.set_task_manager(self.task_manager)
Expand All @@ -555,6 +703,8 @@ async def cleanup_tools(self):
"""Cleanup resources used by the tool manager."""
if self.task_manager is not None:
self.task_manager.kill_all()
if self.mcp_runtime is not None:
await self.mcp_runtime.stop()
if self.tool_manager is not None:
await self.tool_manager.cleanup()

Expand Down Expand Up @@ -646,8 +796,18 @@ def _get_command_router(self):
router = CommandRouter()
register_builtin_commands(router)
self._command_router = router
self._register_plugin_commands()
return self._command_router

def _register_plugin_commands(self) -> None:
if self._command_router is None or self._plugin_runtime is None:
return
from ms_agent.plugins.commands import register_plugin_commands
register_plugin_commands(
self._command_router,
self._plugin_runtime.load_result.command_defs,
)

def _resolve_interactive(self, messages) -> bool:
"""Decide whether this run is an interactive session.

Expand Down Expand Up @@ -709,11 +869,7 @@ def _build_personalization_section(self) -> str:
return PersonalizationInjector.build(config)

async def do_rag(self, messages: List[Message]):
"""Process RAG or knowledge search to enrich the user query with context.

This method handles both traditional RAG and sirchmunk-based knowledge search.
For knowledge search, it also populates searching_detail and search_result
fields in the message for frontend display and next-turn LLM context.
"""Process RAG to enrich the user query with context.

Args:
messages (List[Message]): The message list to process.
Expand Down Expand Up @@ -885,6 +1041,35 @@ async def step(
"""
messages = deepcopy(messages)
messages = self._append_task_notifications(messages)
from ms_agent.hooks.context import (
condense_hook_attachments_for_llm,
extract_latest_user_prompt,
apply_hook_result_to_messages,
)
messages = condense_hook_attachments_for_llm(messages)

# UserPromptSubmit for multi-turn user input (InputCallback path)
hook_runtime = getattr(self, '_hook_runtime', None)
if (hook_runtime is not None and not hook_runtime.is_empty
and messages and messages[-1].role == 'user'
and self.runtime.round > 0):
prompt_text = extract_latest_user_prompt(messages)
submit = await hook_runtime.run_user_prompt_submit(prompt_text)
if submit.action in ('deny', 'block'):
if messages and messages[-1].role == 'user':
messages.pop()
messages.append(Message(
role='system',
content=(
f'UserPromptSubmit operation blocked by hook:\n'
f'{submit.reason}\n\nOriginal prompt: {prompt_text}'),
))
self.runtime.should_stop = True
yield messages
return
apply_hook_result_to_messages(
messages, submit, hook_event='UserPromptSubmit')

if (not self.load_cache) or messages[-1].role != 'assistant':
messages = await self.condense_memory(messages)
await self.on_generate_response(messages)
Expand Down Expand Up @@ -1203,9 +1388,38 @@ async def run_loop(self, messages: Union[List[Message], str],

if self.runtime.round == 0:
messages = await self.create_messages(messages)
await self.do_rag(messages)

hook_runtime = getattr(self, '_hook_runtime', None)
if hook_runtime is not None:
hook_runtime.session_id = self.runtime.session_id

# SessionStart before UserPromptSubmit (§9.3)
await self.on_task_begin(messages)

# UserPromptSubmit — first user message
if hook_runtime is not None and not hook_runtime.is_empty:
from ms_agent.hooks.context import (
extract_latest_user_prompt,
apply_hook_result_to_messages,
)
prompt_text = extract_latest_user_prompt(messages)
submit = await hook_runtime.run_user_prompt_submit(prompt_text)
if submit.action in ('deny', 'block'):
messages.append(Message(
role='system',
content=(
f'UserPromptSubmit operation blocked by hook:\n'
f'{submit.reason}\n\nOriginal prompt: {prompt_text}'),
))
await self.on_task_end(messages)
yield messages
await self.cleanup_tools()
return
apply_hook_result_to_messages(
messages, submit, hook_event='UserPromptSubmit')

await self.do_rag(messages)

for message in messages:
if message.role != 'system':
self.log_output('[' + message.role + ']:')
Expand Down
8 changes: 8 additions & 0 deletions ms_agent/agent/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,22 @@ class Runtime:

round: int = 0

stop_hook_active: bool = False

session_id: str = ''

def to_dict(self):
return {
'should_stop': self.should_stop,
'tag': self.tag,
'round': self.round,
'stop_hook_active': self.stop_hook_active,
'session_id': self.session_id,
}

def from_dict(self, data: dict):
self.should_stop = data['should_stop']
self.tag = data['tag']
self.round = data['round']
self.stop_hook_active = data.get('stop_hook_active', False)
self.session_id = data.get('session_id', '')
2 changes: 2 additions & 0 deletions ms_agent/cli/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import argparse

from ms_agent.cli.app import AppCMD
from ms_agent.cli.plugin import PluginCMD
from ms_agent.cli.run import RunCMD
from ms_agent.cli.ui import UICMD

Expand All @@ -20,6 +21,7 @@ def run_cmd():
RunCMD.define_args(subparsers)
AppCMD.define_args(subparsers)
UICMD.define_args(subparsers)
PluginCMD.define_args(subparsers)

# unknown args will be handled in config.py
args, _ = parser.parse_known_args()
Expand Down
Loading
Loading