EvoScientist 0.0.1b0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. evoscientist-0.0.1b0/EvoScientist/EvoScientist.py +336 -0
  2. evoscientist-0.0.1b0/EvoScientist/__init__.py +74 -0
  3. evoscientist-0.0.1b0/EvoScientist/__main__.py +4 -0
  4. evoscientist-0.0.1b0/EvoScientist/backends.py +405 -0
  5. evoscientist-0.0.1b0/EvoScientist/channels/__init__.py +44 -0
  6. evoscientist-0.0.1b0/EvoScientist/channels/base.py +1074 -0
  7. evoscientist-0.0.1b0/EvoScientist/channels/bus/__init__.py +6 -0
  8. evoscientist-0.0.1b0/EvoScientist/channels/bus/events.py +52 -0
  9. evoscientist-0.0.1b0/EvoScientist/channels/bus/message_bus.py +96 -0
  10. evoscientist-0.0.1b0/EvoScientist/channels/capabilities.py +220 -0
  11. evoscientist-0.0.1b0/EvoScientist/channels/channel_manager.py +1011 -0
  12. evoscientist-0.0.1b0/EvoScientist/channels/config.py +126 -0
  13. evoscientist-0.0.1b0/EvoScientist/channels/consumer.py +414 -0
  14. evoscientist-0.0.1b0/EvoScientist/channels/dingtalk/__init__.py +29 -0
  15. evoscientist-0.0.1b0/EvoScientist/channels/dingtalk/channel.py +354 -0
  16. evoscientist-0.0.1b0/EvoScientist/channels/dingtalk/probe.py +33 -0
  17. evoscientist-0.0.1b0/EvoScientist/channels/dingtalk/serve.py +92 -0
  18. evoscientist-0.0.1b0/EvoScientist/channels/discord/__init__.py +19 -0
  19. evoscientist-0.0.1b0/EvoScientist/channels/discord/channel.py +255 -0
  20. evoscientist-0.0.1b0/EvoScientist/channels/discord/probe.py +33 -0
  21. evoscientist-0.0.1b0/EvoScientist/channels/discord/serve.py +93 -0
  22. evoscientist-0.0.1b0/EvoScientist/channels/email/__init__.py +41 -0
  23. evoscientist-0.0.1b0/EvoScientist/channels/email/channel.py +380 -0
  24. evoscientist-0.0.1b0/EvoScientist/channels/email/probe.py +84 -0
  25. evoscientist-0.0.1b0/EvoScientist/channels/email/serve.py +124 -0
  26. evoscientist-0.0.1b0/EvoScientist/channels/feishu/__init__.py +22 -0
  27. evoscientist-0.0.1b0/EvoScientist/channels/feishu/channel.py +825 -0
  28. evoscientist-0.0.1b0/EvoScientist/channels/feishu/probe.py +39 -0
  29. evoscientist-0.0.1b0/EvoScientist/channels/feishu/serve.py +113 -0
  30. evoscientist-0.0.1b0/EvoScientist/channels/formatter.py +287 -0
  31. evoscientist-0.0.1b0/EvoScientist/channels/imessage/__init__.py +42 -0
  32. evoscientist-0.0.1b0/EvoScientist/channels/imessage/channel_rpc.py +407 -0
  33. evoscientist-0.0.1b0/EvoScientist/channels/imessage/probe.py +106 -0
  34. evoscientist-0.0.1b0/EvoScientist/channels/imessage/rpc_client.py +235 -0
  35. evoscientist-0.0.1b0/EvoScientist/channels/imessage/serve.py +87 -0
  36. evoscientist-0.0.1b0/EvoScientist/channels/imessage/targets.py +232 -0
  37. evoscientist-0.0.1b0/EvoScientist/channels/middleware.py +837 -0
  38. evoscientist-0.0.1b0/EvoScientist/channels/mixins.py +324 -0
  39. evoscientist-0.0.1b0/EvoScientist/channels/plugin.py +226 -0
  40. evoscientist-0.0.1b0/EvoScientist/channels/qq/__init__.py +26 -0
  41. evoscientist-0.0.1b0/EvoScientist/channels/qq/channel.py +259 -0
  42. evoscientist-0.0.1b0/EvoScientist/channels/qq/probe.py +37 -0
  43. evoscientist-0.0.1b0/EvoScientist/channels/qq/serve.py +87 -0
  44. evoscientist-0.0.1b0/EvoScientist/channels/retry.py +122 -0
  45. evoscientist-0.0.1b0/EvoScientist/channels/signal/__init__.py +27 -0
  46. evoscientist-0.0.1b0/EvoScientist/channels/signal/channel.py +462 -0
  47. evoscientist-0.0.1b0/EvoScientist/channels/signal/probe.py +39 -0
  48. evoscientist-0.0.1b0/EvoScientist/channels/signal/serve.py +99 -0
  49. evoscientist-0.0.1b0/EvoScientist/channels/slack/__init__.py +20 -0
  50. evoscientist-0.0.1b0/EvoScientist/channels/slack/channel.py +291 -0
  51. evoscientist-0.0.1b0/EvoScientist/channels/slack/probe.py +48 -0
  52. evoscientist-0.0.1b0/EvoScientist/channels/slack/serve.py +99 -0
  53. evoscientist-0.0.1b0/EvoScientist/channels/standalone.py +142 -0
  54. evoscientist-0.0.1b0/EvoScientist/channels/telegram/__init__.py +17 -0
  55. evoscientist-0.0.1b0/EvoScientist/channels/telegram/channel.py +289 -0
  56. evoscientist-0.0.1b0/EvoScientist/channels/telegram/probe.py +32 -0
  57. evoscientist-0.0.1b0/EvoScientist/channels/telegram/serve.py +81 -0
  58. evoscientist-0.0.1b0/EvoScientist/channels/wechat/__init__.py +69 -0
  59. evoscientist-0.0.1b0/EvoScientist/channels/wechat/channel.py +865 -0
  60. evoscientist-0.0.1b0/EvoScientist/channels/wechat/crypto.py +187 -0
  61. evoscientist-0.0.1b0/EvoScientist/channels/wechat/probe.py +72 -0
  62. evoscientist-0.0.1b0/EvoScientist/channels/wechat/serve.py +139 -0
  63. evoscientist-0.0.1b0/EvoScientist/channels/wechat/verify_server.py +175 -0
  64. evoscientist-0.0.1b0/EvoScientist/cli/__init__.py +37 -0
  65. evoscientist-0.0.1b0/EvoScientist/cli/_app.py +51 -0
  66. evoscientist-0.0.1b0/EvoScientist/cli/_constants.py +39 -0
  67. evoscientist-0.0.1b0/EvoScientist/cli/agent.py +64 -0
  68. evoscientist-0.0.1b0/EvoScientist/cli/channel.py +482 -0
  69. evoscientist-0.0.1b0/EvoScientist/cli/clipboard.py +116 -0
  70. evoscientist-0.0.1b0/EvoScientist/cli/commands.py +595 -0
  71. evoscientist-0.0.1b0/EvoScientist/cli/interactive.py +771 -0
  72. evoscientist-0.0.1b0/EvoScientist/cli/mcp_ui.py +282 -0
  73. evoscientist-0.0.1b0/EvoScientist/cli/skills_cmd.py +86 -0
  74. evoscientist-0.0.1b0/EvoScientist/cli/tui_backends.py +61 -0
  75. evoscientist-0.0.1b0/EvoScientist/cli/tui_interactive.py +1826 -0
  76. evoscientist-0.0.1b0/EvoScientist/cli/tui_runtime.py +99 -0
  77. evoscientist-0.0.1b0/EvoScientist/cli/widgets/__init__.py +21 -0
  78. evoscientist-0.0.1b0/EvoScientist/cli/widgets/assistant_message.py +50 -0
  79. evoscientist-0.0.1b0/EvoScientist/cli/widgets/loading_widget.py +50 -0
  80. evoscientist-0.0.1b0/EvoScientist/cli/widgets/subagent_widget.py +199 -0
  81. evoscientist-0.0.1b0/EvoScientist/cli/widgets/system_message.py +20 -0
  82. evoscientist-0.0.1b0/EvoScientist/cli/widgets/thinking_widget.py +82 -0
  83. evoscientist-0.0.1b0/EvoScientist/cli/widgets/todo_widget.py +77 -0
  84. evoscientist-0.0.1b0/EvoScientist/cli/widgets/tool_call_widget.py +215 -0
  85. evoscientist-0.0.1b0/EvoScientist/cli/widgets/user_message.py +25 -0
  86. evoscientist-0.0.1b0/EvoScientist/config/__init__.py +38 -0
  87. evoscientist-0.0.1b0/EvoScientist/config/onboard.py +1926 -0
  88. evoscientist-0.0.1b0/EvoScientist/config/settings.py +407 -0
  89. evoscientist-0.0.1b0/EvoScientist/llm/__init__.py +23 -0
  90. evoscientist-0.0.1b0/EvoScientist/llm/models.py +205 -0
  91. evoscientist-0.0.1b0/EvoScientist/mcp/__init__.py +32 -0
  92. evoscientist-0.0.1b0/EvoScientist/mcp/client.py +695 -0
  93. evoscientist-0.0.1b0/EvoScientist/middleware/__init__.py +21 -0
  94. evoscientist-0.0.1b0/EvoScientist/middleware/memory.py +770 -0
  95. evoscientist-0.0.1b0/EvoScientist/middleware/tool_error_handler.py +70 -0
  96. evoscientist-0.0.1b0/EvoScientist/paths.py +81 -0
  97. evoscientist-0.0.1b0/EvoScientist/prompts.py +281 -0
  98. evoscientist-0.0.1b0/EvoScientist/sessions.py +345 -0
  99. evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/LICENSE.txt +201 -0
  100. evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/SKILL.md +211 -0
  101. evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/references/cli-compatibility.md +23 -0
  102. evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/references/known-issues.md +244 -0
  103. evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/references/mode-discussion.md +93 -0
  104. evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/references/mode-pipeline.md +83 -0
  105. evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/references/reliability.md +245 -0
  106. evoscientist-0.0.1b0/EvoScientist/skills/agent-swarm-protocol/references/task-board.md +63 -0
  107. evoscientist-0.0.1b0/EvoScientist/skills/find-skills/SKILL.md +76 -0
  108. evoscientist-0.0.1b0/EvoScientist/skills/skill-creator/SKILL.md +362 -0
  109. evoscientist-0.0.1b0/EvoScientist/skills/skill-creator/references/output-patterns.md +82 -0
  110. evoscientist-0.0.1b0/EvoScientist/skills/skill-creator/references/workflows.md +28 -0
  111. evoscientist-0.0.1b0/EvoScientist/skills/skill-creator/scripts/init_skill.py +303 -0
  112. evoscientist-0.0.1b0/EvoScientist/skills/skill-creator/scripts/package_skill.py +110 -0
  113. evoscientist-0.0.1b0/EvoScientist/skills/skill-creator/scripts/quick_validate.py +95 -0
  114. evoscientist-0.0.1b0/EvoScientist/stream/__init__.py +80 -0
  115. evoscientist-0.0.1b0/EvoScientist/stream/display.py +837 -0
  116. evoscientist-0.0.1b0/EvoScientist/stream/emitter.py +94 -0
  117. evoscientist-0.0.1b0/EvoScientist/stream/events.py +512 -0
  118. evoscientist-0.0.1b0/EvoScientist/stream/formatter.py +168 -0
  119. evoscientist-0.0.1b0/EvoScientist/stream/state.py +348 -0
  120. evoscientist-0.0.1b0/EvoScientist/stream/tracker.py +115 -0
  121. evoscientist-0.0.1b0/EvoScientist/stream/utils.py +271 -0
  122. evoscientist-0.0.1b0/EvoScientist/subagent.yaml +147 -0
  123. evoscientist-0.0.1b0/EvoScientist/tools/__init__.py +16 -0
  124. evoscientist-0.0.1b0/EvoScientist/tools/search.py +115 -0
  125. evoscientist-0.0.1b0/EvoScientist/tools/skill_manager.py +120 -0
  126. evoscientist-0.0.1b0/EvoScientist/tools/skills_manager.py +470 -0
  127. evoscientist-0.0.1b0/EvoScientist/tools/think.py +32 -0
  128. evoscientist-0.0.1b0/EvoScientist/utils.py +217 -0
  129. evoscientist-0.0.1b0/EvoScientist.egg-info/PKG-INFO +438 -0
  130. evoscientist-0.0.1b0/EvoScientist.egg-info/SOURCES.txt +171 -0
  131. evoscientist-0.0.1b0/EvoScientist.egg-info/dependency_links.txt +1 -0
  132. evoscientist-0.0.1b0/EvoScientist.egg-info/entry_points.txt +5 -0
  133. evoscientist-0.0.1b0/EvoScientist.egg-info/requires.txt +45 -0
  134. evoscientist-0.0.1b0/EvoScientist.egg-info/top_level.txt +1 -0
  135. evoscientist-0.0.1b0/LICENSE +21 -0
  136. evoscientist-0.0.1b0/PKG-INFO +438 -0
  137. evoscientist-0.0.1b0/README.md +382 -0
  138. evoscientist-0.0.1b0/pyproject.toml +91 -0
  139. evoscientist-0.0.1b0/setup.cfg +4 -0
  140. evoscientist-0.0.1b0/tests/test_agent_mcp_cache.py +59 -0
  141. evoscientist-0.0.1b0/tests/test_backends.py +369 -0
  142. evoscientist-0.0.1b0/tests/test_bus_integration.py +272 -0
  143. evoscientist-0.0.1b0/tests/test_channel_comprehensive.py +1621 -0
  144. evoscientist-0.0.1b0/tests/test_cli_channel_bus_mode.py +122 -0
  145. evoscientist-0.0.1b0/tests/test_cli_run_name.py +38 -0
  146. evoscientist-0.0.1b0/tests/test_cli_serve.py +143 -0
  147. evoscientist-0.0.1b0/tests/test_cli_tui_dispatch.py +53 -0
  148. evoscientist-0.0.1b0/tests/test_config.py +397 -0
  149. evoscientist-0.0.1b0/tests/test_dingtalk_channel.py +290 -0
  150. evoscientist-0.0.1b0/tests/test_discord_channel.py +63 -0
  151. evoscientist-0.0.1b0/tests/test_event_loop.py +200 -0
  152. evoscientist-0.0.1b0/tests/test_feishu_channel.py +497 -0
  153. evoscientist-0.0.1b0/tests/test_llm.py +306 -0
  154. evoscientist-0.0.1b0/tests/test_mcp_client.py +691 -0
  155. evoscientist-0.0.1b0/tests/test_memory_merge.py +73 -0
  156. evoscientist-0.0.1b0/tests/test_onboard.py +826 -0
  157. evoscientist-0.0.1b0/tests/test_paths.py +105 -0
  158. evoscientist-0.0.1b0/tests/test_prompts.py +35 -0
  159. evoscientist-0.0.1b0/tests/test_rich_escape.py +18 -0
  160. evoscientist-0.0.1b0/tests/test_sessions.py +277 -0
  161. evoscientist-0.0.1b0/tests/test_skills_manager.py +341 -0
  162. evoscientist-0.0.1b0/tests/test_slack_channel.py +79 -0
  163. evoscientist-0.0.1b0/tests/test_stream_emitter.py +89 -0
  164. evoscientist-0.0.1b0/tests/test_stream_events.py +87 -0
  165. evoscientist-0.0.1b0/tests/test_stream_state.py +558 -0
  166. evoscientist-0.0.1b0/tests/test_stream_tracker.py +99 -0
  167. evoscientist-0.0.1b0/tests/test_stream_utils.py +240 -0
  168. evoscientist-0.0.1b0/tests/test_telegram_channel.py +60 -0
  169. evoscientist-0.0.1b0/tests/test_tool_error_handler.py +220 -0
  170. evoscientist-0.0.1b0/tests/test_tools.py +18 -0
  171. evoscientist-0.0.1b0/tests/test_tui_widgets.py +451 -0
  172. evoscientist-0.0.1b0/tests/test_ui_runtime.py +58 -0
  173. evoscientist-0.0.1b0/tests/test_wechat_channel.py +452 -0
