EvoScientist 0.0.1.dev5__tar.gz → 0.0.1.dev7__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 (86) hide show
  1. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/EvoScientist.py +30 -11
  2. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/__init__.py +5 -1
  3. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/backends.py +29 -98
  4. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/serve.py +7 -7
  5. evoscientist-0.0.1.dev7/EvoScientist/cli/__init__.py +26 -0
  6. evoscientist-0.0.1.dev7/EvoScientist/cli/_app.py +47 -0
  7. evoscientist-0.0.1.dev7/EvoScientist/cli/agent.py +60 -0
  8. evoscientist-0.0.1.dev7/EvoScientist/cli/channel.py +289 -0
  9. evoscientist-0.0.1.dev7/EvoScientist/cli/commands.py +470 -0
  10. evoscientist-0.0.1.dev7/EvoScientist/cli/interactive.py +717 -0
  11. evoscientist-0.0.1.dev7/EvoScientist/cli/mcp_ui.py +282 -0
  12. evoscientist-0.0.1.dev7/EvoScientist/cli/skills_cmd.py +73 -0
  13. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/config.py +17 -0
  14. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/llm/__init__.py +2 -0
  15. evoscientist-0.0.1.dev7/EvoScientist/llm/models.py +193 -0
  16. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/mcp/__init__.py +5 -2
  17. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/mcp/client.py +131 -55
  18. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/memory.py +26 -50
  19. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/onboard.py +408 -178
  20. evoscientist-0.0.1.dev7/EvoScientist/sessions.py +345 -0
  21. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/stream/display.py +23 -18
  22. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/stream/events.py +59 -4
  23. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/tools/__init__.py +0 -2
  24. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/utils.py +3 -0
  25. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/PKG-INFO +27 -24
  26. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/SOURCES.txt +12 -2
  27. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/requires.txt +4 -3
  28. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/PKG-INFO +27 -24
  29. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/README.md +22 -20
  30. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/pyproject.toml +5 -4
  31. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_backends.py +6 -6
  32. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_llm.py +37 -46
  33. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_mcp_client.py +1 -1
  34. evoscientist-0.0.1.dev7/tests/test_memory_merge.py +73 -0
  35. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_onboard.py +67 -8
  36. evoscientist-0.0.1.dev7/tests/test_sessions.py +287 -0
  37. evoscientist-0.0.1.dev7/tests/test_stream_events.py +87 -0
  38. evoscientist-0.0.1.dev7/tests/test_tools.py +18 -0
  39. evoscientist-0.0.1.dev5/EvoScientist/cli.py +0 -1653
  40. evoscientist-0.0.1.dev5/EvoScientist/llm/models.py +0 -115
  41. evoscientist-0.0.1.dev5/EvoScientist/tools/image.py +0 -74
  42. evoscientist-0.0.1.dev5/tests/test_tools.py +0 -107
  43. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/__main__.py +0 -0
  44. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/channels/__init__.py +0 -0
  45. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/channels/base.py +0 -0
  46. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/__init__.py +0 -0
  47. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/channel_rpc.py +0 -0
  48. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/probe.py +0 -0
  49. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/rpc_client.py +0 -0
  50. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/targets.py +0 -0
  51. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/middleware.py +0 -0
  52. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/paths.py +0 -0
  53. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/prompts.py +0 -0
  54. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/skills/find-skills/SKILL.md +0 -0
  55. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/SKILL.md +0 -0
  56. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/references/output-patterns.md +0 -0
  57. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/references/workflows.md +0 -0
  58. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/scripts/init_skill.py +0 -0
  59. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/scripts/package_skill.py +0 -0
  60. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/scripts/quick_validate.py +0 -0
  61. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/stream/__init__.py +0 -0
  62. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/stream/emitter.py +0 -0
  63. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/stream/formatter.py +0 -0
  64. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/stream/state.py +0 -0
  65. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/stream/tracker.py +0 -0
  66. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/stream/utils.py +0 -0
  67. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/subagent.yaml +0 -0
  68. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/tools/search.py +0 -0
  69. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/tools/skill_manager.py +0 -0
  70. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/tools/skills_manager.py +0 -0
  71. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist/tools/think.py +0 -0
  72. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/dependency_links.txt +0 -0
  73. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/entry_points.txt +0 -0
  74. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/top_level.txt +0 -0
  75. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/LICENSE +0 -0
  76. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/setup.cfg +0 -0
  77. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_cli_run_name.py +0 -0
  78. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_config.py +0 -0
  79. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_event_loop.py +0 -0
  80. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_imports.py +0 -0
  81. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_prompts.py +0 -0
  82. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_skills_manager.py +0 -0
  83. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_stream_emitter.py +0 -0
  84. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_stream_state.py +0 -0
  85. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_stream_tracker.py +0 -0
  86. {evoscientist-0.0.1.dev5 → evoscientist-0.0.1.dev7}/tests/test_stream_utils.py +0 -0
