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.
- echo_agent/__init__.py +5 -0
- echo_agent/__main__.py +538 -0
- echo_agent/_bundled/skills/development/plan/SKILL.md +54 -0
- echo_agent/_bundled/skills/development/skill-creator/SKILL.md +270 -0
- echo_agent/_bundled/skills/development/skill-creator/scripts/init_skill.py +226 -0
- echo_agent/_bundled/skills/development/skill-creator/scripts/package_skill.py +146 -0
- echo_agent/_bundled/skills/development/skill-creator/scripts/quick_validate.py +222 -0
- echo_agent/_bundled/skills/productivity/summarize/SKILL.md +67 -0
- echo_agent/_bundled/skills/productivity/weather/SKILL.md +49 -0
- echo_agent/_bundled/skills/research/arxiv/SKILL.md +232 -0
- echo_agent/_bundled/skills/research/arxiv/scripts/search_arxiv.py +115 -0
- echo_agent/a2a/__init__.py +5 -0
- echo_agent/a2a/client.py +66 -0
- echo_agent/a2a/models.py +98 -0
- echo_agent/a2a/protocol.py +85 -0
- echo_agent/a2a/server.py +71 -0
- echo_agent/agent/__init__.py +0 -0
- echo_agent/agent/approval_gate.py +326 -0
- echo_agent/agent/compression/__init__.py +14 -0
- echo_agent/agent/compression/assembler.py +45 -0
- echo_agent/agent/compression/boundary.py +141 -0
- echo_agent/agent/compression/compressor.py +181 -0
- echo_agent/agent/compression/engine.py +88 -0
- echo_agent/agent/compression/pruner.py +150 -0
- echo_agent/agent/compression/summarizer.py +181 -0
- echo_agent/agent/compression/types.py +41 -0
- echo_agent/agent/compression/validator.py +96 -0
- echo_agent/agent/consolidation.py +96 -0
- echo_agent/agent/context.py +403 -0
- echo_agent/agent/executors/__init__.py +0 -0
- echo_agent/agent/executors/base.py +211 -0
- echo_agent/agent/executors/factory.py +34 -0
- echo_agent/agent/executors/remote.py +193 -0
- echo_agent/agent/loop.py +891 -0
- echo_agent/agent/multi_agent/__init__.py +15 -0
- echo_agent/agent/multi_agent/audit.py +19 -0
- echo_agent/agent/multi_agent/error_messages.py +35 -0
- echo_agent/agent/multi_agent/error_types.py +36 -0
- echo_agent/agent/multi_agent/models.py +37 -0
- echo_agent/agent/multi_agent/registry.py +41 -0
- echo_agent/agent/multi_agent/runtime.py +201 -0
- echo_agent/agent/pipeline/__init__.py +14 -0
- echo_agent/agent/pipeline/context_stage.py +219 -0
- echo_agent/agent/pipeline/inference_stage.py +433 -0
- echo_agent/agent/pipeline/response_stage.py +146 -0
- echo_agent/agent/pipeline/types.py +40 -0
- echo_agent/agent/planning/__init__.py +4 -0
- echo_agent/agent/planning/models.py +83 -0
- echo_agent/agent/planning/planner.py +57 -0
- echo_agent/agent/planning/reflection.py +54 -0
- echo_agent/agent/planning/strategies.py +183 -0
- echo_agent/agent/tools/__init__.py +167 -0
- echo_agent/agent/tools/base.py +149 -0
- echo_agent/agent/tools/circuit_breaker.py +82 -0
- echo_agent/agent/tools/clarify.py +42 -0
- echo_agent/agent/tools/code_exec.py +147 -0
- echo_agent/agent/tools/cronjob.py +93 -0
- echo_agent/agent/tools/delegate.py +393 -0
- echo_agent/agent/tools/filesystem.py +180 -0
- echo_agent/agent/tools/image_gen.py +65 -0
- echo_agent/agent/tools/knowledge.py +81 -0
- echo_agent/agent/tools/memory.py +198 -0
- echo_agent/agent/tools/message.py +39 -0
- echo_agent/agent/tools/notify.py +35 -0
- echo_agent/agent/tools/patch.py +178 -0
- echo_agent/agent/tools/process.py +139 -0
- echo_agent/agent/tools/registry.py +185 -0
- echo_agent/agent/tools/search.py +99 -0
- echo_agent/agent/tools/session_search.py +76 -0
- echo_agent/agent/tools/shell.py +164 -0
- echo_agent/agent/tools/skill_install.py +255 -0
- echo_agent/agent/tools/skills.py +177 -0
- echo_agent/agent/tools/task.py +104 -0
- echo_agent/agent/tools/todo.py +148 -0
- echo_agent/agent/tools/tts.py +77 -0
- echo_agent/agent/tools/vision.py +71 -0
- echo_agent/agent/tools/web.py +208 -0
- echo_agent/agent/tools/workflow.py +89 -0
- echo_agent/bus/__init__.py +11 -0
- echo_agent/bus/events.py +193 -0
- echo_agent/bus/queue.py +158 -0
- echo_agent/bus/rate_limiter.py +51 -0
- echo_agent/channels/__init__.py +0 -0
- echo_agent/channels/base.py +185 -0
- echo_agent/channels/cli.py +149 -0
- echo_agent/channels/cron.py +44 -0
- echo_agent/channels/dingtalk.py +195 -0
- echo_agent/channels/discord.py +359 -0
- echo_agent/channels/email.py +168 -0
- echo_agent/channels/feishu.py +240 -0
- echo_agent/channels/manager.py +417 -0
- echo_agent/channels/matrix.py +281 -0
- echo_agent/channels/qqbot.py +638 -0
- echo_agent/channels/qqbot_media.py +482 -0
- echo_agent/channels/slack.py +297 -0
- echo_agent/channels/telegram.py +275 -0
- echo_agent/channels/webhook.py +106 -0
- echo_agent/channels/wecom.py +152 -0
- echo_agent/channels/weixin.py +603 -0
- echo_agent/channels/whatsapp.py +138 -0
- echo_agent/cli/__init__.py +0 -0
- echo_agent/cli/colors.py +42 -0
- echo_agent/cli/evolution_cmd.py +299 -0
- echo_agent/cli/i18n/__init__.py +123 -0
- echo_agent/cli/i18n/en.py +275 -0
- echo_agent/cli/i18n/zh.py +275 -0
- echo_agent/cli/plugins_cmd.py +205 -0
- echo_agent/cli/prompt.py +102 -0
- echo_agent/cli/service.py +156 -0
- echo_agent/cli/setup.py +1111 -0
- echo_agent/cli/status.py +93 -0
- echo_agent/config/__init__.py +8 -0
- echo_agent/config/default.yaml +199 -0
- echo_agent/config/loader.py +125 -0
- echo_agent/config/schema.py +652 -0
- echo_agent/evaluation/__init__.py +4 -0
- echo_agent/evaluation/dataset.py +66 -0
- echo_agent/evaluation/metrics.py +70 -0
- echo_agent/evaluation/reporter.py +42 -0
- echo_agent/evaluation/runner.py +143 -0
- echo_agent/evolution/__init__.py +38 -0
- echo_agent/evolution/engine.py +335 -0
- echo_agent/evolution/evolver.py +397 -0
- echo_agent/evolution/gate.py +413 -0
- echo_agent/evolution/recorder.py +288 -0
- echo_agent/evolution/scheduler.py +133 -0
- echo_agent/evolution/store.py +331 -0
- echo_agent/evolution/tools.py +110 -0
- echo_agent/evolution/types.py +270 -0
- echo_agent/gateway/__init__.py +7 -0
- echo_agent/gateway/auth.py +178 -0
- echo_agent/gateway/editor.py +121 -0
- echo_agent/gateway/health.py +51 -0
- echo_agent/gateway/hooks.py +86 -0
- echo_agent/gateway/media.py +137 -0
- echo_agent/gateway/rate_limiter.py +72 -0
- echo_agent/gateway/router.py +86 -0
- echo_agent/gateway/server.py +570 -0
- echo_agent/gateway/session_context.py +57 -0
- echo_agent/gateway/session_policy.py +47 -0
- echo_agent/gateway/static/index.html +432 -0
- echo_agent/knowledge/__init__.py +5 -0
- echo_agent/knowledge/index.py +308 -0
- echo_agent/mcp/__init__.py +3 -0
- echo_agent/mcp/client.py +158 -0
- echo_agent/mcp/manager.py +161 -0
- echo_agent/mcp/oauth.py +208 -0
- echo_agent/mcp/security.py +79 -0
- echo_agent/mcp/tool_adapter.py +73 -0
- echo_agent/mcp/transport.py +353 -0
- echo_agent/memory/__init__.py +0 -0
- echo_agent/memory/consolidator.py +273 -0
- echo_agent/memory/contradiction.py +287 -0
- echo_agent/memory/forgetting.py +114 -0
- echo_agent/memory/retrieval.py +184 -0
- echo_agent/memory/reviewer.py +192 -0
- echo_agent/memory/store.py +706 -0
- echo_agent/memory/tiers.py +243 -0
- echo_agent/memory/types.py +168 -0
- echo_agent/memory/vectors.py +148 -0
- echo_agent/models/__init__.py +0 -0
- echo_agent/models/credential_pool.py +86 -0
- echo_agent/models/inference.py +98 -0
- echo_agent/models/provider.py +208 -0
- echo_agent/models/providers/__init__.py +209 -0
- echo_agent/models/providers/anthropic_provider.py +164 -0
- echo_agent/models/providers/bedrock_provider.py +261 -0
- echo_agent/models/providers/format_utils.py +198 -0
- echo_agent/models/providers/gemini_provider.py +159 -0
- echo_agent/models/providers/openai_provider.py +253 -0
- echo_agent/models/providers/openrouter_provider.py +38 -0
- echo_agent/models/rate_limiter.py +75 -0
- echo_agent/models/router.py +325 -0
- echo_agent/models/tokenizer.py +111 -0
- echo_agent/observability/__init__.py +0 -0
- echo_agent/observability/monitor.py +209 -0
- echo_agent/observability/spans.py +75 -0
- echo_agent/observability/telemetry.py +86 -0
- echo_agent/permissions/__init__.py +0 -0
- echo_agent/permissions/allowlist.py +97 -0
- echo_agent/permissions/manager.py +460 -0
- echo_agent/plugins/__init__.py +30 -0
- echo_agent/plugins/context.py +145 -0
- echo_agent/plugins/errors.py +23 -0
- echo_agent/plugins/hooks.py +126 -0
- echo_agent/plugins/loader.py +251 -0
- echo_agent/plugins/manager.py +216 -0
- echo_agent/plugins/manifest.py +70 -0
- echo_agent/runtime_paths.py +25 -0
- echo_agent/scheduler/__init__.py +0 -0
- echo_agent/scheduler/delivery.py +63 -0
- echo_agent/scheduler/service.py +398 -0
- echo_agent/security/__init__.py +11 -0
- echo_agent/security/capabilities.py +54 -0
- echo_agent/security/guards.py +265 -0
- echo_agent/security/path_policy.py +212 -0
- echo_agent/security/risk_classifier.py +75 -0
- echo_agent/security/smart_approval.py +60 -0
- echo_agent/security/tool_policy.py +159 -0
- echo_agent/session/__init__.py +0 -0
- echo_agent/session/manager.py +404 -0
- echo_agent/skills/__init__.py +0 -0
- echo_agent/skills/manager.py +279 -0
- echo_agent/skills/reviewer.py +163 -0
- echo_agent/skills/store.py +358 -0
- echo_agent/storage/__init__.py +0 -0
- echo_agent/storage/backend.py +111 -0
- echo_agent/storage/sqlite.py +523 -0
- echo_agent/tasks/__init__.py +20 -0
- echo_agent/tasks/manager.py +108 -0
- echo_agent/tasks/models.py +180 -0
- echo_agent/tasks/workflow.py +182 -0
- echo_agent/utils/__init__.py +0 -0
- echo_agent/utils/async_io.py +80 -0
- echo_agent/utils/text.py +91 -0
- echo_agent-0.1.0.dist-info/METADATA +286 -0
- echo_agent-0.1.0.dist-info/RECORD +219 -0
- echo_agent-0.1.0.dist-info/WHEEL +4 -0
- echo_agent-0.1.0.dist-info/entry_points.txt +2 -0
echo_agent/__init__.py
ADDED
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.
|