@@ -0,0 +1,336 @@
1
+ """EvoScientist Agent graph construction.
2
+
3
+ This module creates and exports the compiled agent graph.
4
+ Usage:
5
+ from EvoScientist import agent
6
+
7
+ # Notebook / programmatic usage
8
+ for state in agent.stream(
9
+ {"messages": [HumanMessage(content="your question")]},
10
+ config={"configurable": {"thread_id": "1"}},
11
+ stream_mode="values",
12
+ ):
13
+ ...
14
+ """
15
+
16
+ import json
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+
20
+ from deepagents import create_deep_agent
21
+ from deepagents.backends import FilesystemBackend, CompositeBackend
22
+
23
+ from .backends import CustomSandboxBackend, MergedReadOnlyBackend
24
+ from .config import get_effective_config, apply_config_to_env
25
+ from .llm import get_chat_model
26
+ from .mcp import load_mcp_tools
27
+ from .middleware import create_memory_middleware, ToolErrorHandlerMiddleware
28
+ from .prompts import RESEARCHER_INSTRUCTIONS, get_system_prompt
29
+ from .utils import load_subagents
30
+ from .tools import tavily_search, think_tool, skill_manager
31
+ from . import paths as _paths_mod
32
+ from .paths import set_active_workspace, set_workspace_root
33
+
34
+ # =============================================================================
35
+ # Configuration
36
+ # =============================================================================
37
+
38
+ # Load configuration from file/env/defaults
39
+ _config = get_effective_config()
40
+ apply_config_to_env(_config)
41
+
42
+ # NOTE: We intentionally do NOT call set_workspace_root() at module level.
43
+ # The CLI (commands.py) calls set_workspace_root() *before* importing this
44
+ # module. A module-level call here would overwrite the CLI's --workdir
45
+ # value with config.default_workdir, violating the priority chain
46
+ # (CLI args > config file). Instead, config.default_workdir is applied
47
+ # as a fallback inside create_cli_agent() when no explicit workspace_dir
48
+ # is provided.
49
+
50
+ # Research limits (from config)
51
+ MAX_CONCURRENT = _config.max_concurrent
52
+ MAX_ITERATIONS = _config.max_iterations
53
+
54
+ # Workspace settings (defer dir creation to CLI; here we just resolve paths)
55
+ # Read from the paths module so values reflect any earlier set_workspace_root().
56
+ WORKSPACE_DIR = str(_paths_mod.WORKSPACE_ROOT)
57
+ set_active_workspace(WORKSPACE_DIR)
58
+ MEMORY_DIR = str(_paths_mod.MEMORY_DIR) # Shared across sessions (not per-session)
59
+ SKILLS_DIR = str(Path(__file__).parent / "skills")
60
+ USER_SKILLS_DIR = str(_paths_mod.USER_SKILLS_DIR)
61
+ SUBAGENTS_CONFIG = Path(__file__).parent / "subagent.yaml"
62
+
63
+ # =============================================================================
64
+ # Initialization
65
+ # =============================================================================
66
+
67
+
68
+ # Generate system prompt with limits
69
+ SYSTEM_PROMPT = get_system_prompt(
70
+ max_concurrent=MAX_CONCURRENT,
71
+ max_iterations=MAX_ITERATIONS,
72
+ )
73
+
74
+ # Initialize chat model using the LLM module (respects config settings)
75
+ chat_model = get_chat_model(
76
+ model=_config.model,
77
+ provider=_config.provider,
78
+ )
79
+
80
+ # Initialize workspace backend
81
+ _workspace_backend = CustomSandboxBackend(
82
+ root_dir=WORKSPACE_DIR,
83
+ virtual_mode=True,
84
+ timeout=300,
85
+ )
86
+
87
+ # Skills backend: merge user-installed (./skills/) and system (package) skills
88
+ _skills_backend = MergedReadOnlyBackend(
89
+ primary_dir=USER_SKILLS_DIR, # user-installed, takes priority
90
+ secondary_dir=SKILLS_DIR, # package built-in, fallback
91
+ )
92
+
93
+ # Memory backend: persistent filesystem for long-term memory (shared across sessions)
94
+ _memory_backend = FilesystemBackend(
95
+ root_dir=MEMORY_DIR,
96
+ virtual_mode=True,
97
+ )
98
+
99
+ # Composite backend: workspace as default, skills and memory mounted
100
+ backend = CompositeBackend(
101
+ default=_workspace_backend,
102
+ routes={
103
+ "/skills/": _skills_backend,
104
+ "/memory/": _memory_backend,
105
+ },
106
+ )
107
+
108
+ tool_registry = {
109
+ "think_tool": think_tool,
110
+ "tavily_search": tavily_search,
111
+ }
112
+
113
+ # Base tools that every agent variant gets (before MCP)
114
+ BASE_TOOLS = [think_tool, skill_manager]
115
+
116
+ # Cache MCP tools by the effective config signature to avoid reconnecting
117
+ # to MCP servers on every `/new` when config is unchanged.
118
+ _MCP_TOOLS_CACHE_KEY: str | None = None
119
+ _MCP_TOOLS_CACHE_VALUE: dict[str, list] | None = None
120
+
121
+
122
+ def _mcp_config_signature() -> str:
123
+ """Return a stable signature for the effective MCP config."""
124
+ from .mcp.client import load_mcp_config
125
+
126
+ cfg = load_mcp_config()
127
+ if not cfg:
128
+ return ""
129
+ try:
130
+ return json.dumps(cfg, sort_keys=True, ensure_ascii=True)
131
+ except TypeError:
132
+ # Fallback for non-JSON-serializable values (should be rare)
133
+ return repr(cfg)
134
+
135
+
136
+ def _load_mcp_tools_cached() -> dict[str, list]:
137
+ """Load MCP tools with config-aware caching."""
138
+ global _MCP_TOOLS_CACHE_KEY, _MCP_TOOLS_CACHE_VALUE
139
+
140
+ cfg_key = _mcp_config_signature()
141
+ if not cfg_key:
142
+ _MCP_TOOLS_CACHE_KEY = ""
143
+ _MCP_TOOLS_CACHE_VALUE = {}
144
+ return {}
145
+
146
+ if _MCP_TOOLS_CACHE_KEY == cfg_key and _MCP_TOOLS_CACHE_VALUE is not None:
147
+ return {k: list(v) for k, v in _MCP_TOOLS_CACHE_VALUE.items()}
148
+
149
+ loaded = load_mcp_tools()
150
+ _MCP_TOOLS_CACHE_KEY = cfg_key
151
+ _MCP_TOOLS_CACHE_VALUE = {k: list(v) for k, v in loaded.items()}
152
+ return {k: list(v) for k, v in loaded.items()}
153
+
154
+
155
+ def _inject_subagent_middleware(subs: list[dict]) -> None:
156
+ """Ensure every subagent gets ToolErrorHandlerMiddleware.
157
+
158
+ Without this, subagent tool errors are caught by LangGraph's default
159
+ ToolNode handler which produces terse messages without tracebacks or
160
+ retry guidance — reducing the subagent's ability to self-recover.
161
+ """
162
+ for sa in subs:
163
+ sa.setdefault("middleware", []).append(ToolErrorHandlerMiddleware())
164
+
165
+
166
+ def _build_base_kwargs(base_backend, base_middleware):
167
+ """Build agent kwargs *without* MCP (fast, no subprocess spawning)."""
168
+ subs = load_subagents(
169
+ SUBAGENTS_CONFIG,
170
+ tool_registry=tool_registry,
171
+ prompt_refs=_build_prompt_refs(),
172
+ )
173
+ _inject_subagent_middleware(subs)
174
+ return dict(
175
+ name="EvoScientist",
176
+ model=chat_model,
177
+ tools=list(BASE_TOOLS),
178
+ backend=base_backend,
179
+ subagents=subs,
180
+ middleware=base_middleware,
181
+ system_prompt=SYSTEM_PROMPT,
182
+ skills=["/skills/"],
183
+ )
184
+
185
+
186
+ def load_mcp_and_build_kwargs(base_backend, base_middleware):
187
+ """Load MCP tools (cached by config) and build agent kwargs.
188
+
189
+ Re-connects to MCP servers only when the effective MCP config changes.
190
+ Falls back to base kwargs if no MCP configured.
191
+ """
192
+ mcp_by_agent = _load_mcp_tools_cached()
193
+ if not mcp_by_agent:
194
+ return _build_base_kwargs(base_backend, base_middleware)
195
+
196
+ # Fresh tool registry — start from base tools + MCP tools
197
+ registry = dict(tool_registry)
198
+ for tools in mcp_by_agent.values():
199
+ for t in tools:
200
+ registry[t.name] = t
201
+
202
+ mcp_main = mcp_by_agent.pop("main", [])
203
+
204
+ subs = load_subagents(
205
+ SUBAGENTS_CONFIG,
206
+ tool_registry=registry,
207
+ prompt_refs=_build_prompt_refs(),
208
+ )
209
+
210
+ _inject_subagent_middleware(subs)
211
+
212
+ # Inject MCP tools into subagents by name
213
+ for sa in subs:
214
+ if sa_tools := mcp_by_agent.get(sa["name"], []):
215
+ sa.setdefault("tools", []).extend(sa_tools)
216
+
217
+ return dict(
218
+ name="EvoScientist",
219
+ model=chat_model,
220
+ tools=BASE_TOOLS + mcp_main,
221
+ backend=base_backend,
222
+ subagents=subs,
223
+ middleware=base_middleware,
224
+ system_prompt=SYSTEM_PROMPT,
225
+ skills=["/skills/"],
226
+ )
227
+
228
+
229
+ def _build_prompt_refs() -> dict:
230
+ """Build prompt references with the current date (not frozen at import)."""
231
+ return {
232
+ "RESEARCHER_INSTRUCTIONS": RESEARCHER_INSTRUCTIONS.format(
233
+ date=datetime.now().strftime("%Y-%m-%d"),
234
+ ),
235
+ }
236
+
237
+ base_middleware = [
238
+ ToolErrorHandlerMiddleware(),
239
+ create_memory_middleware(MEMORY_DIR, extraction_model=chat_model),
240
+ ]
241
+
242
+ # Default agent (no checkpointer) — used by langgraph dev / LangSmith / notebooks.
243
+ # Lazily constructed on first access so MCP tools are included without
244
+ # spawning subprocesses at import time.
245
+ _EvoScientist_agent = None
246
+
247
+
248
+ def _get_default_agent():
249
+ """Build the default agent (with MCP, no checkpointer) on first access."""
250
+ global _EvoScientist_agent
251
+ if _EvoScientist_agent is None:
252
+ kwargs = load_mcp_and_build_kwargs(backend, base_middleware)
253
+ _EvoScientist_agent = create_deep_agent(**kwargs).with_config(
254
+ {"recursion_limit": 500}
255
+ )
256
+ return _EvoScientist_agent
257
+
258
+
259
+ def __getattr__(name: str):
260
+ if name == "EvoScientist_agent":
261
+ return _get_default_agent()
262
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
263
+
264
+
265
+ def create_cli_agent(workspace_dir: str | None = None, checkpointer=None):
266
+ """Create agent with checkpointer for CLI multi-turn support.
267
+
268
+ A fresh backend is constructed on every call using the current
269
+ ``paths.WORKSPACE_ROOT`` (or the explicit *workspace_dir*), so
270
+ runtime ``set_workspace_root()`` changes are always respected.
271
+
272
+ Args:
273
+ workspace_dir: Per-session workspace directory. If ``None``,
274
+ defaults to the current ``paths.WORKSPACE_ROOT``.
275
+ checkpointer: Optional LangGraph checkpointer. If ``None``,
276
+ falls back to ``InMemorySaver`` (non-persistent).
277
+ """
278
+ import os as _os
279
+ from . import paths as _paths
280
+
281
+ if checkpointer is None:
282
+ from langgraph.checkpoint.memory import InMemorySaver # type: ignore[import-untyped]
283
+ checkpointer = InMemorySaver()
284
+
285
+ # When no explicit workspace_dir is provided, apply config.default_workdir
286
+ # as a fallback. This covers direct callers (notebooks, iMessage server)
287
+ # that never call set_workspace_root() themselves. CLI callers always
288
+ # pass workspace_dir explicitly, so their --workdir is never overwritten.
289
+ if workspace_dir is None:
290
+ if _config.default_workdir:
291
+ set_workspace_root(
292
+ _os.path.abspath(_os.path.expanduser(_config.default_workdir))
293
+ )
294
+ workspace_dir = str(_paths.WORKSPACE_ROOT)
295
+
296
+ # Read paths dynamically so runtime set_workspace_root() changes are picked up
297
+ _mem_dir = str(_paths.MEMORY_DIR)
298
+ _usr_skills_dir = str(_paths.USER_SKILLS_DIR)
299
+
300
+ # Always construct fresh backends from current paths (avoids stale
301
+ # module-level backend when workspace root changed at runtime).
302
+ set_active_workspace(workspace_dir)
303
+ ws_backend = CustomSandboxBackend(
304
+ root_dir=workspace_dir,
305
+ virtual_mode=True,
306
+ timeout=300,
307
+ )
308
+ sk_backend = MergedReadOnlyBackend(
309
+ primary_dir=_usr_skills_dir,
310
+ secondary_dir=SKILLS_DIR,
311
+ )
312
+ # Memory always uses SHARED directory (not per-session) for cross-session persistence
313
+ mem_backend = FilesystemBackend(
314
+ root_dir=_mem_dir,
315
+ virtual_mode=True,
316
+ )
317
+ be = CompositeBackend(
318
+ default=ws_backend,
319
+ routes={
320
+ "/skills/": sk_backend,
321
+ "/memory/": mem_backend,
322
+ },
323
+ )
324
+
325
+ mw = [
326
+ ToolErrorHandlerMiddleware(),
327
+ create_memory_middleware(_mem_dir, extraction_model=chat_model),
328
+ ]
329
+
330
+ # Re-load MCP tools from current config (picks up /mcp add changes)
331
+ kwargs = load_mcp_and_build_kwargs(be, mw)
332
+
333
+ return create_deep_agent(
334
+ **kwargs,
335
+ checkpointer=checkpointer,
336
+ ).with_config({"recursion_limit": 500})
@@ -0,0 +1,74 @@
1
+ """EvoScientist Agent - AI-powered research and code execution.
2
+
3
+ This package exposes a convenience API at the package root while keeping
4
+ imports lazy, so lightweight modules (for example config helpers) can be used
5
+ without importing heavy runtime dependencies.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from importlib import import_module
11
+
12
+
13
+ _EXPORTS: dict[str, tuple[str, str]] = {
14
+ # Agent graph (lazy to avoid expensive initialization at import time)
15
+ "EvoScientist_agent": (".EvoScientist", "EvoScientist_agent"),
16
+ "create_cli_agent": (".EvoScientist", "create_cli_agent"),
17
+ # Backends
18
+ "CustomSandboxBackend": (".backends", "CustomSandboxBackend"),
19
+ "ReadOnlyFilesystemBackend": (".backends", "ReadOnlyFilesystemBackend"),
20
+ # Configuration
21
+ "EvoScientistConfig": (".config", "EvoScientistConfig"),
22
+ "load_config": (".config", "load_config"),
23
+ "save_config": (".config", "save_config"),
24
+ "get_effective_config": (".config", "get_effective_config"),
25
+ "get_config_path": (".config", "get_config_path"),
26
+ # LLM
27
+ "get_chat_model": (".llm", "get_chat_model"),
28
+ "MODELS": (".llm", "MODELS"),
29
+ "list_models": (".llm", "list_models"),
30
+ "DEFAULT_MODEL": (".llm", "DEFAULT_MODEL"),
31
+ # Prompts
32
+ "get_system_prompt": (".prompts", "get_system_prompt"),
33
+ "RESEARCHER_INSTRUCTIONS": (".prompts", "RESEARCHER_INSTRUCTIONS"),
34
+ # Tools
35
+ "tavily_search": (".tools", "tavily_search"),
36
+ "think_tool": (".tools", "think_tool"),
37
+ # Sessions
38
+ "get_checkpointer": (".sessions", "get_checkpointer"),
39
+ "generate_thread_id": (".sessions", "generate_thread_id"),
40
+ "list_threads": (".sessions", "list_threads"),
41
+ "delete_thread": (".sessions", "delete_thread"),
42
+ }
43
+
44
+
45
+ def __getattr__(name: str):
46
+ """Lazily import and cache package-level attributes.
47
+
48
+ Args:
49
+ name: The attribute name to look up.
50
+
51
+ Returns:
52
+ The resolved attribute value.
53
+
54
+ Raises:
55
+ AttributeError: If the name is not in _EXPORTS.
56
+ """
57
+ target = _EXPORTS.get(name)
58
+ if target is None:
59
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
60
+
61
+ module_name, attr_name = target
62
+ module = import_module(module_name, package=__name__)
63
+ value = getattr(module, attr_name)
64
+ # Cache after first load to avoid repeated import lookups.
65
+ globals()[name] = value
66
+ return value
67
+
68
+
69
+ def __dir__() -> list[str]:
70
+ """List available public attributes including lazy exports."""
71
+ return sorted(set(globals()) | set(_EXPORTS))
72
+
73
+
74
+ __all__ = list(_EXPORTS)
@@ -0,0 +1,4 @@
1
+ """Enable `python -m EvoScientist` execution."""
2
+ from EvoScientist.cli import main
3
+
4
+ main()