EvoScientist 0.0.1.dev7__tar.gz → 0.0.1.dev8__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 (137) hide show
  1. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/EvoScientist.py +119 -64
  2. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/__init__.py +12 -2
  3. evoscientist-0.0.1.dev8/EvoScientist/channels/__init__.py +44 -0
  4. evoscientist-0.0.1.dev8/EvoScientist/channels/base.py +1018 -0
  5. evoscientist-0.0.1.dev8/EvoScientist/channels/bus/__init__.py +6 -0
  6. evoscientist-0.0.1.dev8/EvoScientist/channels/bus/events.py +52 -0
  7. evoscientist-0.0.1.dev8/EvoScientist/channels/bus/message_bus.py +96 -0
  8. evoscientist-0.0.1.dev8/EvoScientist/channels/capabilities.py +220 -0
  9. evoscientist-0.0.1.dev8/EvoScientist/channels/channel_manager.py +995 -0
  10. evoscientist-0.0.1.dev8/EvoScientist/channels/config.py +126 -0
  11. evoscientist-0.0.1.dev8/EvoScientist/channels/consumer.py +407 -0
  12. evoscientist-0.0.1.dev8/EvoScientist/channels/discord/__init__.py +19 -0
  13. evoscientist-0.0.1.dev8/EvoScientist/channels/discord/channel.py +255 -0
  14. evoscientist-0.0.1.dev8/EvoScientist/channels/discord/probe.py +33 -0
  15. evoscientist-0.0.1.dev8/EvoScientist/channels/discord/serve.py +93 -0
  16. evoscientist-0.0.1.dev8/EvoScientist/channels/formatter.py +287 -0
  17. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/channels/imessage/__init__.py +9 -0
  18. evoscientist-0.0.1.dev8/EvoScientist/channels/imessage/channel_rpc.py +407 -0
  19. evoscientist-0.0.1.dev8/EvoScientist/channels/imessage/serve.py +87 -0
  20. evoscientist-0.0.1.dev8/EvoScientist/channels/middleware.py +814 -0
  21. evoscientist-0.0.1.dev8/EvoScientist/channels/mixins.py +306 -0
  22. evoscientist-0.0.1.dev8/EvoScientist/channels/plugin.py +226 -0
  23. evoscientist-0.0.1.dev8/EvoScientist/channels/retry.py +122 -0
  24. evoscientist-0.0.1.dev8/EvoScientist/channels/slack/__init__.py +20 -0
  25. evoscientist-0.0.1.dev8/EvoScientist/channels/slack/channel.py +291 -0
  26. evoscientist-0.0.1.dev8/EvoScientist/channels/slack/probe.py +48 -0
  27. evoscientist-0.0.1.dev8/EvoScientist/channels/slack/serve.py +99 -0
  28. evoscientist-0.0.1.dev8/EvoScientist/channels/standalone.py +142 -0
  29. evoscientist-0.0.1.dev8/EvoScientist/channels/telegram/__init__.py +17 -0
  30. evoscientist-0.0.1.dev8/EvoScientist/channels/telegram/channel.py +289 -0
  31. evoscientist-0.0.1.dev8/EvoScientist/channels/telegram/probe.py +32 -0
  32. evoscientist-0.0.1.dev8/EvoScientist/channels/telegram/serve.py +81 -0
  33. evoscientist-0.0.1.dev8/EvoScientist/channels/wechat/__init__.py +69 -0
  34. evoscientist-0.0.1.dev8/EvoScientist/channels/wechat/channel.py +865 -0
  35. evoscientist-0.0.1.dev8/EvoScientist/channels/wechat/crypto.py +187 -0
  36. evoscientist-0.0.1.dev8/EvoScientist/channels/wechat/probe.py +72 -0
  37. evoscientist-0.0.1.dev8/EvoScientist/channels/wechat/serve.py +139 -0
  38. evoscientist-0.0.1.dev8/EvoScientist/channels/wechat/verify_server.py +175 -0
  39. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/cli/__init__.py +1 -1
  40. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/cli/_app.py +4 -0
  41. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/cli/agent.py +7 -3
  42. evoscientist-0.0.1.dev8/EvoScientist/cli/channel.py +435 -0
  43. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/cli/commands.py +115 -18
  44. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/cli/interactive.py +128 -123
  45. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/cli/mcp_ui.py +1 -1
  46. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/cli/skills_cmd.py +20 -7
  47. evoscientist-0.0.1.dev8/EvoScientist/config/__init__.py +38 -0
  48. {evoscientist-0.0.1.dev7/EvoScientist → evoscientist-0.0.1.dev8/EvoScientist/config}/onboard.py +240 -84
  49. evoscientist-0.0.1.dev7/EvoScientist/config.py → evoscientist-0.0.1.dev8/EvoScientist/config/settings.py +124 -24
  50. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/mcp/client.py +31 -3
  51. evoscientist-0.0.1.dev8/EvoScientist/middleware/__init__.py +21 -0
  52. {evoscientist-0.0.1.dev7/EvoScientist → evoscientist-0.0.1.dev8/EvoScientist/middleware}/memory.py +114 -50
  53. evoscientist-0.0.1.dev8/EvoScientist/middleware/tool_error_handler.py +70 -0
  54. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/paths.py +25 -4
  55. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/prompts.py +9 -5
  56. evoscientist-0.0.1.dev8/EvoScientist/skills/agent-swarm-protocol/LICENSE.txt +201 -0
  57. evoscientist-0.0.1.dev8/EvoScientist/skills/agent-swarm-protocol/SKILL.md +144 -0
  58. evoscientist-0.0.1.dev8/EvoScientist/skills/agent-swarm-protocol/references/cli-compatibility.md +23 -0
  59. evoscientist-0.0.1.dev8/EvoScientist/skills/agent-swarm-protocol/references/known-issues.md +210 -0
  60. evoscientist-0.0.1.dev8/EvoScientist/skills/agent-swarm-protocol/references/task-board.md +63 -0
  61. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/stream/display.py +15 -9
  62. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/stream/emitter.py +1 -1
  63. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/stream/events.py +38 -1
  64. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/stream/formatter.py +1 -5
  65. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/tools/skills_manager.py +5 -5
  66. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/utils.py +13 -6
  67. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist.egg-info/PKG-INFO +41 -15
  68. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist.egg-info/SOURCES.txt +54 -5
  69. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist.egg-info/requires.txt +20 -0
  70. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/PKG-INFO +41 -15
  71. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/README.md +25 -14
  72. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/pyproject.toml +12 -1
  73. evoscientist-0.0.1.dev8/tests/test_agent_mcp_cache.py +59 -0
  74. evoscientist-0.0.1.dev8/tests/test_bus_integration.py +278 -0
  75. evoscientist-0.0.1.dev8/tests/test_channel_comprehensive.py +1545 -0
  76. evoscientist-0.0.1.dev8/tests/test_channel_manager.py +166 -0
  77. evoscientist-0.0.1.dev8/tests/test_discord_channel.py +71 -0
  78. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_llm.py +11 -11
  79. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_mcp_client.py +152 -0
  80. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_memory_merge.py +1 -1
  81. evoscientist-0.0.1.dev8/tests/test_message_bus.py +111 -0
  82. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_onboard.py +229 -152
  83. evoscientist-0.0.1.dev8/tests/test_paths.py +105 -0
  84. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_skills_manager.py +6 -6
  85. evoscientist-0.0.1.dev8/tests/test_slack_channel.py +87 -0
  86. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_stream_state.py +36 -169
  87. evoscientist-0.0.1.dev8/tests/test_telegram_channel.py +68 -0
  88. evoscientist-0.0.1.dev8/tests/test_tool_error_handler.py +219 -0
  89. evoscientist-0.0.1.dev8/tests/test_wechat_channel.py +458 -0
  90. evoscientist-0.0.1.dev7/EvoScientist/channels/__init__.py +0 -9
  91. evoscientist-0.0.1.dev7/EvoScientist/channels/base.py +0 -110
  92. evoscientist-0.0.1.dev7/EvoScientist/channels/imessage/channel_rpc.py +0 -427
  93. evoscientist-0.0.1.dev7/EvoScientist/channels/imessage/serve.py +0 -428
  94. evoscientist-0.0.1.dev7/EvoScientist/cli/channel.py +0 -289
  95. evoscientist-0.0.1.dev7/EvoScientist/middleware.py +0 -66
  96. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/__main__.py +0 -0
  97. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/backends.py +0 -0
  98. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/channels/imessage/probe.py +0 -0
  99. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/channels/imessage/rpc_client.py +0 -0
  100. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/channels/imessage/targets.py +0 -0
  101. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/llm/__init__.py +0 -0
  102. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/llm/models.py +0 -0
  103. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/mcp/__init__.py +0 -0
  104. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/sessions.py +0 -0
  105. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/skills/find-skills/SKILL.md +0 -0
  106. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/skills/skill-creator/SKILL.md +0 -0
  107. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/skills/skill-creator/references/output-patterns.md +0 -0
  108. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/skills/skill-creator/references/workflows.md +0 -0
  109. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/skills/skill-creator/scripts/init_skill.py +0 -0
  110. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/skills/skill-creator/scripts/package_skill.py +0 -0
  111. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/skills/skill-creator/scripts/quick_validate.py +0 -0
  112. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/stream/__init__.py +0 -0
  113. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/stream/state.py +0 -0
  114. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/stream/tracker.py +0 -0
  115. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/stream/utils.py +0 -0
  116. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/subagent.yaml +0 -0
  117. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/tools/__init__.py +0 -0
  118. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/tools/search.py +0 -0
  119. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/tools/skill_manager.py +0 -0
  120. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist/tools/think.py +0 -0
  121. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist.egg-info/dependency_links.txt +0 -0
  122. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist.egg-info/entry_points.txt +0 -0
  123. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/EvoScientist.egg-info/top_level.txt +0 -0
  124. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/LICENSE +0 -0
  125. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/setup.cfg +0 -0
  126. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_backends.py +0 -0
  127. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_cli_run_name.py +0 -0
  128. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_config.py +0 -0
  129. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_event_loop.py +0 -0
  130. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_imports.py +0 -0
  131. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_prompts.py +0 -0
  132. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_sessions.py +0 -0
  133. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_stream_emitter.py +0 -0
  134. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_stream_events.py +0 -0
  135. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_stream_tracker.py +0 -0
  136. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_stream_utils.py +0 -0
  137. {evoscientist-0.0.1.dev7 → evoscientist-0.0.1.dev8}/tests/test_tools.py +0 -0
