echo-agent 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. echo_agent/__init__.py +5 -0
  2. echo_agent/__main__.py +538 -0
  3. echo_agent/_bundled/skills/development/plan/SKILL.md +54 -0
  4. echo_agent/_bundled/skills/development/skill-creator/SKILL.md +270 -0
  5. echo_agent/_bundled/skills/development/skill-creator/scripts/init_skill.py +226 -0
  6. echo_agent/_bundled/skills/development/skill-creator/scripts/package_skill.py +146 -0
  7. echo_agent/_bundled/skills/development/skill-creator/scripts/quick_validate.py +222 -0
  8. echo_agent/_bundled/skills/productivity/summarize/SKILL.md +67 -0
  9. echo_agent/_bundled/skills/productivity/weather/SKILL.md +49 -0
  10. echo_agent/_bundled/skills/research/arxiv/SKILL.md +232 -0
  11. echo_agent/_bundled/skills/research/arxiv/scripts/search_arxiv.py +115 -0
  12. echo_agent/a2a/__init__.py +5 -0
  13. echo_agent/a2a/client.py +66 -0
  14. echo_agent/a2a/models.py +98 -0
  15. echo_agent/a2a/protocol.py +85 -0
  16. echo_agent/a2a/server.py +71 -0
  17. echo_agent/agent/__init__.py +0 -0
  18. echo_agent/agent/approval_gate.py +326 -0
  19. echo_agent/agent/compression/__init__.py +14 -0
  20. echo_agent/agent/compression/assembler.py +45 -0
  21. echo_agent/agent/compression/boundary.py +141 -0
  22. echo_agent/agent/compression/compressor.py +181 -0
  23. echo_agent/agent/compression/engine.py +88 -0
  24. echo_agent/agent/compression/pruner.py +150 -0
  25. echo_agent/agent/compression/summarizer.py +181 -0
  26. echo_agent/agent/compression/types.py +41 -0
  27. echo_agent/agent/compression/validator.py +96 -0
  28. echo_agent/agent/consolidation.py +96 -0
  29. echo_agent/agent/context.py +403 -0
  30. echo_agent/agent/executors/__init__.py +0 -0
  31. echo_agent/agent/executors/base.py +211 -0
  32. echo_agent/agent/executors/factory.py +34 -0
  33. echo_agent/agent/executors/remote.py +193 -0
  34. echo_agent/agent/loop.py +891 -0
  35. echo_agent/agent/multi_agent/__init__.py +15 -0
  36. echo_agent/agent/multi_agent/audit.py +19 -0
  37. echo_agent/agent/multi_agent/error_messages.py +35 -0
  38. echo_agent/agent/multi_agent/error_types.py +36 -0
  39. echo_agent/agent/multi_agent/models.py +37 -0
  40. echo_agent/agent/multi_agent/registry.py +41 -0
  41. echo_agent/agent/multi_agent/runtime.py +201 -0
  42. echo_agent/agent/pipeline/__init__.py +14 -0
  43. echo_agent/agent/pipeline/context_stage.py +219 -0
  44. echo_agent/agent/pipeline/inference_stage.py +433 -0
  45. echo_agent/agent/pipeline/response_stage.py +146 -0
  46. echo_agent/agent/pipeline/types.py +40 -0
  47. echo_agent/agent/planning/__init__.py +4 -0
  48. echo_agent/agent/planning/models.py +83 -0
  49. echo_agent/agent/planning/planner.py +57 -0
  50. echo_agent/agent/planning/reflection.py +54 -0
  51. echo_agent/agent/planning/strategies.py +183 -0
  52. echo_agent/agent/tools/__init__.py +167 -0
  53. echo_agent/agent/tools/base.py +149 -0
  54. echo_agent/agent/tools/circuit_breaker.py +82 -0
  55. echo_agent/agent/tools/clarify.py +42 -0
  56. echo_agent/agent/tools/code_exec.py +147 -0
  57. echo_agent/agent/tools/cronjob.py +93 -0
  58. echo_agent/agent/tools/delegate.py +393 -0
  59. echo_agent/agent/tools/filesystem.py +180 -0
  60. echo_agent/agent/tools/image_gen.py +65 -0
  61. echo_agent/agent/tools/knowledge.py +81 -0
  62. echo_agent/agent/tools/memory.py +198 -0
  63. echo_agent/agent/tools/message.py +39 -0
  64. echo_agent/agent/tools/notify.py +35 -0
  65. echo_agent/agent/tools/patch.py +178 -0
  66. echo_agent/agent/tools/process.py +139 -0
  67. echo_agent/agent/tools/registry.py +185 -0
  68. echo_agent/agent/tools/search.py +99 -0
  69. echo_agent/agent/tools/session_search.py +76 -0
  70. echo_agent/agent/tools/shell.py +164 -0
  71. echo_agent/agent/tools/skill_install.py +255 -0
  72. echo_agent/agent/tools/skills.py +177 -0
  73. echo_agent/agent/tools/task.py +104 -0
  74. echo_agent/agent/tools/todo.py +148 -0
  75. echo_agent/agent/tools/tts.py +77 -0
  76. echo_agent/agent/tools/vision.py +71 -0
  77. echo_agent/agent/tools/web.py +208 -0
  78. echo_agent/agent/tools/workflow.py +89 -0
  79. echo_agent/bus/__init__.py +11 -0
  80. echo_agent/bus/events.py +193 -0
  81. echo_agent/bus/queue.py +158 -0
  82. echo_agent/bus/rate_limiter.py +51 -0
  83. echo_agent/channels/__init__.py +0 -0
  84. echo_agent/channels/base.py +185 -0
  85. echo_agent/channels/cli.py +149 -0
  86. echo_agent/channels/cron.py +44 -0
  87. echo_agent/channels/dingtalk.py +195 -0
  88. echo_agent/channels/discord.py +359 -0
  89. echo_agent/channels/email.py +168 -0
  90. echo_agent/channels/feishu.py +240 -0
  91. echo_agent/channels/manager.py +417 -0
  92. echo_agent/channels/matrix.py +281 -0
  93. echo_agent/channels/qqbot.py +638 -0
  94. echo_agent/channels/qqbot_media.py +482 -0
  95. echo_agent/channels/slack.py +297 -0
  96. echo_agent/channels/telegram.py +275 -0
  97. echo_agent/channels/webhook.py +106 -0
  98. echo_agent/channels/wecom.py +152 -0
  99. echo_agent/channels/weixin.py +603 -0
  100. echo_agent/channels/whatsapp.py +138 -0
  101. echo_agent/cli/__init__.py +0 -0
  102. echo_agent/cli/colors.py +42 -0
  103. echo_agent/cli/evolution_cmd.py +299 -0
  104. echo_agent/cli/i18n/__init__.py +123 -0
  105. echo_agent/cli/i18n/en.py +275 -0
  106. echo_agent/cli/i18n/zh.py +275 -0
  107. echo_agent/cli/plugins_cmd.py +205 -0
  108. echo_agent/cli/prompt.py +102 -0
  109. echo_agent/cli/service.py +156 -0
  110. echo_agent/cli/setup.py +1111 -0
  111. echo_agent/cli/status.py +93 -0
  112. echo_agent/config/__init__.py +8 -0
  113. echo_agent/config/default.yaml +199 -0
  114. echo_agent/config/loader.py +125 -0
  115. echo_agent/config/schema.py +652 -0
  116. echo_agent/evaluation/__init__.py +4 -0
  117. echo_agent/evaluation/dataset.py +66 -0
  118. echo_agent/evaluation/metrics.py +70 -0
  119. echo_agent/evaluation/reporter.py +42 -0
  120. echo_agent/evaluation/runner.py +143 -0
  121. echo_agent/evolution/__init__.py +38 -0
  122. echo_agent/evolution/engine.py +335 -0
  123. echo_agent/evolution/evolver.py +397 -0
  124. echo_agent/evolution/gate.py +413 -0
  125. echo_agent/evolution/recorder.py +288 -0
  126. echo_agent/evolution/scheduler.py +133 -0
  127. echo_agent/evolution/store.py +331 -0
  128. echo_agent/evolution/tools.py +110 -0
  129. echo_agent/evolution/types.py +270 -0
  130. echo_agent/gateway/__init__.py +7 -0
  131. echo_agent/gateway/auth.py +178 -0
  132. echo_agent/gateway/editor.py +121 -0
  133. echo_agent/gateway/health.py +51 -0
  134. echo_agent/gateway/hooks.py +86 -0
  135. echo_agent/gateway/media.py +137 -0
  136. echo_agent/gateway/rate_limiter.py +72 -0
  137. echo_agent/gateway/router.py +86 -0
  138. echo_agent/gateway/server.py +570 -0
  139. echo_agent/gateway/session_context.py +57 -0
  140. echo_agent/gateway/session_policy.py +47 -0
  141. echo_agent/gateway/static/index.html +432 -0
  142. echo_agent/knowledge/__init__.py +5 -0
  143. echo_agent/knowledge/index.py +308 -0
  144. echo_agent/mcp/__init__.py +3 -0
  145. echo_agent/mcp/client.py +158 -0
  146. echo_agent/mcp/manager.py +161 -0
  147. echo_agent/mcp/oauth.py +208 -0
  148. echo_agent/mcp/security.py +79 -0
  149. echo_agent/mcp/tool_adapter.py +73 -0
  150. echo_agent/mcp/transport.py +353 -0
  151. echo_agent/memory/__init__.py +0 -0
  152. echo_agent/memory/consolidator.py +273 -0
  153. echo_agent/memory/contradiction.py +287 -0
  154. echo_agent/memory/forgetting.py +114 -0
  155. echo_agent/memory/retrieval.py +184 -0
  156. echo_agent/memory/reviewer.py +192 -0
  157. echo_agent/memory/store.py +706 -0
  158. echo_agent/memory/tiers.py +243 -0
  159. echo_agent/memory/types.py +168 -0
  160. echo_agent/memory/vectors.py +148 -0
  161. echo_agent/models/__init__.py +0 -0
  162. echo_agent/models/credential_pool.py +86 -0
  163. echo_agent/models/inference.py +98 -0
  164. echo_agent/models/provider.py +208 -0
  165. echo_agent/models/providers/__init__.py +209 -0
  166. echo_agent/models/providers/anthropic_provider.py +164 -0
  167. echo_agent/models/providers/bedrock_provider.py +261 -0
  168. echo_agent/models/providers/format_utils.py +198 -0
  169. echo_agent/models/providers/gemini_provider.py +159 -0
  170. echo_agent/models/providers/openai_provider.py +253 -0
  171. echo_agent/models/providers/openrouter_provider.py +38 -0
  172. echo_agent/models/rate_limiter.py +75 -0
  173. echo_agent/models/router.py +325 -0
  174. echo_agent/models/tokenizer.py +111 -0
  175. echo_agent/observability/__init__.py +0 -0
  176. echo_agent/observability/monitor.py +209 -0
  177. echo_agent/observability/spans.py +75 -0
  178. echo_agent/observability/telemetry.py +86 -0
  179. echo_agent/permissions/__init__.py +0 -0
  180. echo_agent/permissions/allowlist.py +97 -0
  181. echo_agent/permissions/manager.py +460 -0
  182. echo_agent/plugins/__init__.py +30 -0
  183. echo_agent/plugins/context.py +145 -0
  184. echo_agent/plugins/errors.py +23 -0
  185. echo_agent/plugins/hooks.py +126 -0
  186. echo_agent/plugins/loader.py +251 -0
  187. echo_agent/plugins/manager.py +216 -0
  188. echo_agent/plugins/manifest.py +70 -0
  189. echo_agent/runtime_paths.py +25 -0
  190. echo_agent/scheduler/__init__.py +0 -0
  191. echo_agent/scheduler/delivery.py +63 -0
  192. echo_agent/scheduler/service.py +398 -0
  193. echo_agent/security/__init__.py +11 -0
  194. echo_agent/security/capabilities.py +54 -0
  195. echo_agent/security/guards.py +265 -0
  196. echo_agent/security/path_policy.py +212 -0
  197. echo_agent/security/risk_classifier.py +75 -0
  198. echo_agent/security/smart_approval.py +60 -0
  199. echo_agent/security/tool_policy.py +159 -0
  200. echo_agent/session/__init__.py +0 -0
  201. echo_agent/session/manager.py +404 -0
  202. echo_agent/skills/__init__.py +0 -0
  203. echo_agent/skills/manager.py +279 -0
  204. echo_agent/skills/reviewer.py +163 -0
  205. echo_agent/skills/store.py +358 -0
  206. echo_agent/storage/__init__.py +0 -0
  207. echo_agent/storage/backend.py +111 -0
  208. echo_agent/storage/sqlite.py +523 -0
  209. echo_agent/tasks/__init__.py +20 -0
  210. echo_agent/tasks/manager.py +108 -0
  211. echo_agent/tasks/models.py +180 -0
  212. echo_agent/tasks/workflow.py +182 -0
  213. echo_agent/utils/__init__.py +0 -0
  214. echo_agent/utils/async_io.py +80 -0
  215. echo_agent/utils/text.py +91 -0
  216. echo_agent-0.1.0.dist-info/METADATA +286 -0
  217. echo_agent-0.1.0.dist-info/RECORD +219 -0
  218. echo_agent-0.1.0.dist-info/WHEEL +4 -0
  219. echo_agent-0.1.0.dist-info/entry_points.txt +2 -0