@@ -26,7 +26,7 @@ from .mcp import load_mcp_tools
26
26
  from .middleware import create_skills_middleware, create_memory_middleware
27
27
  from .prompts import RESEARCHER_INSTRUCTIONS, get_system_prompt
28
28
  from .utils import load_subagents
29
- from .tools import tavily_search, think_tool, skill_manager, view_image
29
+ from .tools import tavily_search, think_tool, skill_manager
30
30
  from .paths import (
31
31
  ensure_dirs,
32
32
  default_workspace_dir,
@@ -115,11 +115,10 @@ backend = CompositeBackend(
115
115
  tool_registry = {
116
116
  "think_tool": think_tool,
117
117
  "tavily_search": tavily_search,
118
- "view_image": view_image,
119
118
  }
120
119
 
121
120
  # Base tools that every agent variant gets (before MCP)
122
- BASE_TOOLS = [think_tool, skill_manager, view_image]
121
+ BASE_TOOLS = [think_tool, skill_manager]
123
122
 
124
123
 
125
124
  def _build_base_kwargs(base_backend, base_middleware):
@@ -190,21 +189,41 @@ base_middleware = [
190
189
  ]
191
190
 
192
191
  # Default agent (no checkpointer) — used by langgraph dev / LangSmith / notebooks.
193
- # Built WITHOUT MCP at import time to avoid spawning subprocesses on every import.
194
- # MCP tools are loaded on-demand in create_cli_agent().
195
- _AGENT_KWARGS = _build_base_kwargs(backend, base_middleware)
196
- EvoScientist_agent = create_deep_agent(**_AGENT_KWARGS).with_config({"recursion_limit": 500})
192
+ # Lazily constructed on first access so MCP tools are included without
193
+ # spawning subprocesses at import time.
194
+ _EvoScientist_agent = None
195
+
196
+
197
+ def _get_default_agent():
198
+ """Build the default agent (with MCP, no checkpointer) on first access."""
199
+ global _EvoScientist_agent
200
+ if _EvoScientist_agent is None:
201
+ kwargs = load_mcp_and_build_kwargs(backend, base_middleware)
202
+ _EvoScientist_agent = create_deep_agent(**kwargs).with_config(
203
+ {"recursion_limit": 500}
204
+ )
205
+ return _EvoScientist_agent
206
+
207
+
208
+ def __getattr__(name: str):
209
+ if name == "EvoScientist_agent":
210
+ return _get_default_agent()
211
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
197
212
 
198
213
 
199
- def create_cli_agent(workspace_dir: str | None = None):
200
- """Create agent with InMemorySaver checkpointer for CLI multi-turn support.
214
+ def create_cli_agent(workspace_dir: str | None = None, checkpointer=None):
215
+ """Create agent with checkpointer for CLI multi-turn support.
201
216
 
202
217
  Args:
203
218
  workspace_dir: Optional per-session workspace directory. If provided,
204
219
  creates a fresh backend rooted at this path. If None, uses the
205
220
  module-level default backend (./workspace).
221
+ checkpointer: Optional LangGraph checkpointer. If None, falls back
222
+ to ``InMemorySaver`` (non-persistent).
206
223
  """
207
- from langgraph.checkpoint.memory import InMemorySaver # type: ignore[import-untyped]
224
+ if checkpointer is None:
225
+ from langgraph.checkpoint.memory import InMemorySaver # type: ignore[import-untyped]
226
+ checkpointer = InMemorySaver()
208
227
 
209
228
  if workspace_dir:
210
229
  set_active_workspace(workspace_dir)
@@ -242,5 +261,5 @@ def create_cli_agent(workspace_dir: str | None = None):
242
261
 
243
262
  return create_deep_agent(
244
263
  **kwargs,
245
- checkpointer=InMemorySaver(),
264
+ checkpointer=checkpointer,
246
265
  ).with_config({"recursion_limit": 500})
@@ -36,7 +36,11 @@ _EXPORTS: dict[str, tuple[str, str]] = {
36
36
  # Tools
37
37
  "tavily_search": (".tools", "tavily_search"),
38
38
  "think_tool": (".tools", "think_tool"),
39
- "view_image": (".tools", "view_image"),
39
+ # Sessions
40
+ "get_checkpointer": (".sessions", "get_checkpointer"),
41
+ "generate_thread_id": (".sessions", "generate_thread_id"),
42
+ "list_threads": (".sessions", "list_threads"),
43
+ "delete_thread": (".sessions", "delete_thread"),
40
44
  }
41
45
 
42
46
 
@@ -2,18 +2,16 @@
2
2
 
3
3
  import os
4
4
  import re
5
- import subprocess
6
5
  import uuid
7
6
  from pathlib import Path
8
7
 
9
- from deepagents.backends import FilesystemBackend
8
+ from deepagents.backends import FilesystemBackend, LocalShellBackend
10
9
  from deepagents.backends.filesystem import WriteResult, EditResult
11
10
  from deepagents.backends.protocol import (
12
11
  BackendProtocol,
13
12
  ExecuteResponse,
14
13
  FileDownloadResponse,
15
14
  FileUploadResponse,
16
- SandboxBackendProtocol,
17
15
  )
18
16
 
19
17
  # System path prefixes that should never appear in virtual paths.
@@ -226,25 +224,24 @@ class MergedReadOnlyBackend(BackendProtocol):
226
224
  ]
227
225
 
228
226
 
229
- class CustomSandboxBackend(FilesystemBackend, SandboxBackendProtocol):
227
+ class CustomSandboxBackend(LocalShellBackend):
230
228
  """
231
- Custom sandbox backend - inherits FilesystemBackend and implements execute method.
229
+ Custom sandbox backend - inherits LocalShellBackend with added safety.
232
230
 
233
231
  Features:
234
232
  - Inherits all file operations (ls, read, write, edit, grep, glob)
235
- - Adds shell command execution capability
236
- - Command validation prevents directory traversal and dangerous operations
237
- - Runs commands in specified working directory
233
+ - Inherits shell command execution with output truncation and timeout
234
+ - Adds command validation to prevent directory traversal and dangerous operations
235
+ - Adds path sanitization to auto-correct common LLM path mistakes
238
236
  - Compatible with LangGraph checkpointer (no thread locks)
239
237
  """
240
238
 
241
239
  def __init__(
242
240
  self,
243
241
  root_dir: str = ".",
242
+ *,
244
243
  virtual_mode: bool = True,
245
- working_dir: str | None = None,
246
244
  timeout: int = 300,
247
- shell: str = "/bin/bash",
248
245
  max_output_bytes: int = 100_000,
249
246
  env: dict[str, str] | None = None,
250
247
  inherit_env: bool = True,
@@ -255,35 +252,23 @@ class CustomSandboxBackend(FilesystemBackend, SandboxBackendProtocol):
255
252
  Args:
256
253
  root_dir: File system root directory
257
254
  virtual_mode: Whether to enable virtual path mode
258
- working_dir: Working directory for command execution (defaults to root_dir)
259
255
  timeout: Command execution timeout in seconds
260
- shell: Shell program to use
261
256
  max_output_bytes: Max output size before truncation (default 100KB)
262
257
  env: Extra environment variables for subprocess
263
258
  inherit_env: Whether to inherit parent process env (default True)
264
259
  """
265
- super().__init__(root_dir=root_dir, virtual_mode=virtual_mode)
266
-
260
+ super().__init__(
261
+ root_dir=root_dir,
262
+ virtual_mode=virtual_mode,
263
+ timeout=timeout,
264
+ max_output_bytes=max_output_bytes,
265
+ env=env,
266
+ inherit_env=inherit_env,
267
+ )
268
+ # Override parent's "local-" prefix with our own
267
269
  self._sandbox_id = f"evosci-{uuid.uuid4().hex[:8]}"
268
- self.working_dir = working_dir or root_dir
269
- self.timeout = timeout
270
- self.shell = shell
271
- self.virtual_mode = virtual_mode
272
- self._max_output_bytes = max_output_bytes
273
-
274
- # Build subprocess environment
275
- if inherit_env:
276
- self._env = {**os.environ, **(env or {})}
277
- else:
278
- self._env = dict(env) if env else {}
279
-
280
270
  # Ensure working directory exists
281
- os.makedirs(self.working_dir, exist_ok=True)
282
-
283
- @property
284
- def id(self) -> str:
285
- """Unique identifier for the sandbox backend instance."""
286
- return self._sandbox_id
271
+ os.makedirs(str(self.cwd), exist_ok=True)
287
272
 
288
273
  def _resolve_path(self, key: str) -> Path:
289
274
  """Resolve path with sanitization to prevent nested directories.
@@ -326,74 +311,20 @@ class CustomSandboxBackend(FilesystemBackend, SandboxBackendProtocol):
326
311
  - Access to paths outside workspace
327
312
  - Dangerous system commands
328
313
 
329
- Args:
330
- command: Command string to execute
331
-
332
- Returns:
333
- ExecuteResponse containing output, exit_code, and truncated flag
314
+ Then delegates to LocalShellBackend.execute() for actual execution.
334
315
  """
335
- try:
336
- # Validate command safety
337
- error = validate_command(command)
338
- if error:
339
- return ExecuteResponse(
340
- output=error,
341
- exit_code=1,
342
- truncated=False,
343
- )
344
-
345
- # Convert virtual paths to relative paths
346
- if self.virtual_mode:
347
- command = convert_virtual_paths_in_command(command=command)
348
-
349
- result = subprocess.run(
350
- command,
351
- shell=True,
352
- executable=self.shell,
353
- cwd=self.working_dir,
354
- capture_output=True,
355
- text=True,
356
- timeout=self.timeout,
357
- env=self._env,
358
- )
359
-
360
- output_parts = []
361
- if result.stdout:
362
- output_parts.append(result.stdout)
363
- if result.stderr:
364
- stderr_lines = result.stderr.strip().split("\n")
365
- output_parts.extend(f"[stderr] {line}" for line in stderr_lines)
366
- output = "\n".join(output_parts) if output_parts else ""
367
-
368
- if result.returncode != 0:
369
- output = f"{output.rstrip()}\n\nExit code: {result.returncode}"
370
-
371
- truncated = False
372
- if len(output) > self._max_output_bytes:
373
- output = output[:self._max_output_bytes]
374
- output += f"\n\n... Output truncated at {self._max_output_bytes} bytes."
375
- truncated = True
376
-
377
- return ExecuteResponse(
378
- output=output,
379
- exit_code=result.returncode,
380
- truncated=truncated,
381
- )
382
-
383
- except subprocess.TimeoutExpired:
316
+ # Validate command safety
317
+ error = validate_command(command)
318
+ if error:
384
319
  return ExecuteResponse(
385
- output=f"Command timed out after {self.timeout} seconds",
386
- exit_code=-1,
387
- truncated=False,
388
- )
389
- except Exception as e:
390
- return ExecuteResponse(
391
- output=f"Error executing command: {str(e)}",
392
- exit_code=-1,
320
+ output=error,
321
+ exit_code=1,
393
322
  truncated=False,
394
323
  )
395
324
 
396
- async def aexecute(self, command: str) -> ExecuteResponse:
397
- """Async version of execute (runs sync version in thread)."""
398
- import asyncio
399
- return await asyncio.to_thread(self.execute, command)
325
+ # Convert virtual paths to relative paths
326
+ if self.virtual_mode:
327
+ command = convert_virtual_paths_in_command(command=command)
328
+
329
+ # Delegate to parent for subprocess execution
330
+ return super().execute(command)
@@ -25,11 +25,6 @@ from typing import Callable
25
25
  from . import IMessageChannel, IMessageConfig
26
26
  from ..base import OutgoingMessage
27
27
 
28
- logging.basicConfig(
29
- level=logging.DEBUG,
30
- format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
31
- datefmt="%H:%M:%S",
32
- )
33
28
  logger = logging.getLogger(__name__)
34
29
 
35
30
 
@@ -223,7 +218,7 @@ class IMessageServer:
223
218
  await self.channel.send(OutgoingMessage(
224
219
  recipient=sender,
225
220
  content=response,
226
- metadata=metadata,
221
+ metadata=metadata or {},
227
222
  ))
228
223
  if self._on_activity:
229
224
  try:
@@ -387,7 +382,7 @@ async def async_main():
387
382
  config = IMessageConfig(
388
383
  cli_path=args.cli_path,
389
384
  db_path=args.db_path,
390
- allowed_senders=set(args.allowed_senders) if args.allowed_senders else None,
385
+ allowed_senders=list(args.allowed_senders) if args.allowed_senders else [],
391
386
  include_attachments=args.attachments,
392
387
  )
393
388
 
@@ -421,6 +416,11 @@ async def async_main():
421
416
 
422
417
  def main():
423
418
  """Entry point."""
419
+ logging.basicConfig(
420
+ level=logging.DEBUG,
421
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
422
+ datefmt="%H:%M:%S",
423
+ )
424
424
  asyncio.run(async_main())
425
425
 
426
426
 
@@ -0,0 +1,26 @@
1
+ """EvoScientist CLI package."""
2
+
3
+ # Backward-compat re-exports (tests import these from EvoScientist.cli)
4
+ from ..stream.state import ( # noqa: F401
5
+ SubAgentState,
6
+ StreamState,
7
+ _parse_todo_items,
8
+ _build_todo_stats,
9
+ )
10
+ from .channel import ChannelMessage, _ChannelState # noqa: F401
11
+ from .agent import _deduplicate_run_name # noqa: F401
12
+
13
+ from ._app import app # noqa: F401
14
+ from . import commands # noqa: F401 — registers @app.command decorators
15
+
16
+
17
+ def main():
18
+ """CLI entry point."""
19
+ import warnings
20
+
21
+ warnings.filterwarnings("ignore", message=".*not known to support tools.*")
22
+ warnings.filterwarnings("ignore", message=".*type is unknown and inference may fail.*")
23
+ from .commands import _configure_logging
24
+
25
+ _configure_logging()
26
+ app()
@@ -0,0 +1,47 @@
1
+ """Typer application objects — no intra-package imports to avoid circular deps."""
2
+
3
+ import typer # type: ignore[import-untyped]
4
+
5
+ app = typer.Typer(
6
+ no_args_is_help=False,
7
+ add_completion=False,
8
+ context_settings={"help_option_names": ["-h", "--help"]},
9
+ )
10
+
11
+ # Config subcommand group
12
+ config_app = typer.Typer(help="Configuration management commands", invoke_without_command=True)
13
+ app.add_typer(config_app, name="config")
14
+
15
+ # MCP subcommand group
16
+ _MCP_HELP = """\
17
+ Configure and manage MCP servers
18
+
19
+ Examples:
20
+ # Add a local MCP server (stdio auto-detected):
21
+ EvoSci mcp add local-server python -- /path/to/server.py
22
+
23
+ # Add an npx-based server:
24
+ EvoSci mcp add sequential-thinking npx -- -y @modelcontextprotocol/server-sequential-thinking
25
+
26
+ # Add an HTTP server (http auto-detected from URL):
27
+ EvoSci mcp add docs-langchain https://docs.langchain.com/mcp
28
+
29
+ # Add a stdio server with env vars (hardcoded):
30
+ EvoSci mcp add my-server node --env API_KEY=xxx -- server.js
31
+
32
+ # Add a server with runtime env ref (resolved from .env at startup):
33
+ EvoSci mcp add brave-search npx --env-ref BRAVE_API_KEY -- -y @modelcontextprotocol/server-brave-search
34
+
35
+ # Expose to a specific sub-agent (e.g. research-agent):
36
+ EvoSci mcp add brave-search npx --env-ref BRAVE_API_KEY -e research-agent -- -y @modelcontextprotocol/server-brave-search
37
+
38
+ # Expose to multiple agents:
39
+ EvoSci mcp add local-server python -e main,research-agent,code-agent -- /path/to/server.py
40
+
41
+ # Explicit transport override:
42
+ EvoSci mcp add my-sse https://example.com/sse --transport sse
43
+
44
+ Sub-agents (-e): planner-agent | research-agent | code-agent | debug-agent | data-analysis-agent | writing-agent
45
+ """
46
+ mcp_app = typer.Typer(help=_MCP_HELP, invoke_without_command=True)
47
+ app.add_typer(mcp_app, name="mcp")
@@ -0,0 +1,60 @@
1
+ """Agent loading and workspace helpers."""
2
+
3
+ import os
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+
7
+ from ..paths import new_run_dir, RUNS_DIR
8
+
9
+
10
+ def _shorten_path(path: str) -> str:
11
+ """Shorten absolute path to relative path from current directory."""
12
+ if not path:
13
+ return path
14
+ try:
15
+ cwd = os.getcwd()
16
+ if path.startswith(cwd):
17
+ rel = path[len(cwd):].lstrip(os.sep)
18
+ return os.path.join(os.path.basename(cwd), rel) if rel else os.path.basename(cwd)
19
+ return path
20
+ except Exception:
21
+ return path
22
+
23
+
24
+ def _deduplicate_run_name(name: str, runs_dir: Path = RUNS_DIR) -> str:
25
+ """Return *name* if available, otherwise *name_1*, *name_2*, etc."""
26
+ if not (runs_dir / name).exists():
27
+ return name
28
+ i = 1
29
+ while (runs_dir / f"{name}_{i}").exists():
30
+ i += 1
31
+ return f"{name}_{i}"
32
+
33
+
34
+ def _create_session_workspace(name: str | None = None) -> str:
35
+ """Create a per-session workspace directory and return its path.
36
+
37
+ Args:
38
+ name: Optional human-friendly run name. Duplicates are resolved
39
+ by appending ``_1``, ``_2``, etc. Falls back to a timestamp
40
+ if *name* is None.
41
+ """
42
+ if name:
43
+ session_id = _deduplicate_run_name(name)
44
+ else:
45
+ session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
46
+ workspace_dir = str(new_run_dir(session_id))
47
+ os.makedirs(workspace_dir, exist_ok=True)
48
+ return workspace_dir
49
+
50
+
51
+ def _load_agent(workspace_dir: str | None = None, checkpointer=None):
52
+ """Load the CLI agent with optional persistent checkpointer.
53
+
54
+ Args:
55
+ workspace_dir: Optional per-session workspace directory.
56
+ checkpointer: Optional LangGraph checkpointer (e.g. ``AsyncSqliteSaver``).
57
+ Falls back to ``InMemorySaver`` when ``None``.
58
+ """
59
+ from ..EvoScientist import create_cli_agent
60
+ return create_cli_agent(workspace_dir=workspace_dir, checkpointer=checkpointer)