@@ -13,6 +13,7 @@ Usage:
13
13
  ...
14
14
  """
15
15
 
16
+ import json
16
17
  from datetime import datetime
17
18
  from pathlib import Path
18
19
 
@@ -23,17 +24,12 @@ from .backends import CustomSandboxBackend, MergedReadOnlyBackend
23
24
  from .config import get_effective_config, apply_config_to_env
24
25
  from .llm import get_chat_model
25
26
  from .mcp import load_mcp_tools
26
- from .middleware import create_skills_middleware, create_memory_middleware
27
+ from .middleware import create_memory_middleware, ToolErrorHandlerMiddleware
27
28
  from .prompts import RESEARCHER_INSTRUCTIONS, get_system_prompt
28
29
  from .utils import load_subagents
29
30
  from .tools import tavily_search, think_tool, skill_manager
30
- from .paths import (
31
- ensure_dirs,
32
- default_workspace_dir,
33
- set_active_workspace,
34
- MEMORY_DIR as _MEMORY_DIR_PATH,
35
- USER_SKILLS_DIR as _USER_SKILLS_DIR_PATH,
36
- )
31
+ from . import paths as _paths_mod
32
+ from .paths import set_active_workspace, set_workspace_root
37
33
 
38
34
  # =============================================================================
39
35
  # Configuration
@@ -43,20 +39,25 @@ from .paths import (
43
39
  _config = get_effective_config()
44
40
  apply_config_to_env(_config)
45
41
 
46
- # Backend mode: "sandbox" (with execute) or "filesystem" (read/write only)
47
- BACKEND_MODE = "sandbox"
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.
48
49
 
49
50
  # Research limits (from config)
50
51
  MAX_CONCURRENT = _config.max_concurrent
51
52
  MAX_ITERATIONS = _config.max_iterations
52
53
 
53
- # Workspace settings
54
- ensure_dirs()
55
- WORKSPACE_DIR = str(default_workspace_dir())
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)
56
57
  set_active_workspace(WORKSPACE_DIR)
57
- MEMORY_DIR = str(_MEMORY_DIR_PATH) # Shared across sessions (not per-session)
58
+ MEMORY_DIR = str(_paths_mod.MEMORY_DIR) # Shared across sessions (not per-session)
58
59
  SKILLS_DIR = str(Path(__file__).parent / "skills")
59
- USER_SKILLS_DIR = str(_USER_SKILLS_DIR_PATH)
60
+ USER_SKILLS_DIR = str(_paths_mod.USER_SKILLS_DIR)
60
61
  SUBAGENTS_CONFIG = Path(__file__).parent / "subagent.yaml"
61
62
 
62
63
  # =============================================================================
@@ -78,18 +79,12 @@ chat_model = get_chat_model(
78
79
  provider=_config.provider,
79
80
  )
80
81
 
81
- # Initialize workspace backend based on mode
82
- if BACKEND_MODE == "sandbox":
83
- _workspace_backend = CustomSandboxBackend(
84
- root_dir=WORKSPACE_DIR,
85
- virtual_mode=True,
86
- timeout=300,
87
- )
88
- else:
89
- _workspace_backend = FilesystemBackend(
90
- root_dir=WORKSPACE_DIR,
91
- virtual_mode=True,
92
- )
82
+ # Initialize workspace backend
83
+ _workspace_backend = CustomSandboxBackend(
84
+ root_dir=WORKSPACE_DIR,
85
+ virtual_mode=True,
86
+ timeout=300,
87
+ )
93
88
 
94
89
  # Skills backend: merge user-installed (./skills/) and system (package) skills
95
90
  _skills_backend = MergedReadOnlyBackend(
@@ -120,6 +115,44 @@ tool_registry = {
120
115
  # Base tools that every agent variant gets (before MCP)
121
116
  BASE_TOOLS = [think_tool, skill_manager]
122
117
 
118
+ # Cache MCP tools by the effective config signature to avoid reconnecting
119
+ # to MCP servers on every `/new` when config is unchanged.
120
+ _MCP_TOOLS_CACHE_KEY: str | None = None
121
+ _MCP_TOOLS_CACHE_VALUE: dict[str, list] | None = None
122
+
123
+
124
+ def _mcp_config_signature() -> str:
125
+ """Return a stable signature for the effective MCP config."""
126
+ from .mcp.client import load_mcp_config
127
+
128
+ cfg = load_mcp_config()
129
+ if not cfg:
130
+ return ""
131
+ try:
132
+ return json.dumps(cfg, sort_keys=True, ensure_ascii=True)
133
+ except TypeError:
134
+ # Fallback for non-JSON-serializable values (should be rare)
135
+ return repr(cfg)
136
+
137
+
138
+ def _load_mcp_tools_cached() -> dict[str, list]:
139
+ """Load MCP tools with config-aware caching."""
140
+ global _MCP_TOOLS_CACHE_KEY, _MCP_TOOLS_CACHE_VALUE
141
+
142
+ cfg_key = _mcp_config_signature()
143
+ if not cfg_key:
144
+ _MCP_TOOLS_CACHE_KEY = ""
145
+ _MCP_TOOLS_CACHE_VALUE = {}
146
+ return {}
147
+
148
+ if _MCP_TOOLS_CACHE_KEY == cfg_key and _MCP_TOOLS_CACHE_VALUE is not None:
149
+ return {k: list(v) for k, v in _MCP_TOOLS_CACHE_VALUE.items()}
150
+
151
+ loaded = load_mcp_tools()
152
+ _MCP_TOOLS_CACHE_KEY = cfg_key
153
+ _MCP_TOOLS_CACHE_VALUE = {k: list(v) for k, v in loaded.items()}
154
+ return {k: list(v) for k, v in loaded.items()}
155
+
123
156
 
124
157
  def _build_base_kwargs(base_backend, base_middleware):
125
158
  """Build agent kwargs *without* MCP (fast, no subprocess spawning)."""
@@ -136,16 +169,17 @@ def _build_base_kwargs(base_backend, base_middleware):
136
169
  subagents=subs,
137
170
  middleware=base_middleware,
138
171
  system_prompt=SYSTEM_PROMPT,
172
+ skills=["/skills/"],
139
173
  )
140
174
 
141
175
 
142
176
  def load_mcp_and_build_kwargs(base_backend, base_middleware):
143
- """(Re-)load MCP tools and build agent kwargs.
177
+ """Load MCP tools (cached by config) and build agent kwargs.
144
178
 
145
- Called on every ``create_cli_agent()`` call so that ``/new`` picks up
146
- MCP config changes. Falls back to base kwargs if no MCP configured.
179
+ Re-connects to MCP servers only when the effective MCP config changes.
180
+ Falls back to base kwargs if no MCP configured.
147
181
  """