echo_agent/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Echo Agent — a modular AI agent framework."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
echo_agent/__main__.py ADDED
@@ -0,0 +1,538 @@
1
+ """Echo Agent entry point — bootstraps all subsystems and runs the agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import signal
8
+ import sys
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Any, Callable
12
+
13
+ from loguru import logger
14
+
15
+
16
+ def _configure_logging(level: str) -> None:
17
+ logger.remove()
18
+ logger.add(sys.stderr, level=level, format="<green>{time:HH:mm:ss}</green> | <level>{level:<7}</level> | {message}")
19
+
20
+
21
+ @dataclass
22
+ class _BootstrapResult:
23
+ config: Any = None
24
+ workspace: Path = field(default_factory=lambda: Path("."))
25
+ storage: Any = None
26
+ bus: Any = None
27
+ router: Any = None
28
+ provider: Any = None
29
+ agent: Any = None
30
+ channels: Any = None
31
+ scheduler: Any = None
32
+ health: Any = None
33
+
34
+
35
+ async def _bootstrap(
36
+ config_path: str | None = None,
37
+ overrides: dict[str, Any] | None = None,
38
+ on_cli_exit: Callable[[], None] | None = None,
39
+ ) -> _BootstrapResult:
40
+ """Shared bootstrap: config → storage → providers → bus → agent → channels."""
41
+ from echo_agent.agent.loop import AgentLoop
42
+ from echo_agent.bus.queue import MessageBus
43
+ from echo_agent.channels.manager import ChannelManager
44
+ from echo_agent.config.loader import load_config, resolve_config_file
45
+ from echo_agent.models.provider import LLMProvider, LLMResponse
46
+ from echo_agent.models.providers import create_provider
47
+ from echo_agent.models.router import ModelRouter
48
+ from echo_agent.observability.monitor import HealthChecker
49
+ from echo_agent.scheduler.delivery import build_scheduled_job_handler
50
+ from echo_agent.storage.sqlite import SQLiteBackend
51
+
52
+ config_file = resolve_config_file(config_path)
53
+ config = load_config(config_path=config_file, overrides=overrides)
54
+ _configure_logging(config.observability.log_level)
55
+
56
+ workspace_value = Path(config.workspace).expanduser()
57
+ if not workspace_value.is_absolute():
58
+ workspace_base = Path.cwd() if overrides and "workspace" in overrides else (config_file.parent if config_file else Path.cwd())
59
+ workspace_value = workspace_base / workspace_value
60
+ ws = workspace_value.resolve()
61
+ ws.mkdir(parents=True, exist_ok=True)
62
+
63
+ storage = SQLiteBackend(ws / config.storage.database_path)
64
+ await storage.initialize()
65
+
66
+ bus = MessageBus()
67
+ router = ModelRouter(config.models)
68
+ provider: LLMProvider | None = None
69
+ provider_errors: list[str] = []
70
+
71
+ for pc in config.models.providers:
72
+ try:
73
+ p = create_provider(pc, default_model=config.models.default_model)
74
+ router.register_provider(pc.name, p)
75
+ if provider is None:
76
+ provider = p
77
+ logger.info("Registered provider: {}", pc.name)
78
+ except Exception as e:
79
+ provider_errors.append(f"{pc.name or '<unnamed>'}: {e}")
80
+ logger.warning("Failed to create provider '{}': {}", pc.name, e)
81
+
82
+ if provider is None:
83
+ if config.models.providers:
84
+ details = "; ".join(provider_errors) or "all configured providers were skipped"
85
+ stub_message = (
86
+ "[No LLM provider could be initialized. Check provider SDK/API key. "
87
+ f"Details: {details}]"
88
+ )
89
+ logger.warning("No providers initialized — using stub: {}", details)
90
+ else:
91
+ stub_message = "[No LLM provider configured. Set up a provider in echo-agent.yaml]"
92
+ logger.warning("No providers configured — using stub")
93
+
94
+ class _StubProvider(LLMProvider):
95
+ async def chat(self, messages, tools=None, model=None, tool_choice=None, **kw):
96
+ return LLMResponse(content=stub_message)
97
+ def get_default_model(self):
98
+ return "stub"
99
+ provider = _StubProvider()
100
+ router.register_provider("stub", provider)
101
+
102
+ from echo_agent.scheduler.service import Scheduler
103
+ scheduler: Scheduler | None = None
104
+ if config.scheduler.enabled:
105
+ scheduler = Scheduler(
106
+ store_path=ws / "data" / "scheduler.json",
107
+ on_job=build_scheduled_job_handler(bus),
108
+ max_concurrent=config.scheduler.max_concurrent_jobs,
109
+ )
110
+
111
+ from echo_agent.tasks.manager import TaskManager
112
+ from echo_agent.tasks.workflow import WorkflowEngine
113
+ task_manager = TaskManager(storage)
114
+ workflow_engine = WorkflowEngine(storage, task_manager)
115
+
116
+ agent = AgentLoop(
117
+ bus=bus, config=config, provider=provider, workspace=ws,
118
+ router=router,
119
+ scheduler=scheduler, storage=storage,
120
+ task_manager=task_manager, workflow_engine=workflow_engine,
121
+ )
122
+
123
+ # Plugin system — discover and activate plugins
124
+ from echo_agent.plugins.manager import PluginManager
125
+
126
+ plugin_manager = PluginManager(
127
+ config=config,
128
+ workspace=ws,
129
+ bus=bus,
130
+ tool_registry=agent.tools,
131
+ provider=provider,
132
+ )
133
+ await plugin_manager.discover_and_load()
134
+ agent.set_plugin_manager(plugin_manager)
135
+
136
+ # Self-evolving skill harness
137
+ if config.evolution.enabled:
138
+ try:
139
+ from echo_agent.evaluation.dataset import EvalDataset
140
+ from echo_agent.evaluation.runner import EvalRunner
141
+ from echo_agent.evolution.engine import EvolutionEngine
142
+
143
+ dataset_path = ws / config.evolution.eval_dataset_path
144
+ if not dataset_path.is_absolute():
145
+ dataset_path = (ws / config.evolution.eval_dataset_path).resolve()
146
+
147
+ def _load_eval_dataset() -> EvalDataset:
148
+ return EvalDataset.from_path(dataset_path)
149
+
150
+ def _make_eval_runner() -> EvalRunner:
151
+ return EvalRunner(
152
+ agent,
153
+ parallel=config.evolution.eval_parallel,
154
+ timeout=config.evolution.eval_timeout_seconds,
155
+ )
156
+
157
+ reflection_module = None
158
+ try:
159
+ from echo_agent.agent.planning.reflection import ReflectionModule
160
+ reflection_module = ReflectionModule(provider.chat_with_retry)
161
+ except Exception as e:
162
+ logger.debug("Reflection module unavailable for evolution: {}", e)
163
+
164
+ evolution_engine = EvolutionEngine(
165
+ config=config.evolution,
166
+ workspace=ws,
167
+ storage=storage,
168
+ provider=provider,
169
+ skill_store=agent.skill_store,
170
+ skill_manager=None,
171
+ eval_runner_factory=_make_eval_runner,
172
+ eval_dataset_loader=_load_eval_dataset,
173
+ hooks=plugin_manager.hooks,
174
+ reflection=reflection_module,
175
+ )
176
+ agent.set_evolution_engine(evolution_engine)
177
+ logger.info("Evolution engine attached (trigger={})", config.evolution.trigger_mode)
178
+ except Exception as e:
179
+ logger.warning("Failed to attach evolution engine: {}", e)
180
+
181
+ channels = ChannelManager(config.channels, bus, on_cli_exit=on_cli_exit)
182
+ health = HealthChecker(check_interval=config.observability.health_check_interval_seconds)
183
+
184
+ from echo_agent.observability.monitor import ComponentHealth as CH
185
+
186
+ async def _check_bus() -> CH:
187
+ return CH.HEALTHY if bus.pending_inbound < 900 else CH.DEGRADED
188
+
189
+ async def _check_agent() -> CH:
190
+ return CH.HEALTHY if agent._running else CH.UNHEALTHY
191
+
192
+ async def _check_storage() -> CH:
193
+ return CH.HEALTHY if storage._db else CH.UNHEALTHY
194
+
195
+ health.register_check("bus", _check_bus)
196
+ health.register_check("agent", _check_agent)
197
+ health.register_check("storage", _check_storage)
198
+
199
+ async def _session_cleanup() -> CH:
200
+ count = await agent.sessions.cleanup_expired()
201
+ if count:
202
+ logger.info("Cleaned up {} expired sessions", count)
203
+ return CH.HEALTHY
204
+
205
+ health.register_check("session_cleanup", _session_cleanup)
206
+
207
+ return _BootstrapResult(
208
+ config=config, workspace=ws, storage=storage, bus=bus,
209
+ router=router, provider=provider, agent=agent,
210
+ channels=channels, scheduler=scheduler, health=health,
211
+ )
212
+
213
+
214
+ def _install_signal_handler(shutdown: asyncio.Event) -> None:
215
+ loop = asyncio.get_running_loop()
216
+ for sig in (signal.SIGINT, signal.SIGTERM):
217
+ try:
218
+ loop.add_signal_handler(sig, shutdown.set)
219
+ except NotImplementedError:
220
+ pass
221
+
222
+
223
+ async def _run(config_path: str | None = None, workspace: str | None = None) -> None:
224
+ if config_path is None and workspace:
225
+ from echo_agent.config.loader import resolve_config_file
226
+ config_path = str(resolve_config_file(search_dir=workspace) or "")
227
+ overrides = {"workspace": workspace} if workspace else None
228
+ shutdown = asyncio.Event()
229
+ ctx = await _bootstrap(config_path=config_path, overrides=overrides, on_cli_exit=shutdown.set)
230
+
231
+ logger.info("Echo Agent starting — workspace: {}", ctx.workspace)
232
+
233
+ _install_signal_handler(shutdown)
234
+
235
+ await ctx.bus.start()
236
+ await ctx.agent.start()
237
+ await ctx.channels.start_all()
238
+ if not ctx.channels.active_channels and not ctx.config.gateway.enabled:
239
+ logger.error(
240
+ "No active input channels. Run in an interactive terminal, enable gateway, "
241
+ "or configure another channel."
242
+ )
243
+ await ctx.channels.stop_all()
244
+ await ctx.agent.stop()
245
+ await ctx.bus.stop()
246
+ await ctx.storage.close()
247
+ return
248
+ if ctx.scheduler:
249
+ await ctx.scheduler.start()
250
+ await ctx.health.start()
251
+
252
+ gateway = None
253
+ if ctx.config.gateway.enabled:
254
+ from echo_agent.gateway.server import GatewayServer
255
+ gateway = GatewayServer(
256
+ config=ctx.config.gateway,
257
+ bus=ctx.bus,
258
+ channel_manager=ctx.channels,
259
+ session_manager=ctx.agent.sessions,
260
+ workspace=ctx.workspace,
261
+ agent_loop=ctx.agent,
262
+ a2a_config=ctx.config.a2a,
263
+ )
264
+ await gateway.start()
265
+ logger.info("Gateway started on {}:{}", ctx.config.gateway.host, ctx.config.gateway.port)
266
+
267
+ logger.info("Echo Agent ready — channels: {}", ctx.channels.active_channels)
268
+ await shutdown.wait()
269
+
270
+ logger.info("Shutting down...")
271
+ if gateway:
272
+ await gateway.stop()
273
+ await ctx.health.stop()
274
+ if ctx.scheduler:
275
+ await ctx.scheduler.stop()
276
+ await ctx.channels.stop_all()
277
+ await ctx.agent.stop()
278
+ await ctx.bus.stop()
279
+ await ctx.storage.close()
280
+ logger.info("Echo Agent stopped")
281
+
282
+
283
+ async def _run_gateway(config_path: str | None = None, host: str | None = None, port: int | None = None, workspace: str | None = None) -> None:
284
+ if config_path is None and workspace:
285
+ from echo_agent.config.loader import resolve_config_file
286
+ config_path = str(resolve_config_file(search_dir=workspace) or "")
287
+ overrides: dict[str, Any] = {"workspace": workspace} if workspace else {}
288
+ shutdown = asyncio.Event()
289
+ ctx = await _bootstrap(config_path=config_path, overrides=overrides or None, on_cli_exit=shutdown.set)
290
+ ctx.config.gateway.enabled = True
291
+ if host:
292
+ ctx.config.gateway.host = host
293
+ if port:
294
+ ctx.config.gateway.port = port
295
+
296
+ _install_signal_handler(shutdown)
297
+
298
+ await ctx.bus.start()
299
+ await ctx.agent.start()
300
+ await ctx.channels.start_all()
301
+
302
+ from echo_agent.gateway.server import GatewayServer
303
+ gateway = GatewayServer(
304
+ config=ctx.config.gateway,
305
+ bus=ctx.bus,
306
+ channel_manager=ctx.channels,
307
+ session_manager=ctx.agent.sessions,
308
+ workspace=ctx.workspace,
309
+ agent_loop=ctx.agent,
310
+ a2a_config=ctx.config.a2a,
311
+ )
312
+ await gateway.start()
313
+ logger.info("Gateway listening on {}:{}", ctx.config.gateway.host, ctx.config.gateway.port)
314
+
315
+ await shutdown.wait()
316
+
317
+ await gateway.stop()
318
+ await ctx.channels.stop_all()
319
+ await ctx.agent.stop()
320
+ await ctx.bus.stop()
321
+ await ctx.storage.close()
322
+
323
+
324
+ def _run_eval(args) -> None:
325
+ from pathlib import Path as _Path
326
+
327
+ config_path = args.config or getattr(args, "top_config", None)
328
+ workspace = args.workspace or getattr(args, "top_workspace", None)
329
+
330
+ from echo_agent.config.loader import load_config, resolve_config_file
331
+ config_file = resolve_config_file(config_path)
332
+ config = load_config(config_path=config_file)
333
+
334
+ dataset_path = args.dataset or config.evaluation.dataset_path
335
+ path = _Path(dataset_path)
336
+ if not path.exists():
337
+ print(f"Dataset not found: {path}")
338
+ print("Create a YAML file with test cases. Example:")
339
+ print(" - id: test_001")
340
+ print(" input: 'Hello'")
341
+ print(" expected_contains: ['hello', 'hi']")
342
+ return
343
+
344
+ from echo_agent.evaluation import EvalRunner, EvalDataset
345
+
346
+ dataset = EvalDataset.from_path(path)
347
+ if args.tag:
348
+ cases = dataset.filter_by_tag(args.tag)
349
+ dataset = EvalDataset(cases)
350
+
351
+ if not dataset.cases:
352
+ print("No test cases found.")
353
+ return
354
+
355
+ async def run():
356
+ overrides = {"workspace": workspace} if workspace else None
357
+ ctx = await _bootstrap(config_path=config_path, overrides=overrides)
358
+ await ctx.bus.start()
359
+ await ctx.agent.start()
360
+
361
+ try:
362
+ runner = EvalRunner(ctx.agent, parallel=args.parallel, timeout=config.evaluation.timeout_per_case)
363
+ report = await runner.run_dataset(dataset)
364
+
365
+ from echo_agent.evaluation.reporter import EvalReporter
366
+ reporter = EvalReporter()
367
+ print(reporter.to_table(report))
368
+
369
+ if args.output:
370
+ _Path(args.output).write_text(reporter.to_json(report), encoding="utf-8")
371
+ print(f"\nResults saved to {args.output}")
372
+ finally:
373
+ await ctx.agent.stop()
374
+ await ctx.bus.stop()
375
+ await ctx.storage.close()
376
+
377
+ asyncio.run(run())
378
+
379
+
380
+ def main() -> None:
381
+ parser = argparse.ArgumentParser(prog="echo-agent", description="Echo Agent — modular AI agent framework")
382
+ subparsers = parser.add_subparsers(dest="command")
383
+
384
+ # run
385
+ run_parser = subparsers.add_parser("run", help="Start the agent")
386
+ run_parser.add_argument("-c", "--config", help="Path to config file")
387
+ run_parser.add_argument("-w", "--workspace", help="Workspace directory")
388
+
389
+ # setup
390
+ setup_parser = subparsers.add_parser("setup", help="Run the setup wizard")
391
+ setup_parser.add_argument(
392
+ "section", nargs="?", default=None,
393
+ help="Setup section: language, model, permissions, terminal, agent, tools, channel, gateway, observability, evolution, doctor",
394
+ )
395
+ setup_parser.add_argument("-c", "--config", help="Path to config file")
396
+ setup_parser.add_argument("-w", "--workspace", help="Workspace directory")
397
+ setup_parser.add_argument("--lang", choices=["en", "zh", "auto"], default=None,
398
+ help="Override interface language (default: auto-detect from OS)")
399
+ setup_parser.add_argument("--flow", choices=["quickstart", "full"], default=None,
400
+ help="Skip the menu and run a specific flow")
401
+
402
+ # status
403
+ status_parser = subparsers.add_parser("status", help="Show current configuration status")
404
+ status_parser.add_argument("-c", "--config", help="Path to config file")
405
+ status_parser.add_argument("-w", "--workspace", help="Workspace directory")
406
+
407
+ # gateway
408
+ gw_parser = subparsers.add_parser("gateway", help="Start the gateway server")
409
+ gw_parser.add_argument("-c", "--config", help="Path to config file")
410
+ gw_parser.add_argument("-w", "--workspace", help="Workspace directory")
411
+ gw_parser.add_argument("--host", help="Gateway host")
412
+ gw_parser.add_argument("--port", type=int, help="Gateway port")
413
+
414
+ # eval
415
+ eval_parser = subparsers.add_parser("eval", help="Run evaluation test suite")
416
+ eval_parser.add_argument("--dataset", "-d", default="", help="Path to eval dataset (YAML/JSON)")
417
+ eval_parser.add_argument("--tag", "-t", default="", help="Filter cases by tag")
418
+ eval_parser.add_argument("--parallel", "-p", type=int, default=3, help="Parallel cases")
419
+ eval_parser.add_argument("--output", "-o", default="", help="Output file for results")
420
+ eval_parser.add_argument("-c", "--config", help="Path to config file")
421
+ eval_parser.add_argument("-w", "--workspace", help="Workspace directory")
422
+
423
+ # service
424
+ svc_parser = subparsers.add_parser("service", help="Manage systemd service (Linux)")
425
+ svc_parser.add_argument("action", choices=["install", "uninstall", "start", "stop", "restart", "status", "logs"], help="Service action")
426
+ svc_parser.add_argument("-w", "--workspace", help="Workspace directory (used by install)")
427
+
428
+ # plugin
429
+ plugin_parser = subparsers.add_parser("plugin", help="Manage plugins")
430
+ plugin_parser.add_argument("action", choices=["list", "info", "enable", "disable", "check"], help="Plugin action")
431
+ plugin_parser.add_argument("name", nargs="?", default="", help="Plugin name (for info/enable/disable)")
432
+ plugin_parser.add_argument("-c", "--config", help="Path to config file")
433
+ plugin_parser.add_argument("-w", "--workspace", help="Workspace directory")
434
+
435
+ # evolution
436
+ evo_parser = subparsers.add_parser("evolution", help="Manage the self-evolving skill harness")
437
+ evo_parser.add_argument(
438
+ "action",
439
+ choices=[
440
+ "status", "run", "list-candidates", "show-candidate",
441
+ "promote", "rollback", "init-dataset",
442
+ ],
443
+ help="Evolution action",
444
+ )
445
+ evo_parser.add_argument("target", nargs="?", default="", help="Skill name (rollback) or candidate id (show-candidate/promote)")
446
+ evo_parser.add_argument("--status", dest="status_filter", default="", help="Filter list-candidates by status")
447
+ evo_parser.add_argument("-c", "--config", help="Path to config file")
448
+ evo_parser.add_argument("-w", "--workspace", help="Workspace directory")
449
+
450
+ # top-level flags for backward compat
451
+ parser.add_argument("-c", "--config", help="Path to config file", dest="top_config")
452
+ parser.add_argument("-w", "--workspace", help="Workspace directory", dest="top_workspace")
453
+
454
+ args = parser.parse_args()
455
+
456
+ if args.command == "setup":
457
+ from echo_agent.cli.setup import run_setup_wizard
458
+ lang_arg = getattr(args, "lang", None)
459
+ if lang_arg == "auto":
460
+ lang_arg = None
461
+ run_setup_wizard(
462
+ section=args.section,
463
+ config_path=args.config or args.top_config,
464
+ workspace=args.workspace or args.top_workspace,
465
+ lang=lang_arg,
466
+ flow=getattr(args, "flow", None),
467
+ )
468
+ return
469
+
470
+ if args.command == "status":
471
+ from echo_agent.cli.status import show_status
472
+ show_status(config_path=args.config or args.top_config, workspace=args.workspace or args.top_workspace)
473
+ return
474
+
475
+ if args.command == "gateway":
476
+ try:
477
+ asyncio.run(_run_gateway(config_path=args.config or args.top_config, host=args.host, port=args.port, workspace=args.workspace or args.top_workspace))
478
+ except KeyboardInterrupt:
479
+ pass
480
+ return
481
+
482
+ if args.command == "eval":
483
+ _run_eval(args)
484
+ return
485
+
486
+ if args.command == "service":
487
+ from echo_agent.cli.service import run_action
488
+ run_action(args.action, workspace=args.workspace or args.top_workspace)
489
+ return
490
+
491
+ if args.command == "plugin":
492
+ from echo_agent.cli.plugins_cmd import run_plugin_command
493
+ run_plugin_command(
494
+ action=args.action,
495
+ name=args.name,
496
+ config_path=args.config or args.top_config,
497
+ workspace=args.workspace or args.top_workspace,
498
+ )
499
+ return
500
+
501
+ if args.command == "evolution":
502
+ from echo_agent.cli.evolution_cmd import run_evolution_command
503
+ target = getattr(args, "target", "") or ""
504
+ skill = ""
505
+ candidate_id = ""
506
+ if args.action == "rollback":
507
+ skill = target
508
+ elif args.action in ("show-candidate", "promote"):
509
+ candidate_id = target
510
+ try:
511
+ run_evolution_command(
512
+ action=args.action,
513
+ skill=skill,
514
+ status_filter=getattr(args, "status_filter", "") or "",
515
+ candidate_id=candidate_id,
516
+ config_path=args.config or args.top_config,
517
+ workspace=args.workspace or args.top_workspace,
518
+ )
519
+ except KeyboardInterrupt:
520
+ pass
521
+ return
522
+
523
+ # "run" command or no command (backward compat)
524
+ config_path = getattr(args, "config", None) or args.top_config
525
+ workspace = getattr(args, "workspace", None) or args.top_workspace
526
+
527
+ from echo_agent.cli.setup import prompt_first_run_setup
528
+ if prompt_first_run_setup(config_path=config_path, workspace=workspace):
529
+ return
530
+
531
+ try:
532
+ asyncio.run(_run(config_path=config_path, workspace=workspace))
533
+ except KeyboardInterrupt:
534
+ pass
535
+
536
+
537
+ if __name__ == "__main__":
538
+ main()
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: plan
3
+ description: Plan mode — inspect context, write a markdown plan into the active workspace's `.echo-agent/plans/` directory, and do not execute the work.
4
+ version: 1.0.0
5
+ metadata:
6
+ echo:
7
+ tags: [planning, plan-mode, implementation, workflow]
8
+ ---
9
+
10
+ # Plan Mode
11
+
12
+ Use this skill when the user wants a plan instead of execution.
13
+
14
+ ## Core behavior
15
+
16
+ For this turn, you are planning only.
17
+
18
+ - Do not implement code.
19
+ - Do not edit project files except the plan markdown file.
20
+ - Do not run mutating terminal commands, commit, push, or perform external actions.
21
+ - You may inspect the repo or other context with read-only commands/tools when needed.
22
+ - Your deliverable is a markdown plan saved inside the active workspace under `.echo-agent/plans/`.
23
+
24
+ ## Output requirements
25
+
26
+ Write a markdown plan that is concrete and actionable.
27
+
28
+ Include, when relevant:
29
+ - Goal
30
+ - Current context / assumptions
31
+ - Proposed approach
32
+ - Step-by-step plan
33
+ - Files likely to change
34
+ - Tests / validation
35
+ - Risks, tradeoffs, and open questions
36
+
37
+ If the task is code-related, include exact file paths, likely test targets, and verification steps.
38
+
39
+ ## Save location
40
+
41
+ Save the plan with `write_file` under:
42
+ - `.echo-agent/plans/YYYY-MM-DD_HHMMSS-<slug>.md`
43
+
44
+ Use this path relative to the active working directory.
45
+
46
+ If the runtime provides a specific target path, use that exact path.
47
+ If not, create a sensible timestamped filename yourself under `.echo-agent/plans/`.
48
+
49
+ ## Interaction style
50
+
51
+ - If the request is clear enough, write the plan directly.
52
+ - If no explicit instruction accompanies `/plan`, infer the task from the current conversation context.
53
+ - If it is genuinely underspecified, ask a brief clarifying question instead of guessing.
54
+ - After saving the plan, reply briefly with what you planned and the saved path.