148
- mcp_by_agent = load_mcp_tools()
182
+ mcp_by_agent = _load_mcp_tools_cached()
149
183
  if not mcp_by_agent:
150
184
  return _build_base_kwargs(base_backend, base_middleware)
151
185
 
@@ -176,6 +210,7 @@ def load_mcp_and_build_kwargs(base_backend, base_middleware):
176
210
  subagents=subs,
177
211
  middleware=base_middleware,
178
212
  system_prompt=SYSTEM_PROMPT,
213
+ skills=["/skills/"],
179
214
  )
180
215
 
181
216
 
@@ -184,8 +219,8 @@ prompt_refs = {
184
219
  }
185
220
 
186
221
  base_middleware = [
222
+ ToolErrorHandlerMiddleware(),
187
223
  create_memory_middleware(MEMORY_DIR, extraction_model=chat_model),
188
- create_skills_middleware(backend),
189
224
  ]
190
225
 
191
226
  # Default agent (no checkpointer) — used by langgraph dev / LangSmith / notebooks.
@@ -214,46 +249,66 @@ def __getattr__(name: str):
214
249
  def create_cli_agent(workspace_dir: str | None = None, checkpointer=None):
215
250
  """Create agent with checkpointer for CLI multi-turn support.
216
251
 
252
+ A fresh backend is constructed on every call using the current
253
+ ``paths.WORKSPACE_ROOT`` (or the explicit *workspace_dir*), so
254
+ runtime ``set_workspace_root()`` changes are always respected.
255
+
217
256
  Args:
218
- workspace_dir: Optional per-session workspace directory. If provided,
219
- creates a fresh backend rooted at this path. If None, uses the
220
- module-level default backend (./workspace).
221
- checkpointer: Optional LangGraph checkpointer. If None, falls back
222
- to ``InMemorySaver`` (non-persistent).
257
+ workspace_dir: Per-session workspace directory. If ``None``,
258
+ defaults to the current ``paths.WORKSPACE_ROOT``.
259
+ checkpointer: Optional LangGraph checkpointer. If ``None``,
260
+ falls back to ``InMemorySaver`` (non-persistent).
223
261
  """
262
+ import os as _os
263
+ from . import paths as _paths
264
+
224
265
  if checkpointer is None:
225
266
  from langgraph.checkpoint.memory import InMemorySaver # type: ignore[import-untyped]
226
267
  checkpointer = InMemorySaver()
227
268
 
228
- if workspace_dir:
229
- set_active_workspace(workspace_dir)
230
- ws_backend = CustomSandboxBackend(
231
- root_dir=workspace_dir,
232
- virtual_mode=True,
233
- timeout=300,
234
- )
235
- sk_backend = MergedReadOnlyBackend(
236
- primary_dir=USER_SKILLS_DIR,
237
- secondary_dir=SKILLS_DIR,
238
- )
239
- # Memory always uses SHARED directory (not per-session) for cross-session persistence
240
- mem_backend = FilesystemBackend(
241
- root_dir=MEMORY_DIR,
242
- virtual_mode=True,
243
- )
244
- be = CompositeBackend(
245
- default=ws_backend,
246
- routes={
247
- "/skills/": sk_backend,
248
- "/memory/": mem_backend,
249
- },
250
- )
251
- else:
252
- be = backend
269
+ # When no explicit workspace_dir is provided, apply config.default_workdir
270
+ # as a fallback. This covers direct callers (notebooks, iMessage server)
271
+ # that never call set_workspace_root() themselves. CLI callers always
272
+ # pass workspace_dir explicitly, so their --workdir is never overwritten.
273
+ if workspace_dir is None:
274
+ if _config.default_workdir:
275
+ set_workspace_root(
276
+ _os.path.abspath(_os.path.expanduser(_config.default_workdir))
277
+ )
278
+ workspace_dir = str(_paths.WORKSPACE_ROOT)
279
+
280
+ # Read paths dynamically so runtime set_workspace_root() changes are picked up
281
+ _mem_dir = str(_paths.MEMORY_DIR)
282
+ _usr_skills_dir = str(_paths.USER_SKILLS_DIR)
283
+
284
+ # Always construct fresh backends from current paths (avoids stale
285
+ # module-level backend when workspace root changed at runtime).
286
+ set_active_workspace(workspace_dir)
287
+ ws_backend = CustomSandboxBackend(
288
+ root_dir=workspace_dir,
289
+ virtual_mode=True,
290
+ timeout=300,
291
+ )
292
+ sk_backend = MergedReadOnlyBackend(
293
+ primary_dir=_usr_skills_dir,
294
+ secondary_dir=SKILLS_DIR,
295
+ )
296
+ # Memory always uses SHARED directory (not per-session) for cross-session persistence
297
+ mem_backend = FilesystemBackend(
298
+ root_dir=_mem_dir,
299
+ virtual_mode=True,
300
+ )
301
+ be = CompositeBackend(
302
+ default=ws_backend,
303
+ routes={
304
+ "/skills/": sk_backend,
305
+ "/memory/": mem_backend,
306
+ },
307
+ )
253
308
 
254
309
  mw = [
255
- create_memory_middleware(MEMORY_DIR, extraction_model=chat_model),
256
- create_skills_middleware(be),
310
+ ToolErrorHandlerMiddleware(),
311
+ create_memory_middleware(_mem_dir, extraction_model=chat_model),
257
312
  ]
258
313
 
259
314
  # Re-load MCP tools from current config (picks up /mcp add changes)
@@ -28,8 +28,6 @@ _EXPORTS: dict[str, tuple[str, str]] = {
28
28
  "MODELS": (".llm", "MODELS"),
29
29
  "list_models": (".llm", "list_models"),
30
30
  "DEFAULT_MODEL": (".llm", "DEFAULT_MODEL"),
31
- # Middleware
32
- "create_skills_middleware": (".middleware", "create_skills_middleware"),
33
31
  # Prompts
34
32
  "get_system_prompt": (".prompts", "get_system_prompt"),
35
33
  "RESEARCHER_INSTRUCTIONS": (".prompts", "RESEARCHER_INSTRUCTIONS"),
@@ -45,6 +43,17 @@ _EXPORTS: dict[str, tuple[str, str]] = {
45
43
 
46
44
 
47
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
+ """
48
57
  target = _EXPORTS.get(name)
49
58
  if target is None:
50
59
  raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -58,6 +67,7 @@ def __getattr__(name: str):
58
67
 
59
68
 
60
69
  def __dir__() -> list[str]:
70
+ """List available public attributes including lazy exports."""
61
71
  return sorted(set(globals()) | set(_EXPORTS))
62
72
 
63
73
 
@@ -0,0 +1,44 @@
1
+ """Communication channels for EvoScientist.
2
+
3
+ This module provides an extensible interface for different messaging channels
4
+ (iMessage, Telegram, Discord, Slack, WeChat) to communicate with the EvoScientist agent.
5
+ """
6
+
7
+ from .base import Channel, RawIncoming, IncomingMessage, OutgoingMessage, chunk_text
8
+ from .bus import MessageBus, InboundMessage, OutboundMessage
9
+ from .capabilities import ChannelCapabilities
10
+ from .channel_manager import ChannelManager, register_channel, create_channel, available_channels
11
+ from .consumer import InboundConsumer
12
+ from .formatter import UnifiedFormatter
13
+ from .middleware import TypingManager
14
+ from .plugin import ChannelPlugin, ChannelMeta, ReloadPolicy
15
+ from .standalone import run_standalone
16
+
17
+ # Backward compat: ChannelServer is now Channel itself
18
+ ChannelServer = Channel
19
+
20
+ __all__ = [
21
+ "Channel",
22
+ "ChannelServer",
23
+ "ChannelManager",
24
+ "MessageBus",
25
+ "RawIncoming",
26
+ "IncomingMessage",
27
+ "OutgoingMessage",
28
+ "InboundMessage",
29
+ "OutboundMessage",
30
+ "InboundConsumer",
31
+ "run_standalone",
32
+ "register_channel",
33
+ "create_channel",
34
+ "available_channels",
35
+ # New modules
36
+ "ChannelCapabilities",
37
+ "UnifiedFormatter",
38
+ "TypingManager",
39
+ "chunk_text",
40
+ # Plugin architecture
41
+ "ChannelPlugin",
42
+ "ChannelMeta",
43
+ "ReloadPolicy",
44
+ ]