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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""EvolutionScheduler — chooses *when* to invoke ``EvolutionEngine.run_evolution``.
|
|
2
|
+
|
|
3
|
+
Three trigger modes (mutually exclusive):
|
|
4
|
+
|
|
5
|
+
- manual : never fires automatically; CLI / agent tools / Gateway only.
|
|
6
|
+
- threshold : fires whenever the unconsumed-trajectory count crosses N.
|
|
7
|
+
- scheduled : fires on a cron expression (using croniter).
|
|
8
|
+
|
|
9
|
+
The scheduler runs a single asyncio polling task; it does not push work into
|
|
10
|
+
the global :class:`echo_agent.scheduler.service.Scheduler` because evolution
|
|
11
|
+
runs are session-internal and should not survive a restart unfinished.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import Awaitable, Callable
|
|
19
|
+
|
|
20
|
+
from loguru import logger
|
|
21
|
+
|
|
22
|
+
from echo_agent.evolution.types import EvolutionRun
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
RunEvolutionFn = Callable[..., Awaitable[EvolutionRun]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EvolutionScheduler:
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
run_fn: RunEvolutionFn,
|
|
33
|
+
unconsumed_count_fn: Callable[[], Awaitable[int]],
|
|
34
|
+
trigger_mode: str = "manual",
|
|
35
|
+
threshold: int = 50,
|
|
36
|
+
cron_expression: str = "0 4 * * *",
|
|
37
|
+
poll_interval_seconds: float = 30.0,
|
|
38
|
+
):
|
|
39
|
+
self._run_fn = run_fn
|
|
40
|
+
self._count_fn = unconsumed_count_fn
|
|
41
|
+
self._mode = trigger_mode
|
|
42
|
+
self._threshold = max(1, int(threshold))
|
|
43
|
+
self._cron_expression = cron_expression
|
|
44
|
+
self._poll_interval = max(5.0, float(poll_interval_seconds))
|
|
45
|
+
self._task: asyncio.Task | None = None
|
|
46
|
+
self._running = False
|
|
47
|
+
self._next_cron_ts: float | None = None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def mode(self) -> str:
|
|
51
|
+
return self._mode
|
|
52
|
+
|
|
53
|
+
async def start(self) -> None:
|
|
54
|
+
if self._running or self._mode == "manual":
|
|
55
|
+
return
|
|
56
|
+
self._running = True
|
|
57
|
+
if self._mode == "scheduled":
|
|
58
|
+
self._next_cron_ts = self._compute_next_cron_ts(datetime.now())
|
|
59
|
+
self._task = asyncio.create_task(self._loop(), name="evolution-scheduler")
|
|
60
|
+
logger.info(
|
|
61
|
+
"EvolutionScheduler started (mode={}, threshold={}, cron='{}')",
|
|
62
|
+
self._mode,
|
|
63
|
+
self._threshold,
|
|
64
|
+
self._cron_expression,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def stop(self) -> None:
|
|
68
|
+
self._running = False
|
|
69
|
+
if self._task is not None:
|
|
70
|
+
self._task.cancel()
|
|
71
|
+
try:
|
|
72
|
+
await self._task
|
|
73
|
+
except asyncio.CancelledError:
|
|
74
|
+
pass
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.debug("EvolutionScheduler stop raised: {}", e)
|
|
77
|
+
self._task = None
|
|
78
|
+
|
|
79
|
+
async def _loop(self) -> None:
|
|
80
|
+
try:
|
|
81
|
+
while self._running:
|
|
82
|
+
try:
|
|
83
|
+
await self._tick()
|
|
84
|
+
except asyncio.CancelledError:
|
|
85
|
+
raise
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.warning("EvolutionScheduler tick failed: {}", e)
|
|
88
|
+
await asyncio.sleep(self._poll_interval)
|
|
89
|
+
except asyncio.CancelledError:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
async def _tick(self) -> None:
|
|
93
|
+
if self._mode == "threshold":
|
|
94
|
+
await self._tick_threshold()
|
|
95
|
+
elif self._mode == "scheduled":
|
|
96
|
+
await self._tick_cron()
|
|
97
|
+
|
|
98
|
+
async def _tick_threshold(self) -> None:
|
|
99
|
+
try:
|
|
100
|
+
count = await self._count_fn()
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.debug("threshold count failed: {}", e)
|
|
103
|
+
return
|
|
104
|
+
if count >= self._threshold:
|
|
105
|
+
logger.info("EvolutionScheduler threshold reached ({} >= {})", count, self._threshold)
|
|
106
|
+
await self._safe_run("threshold")
|
|
107
|
+
|
|
108
|
+
async def _tick_cron(self) -> None:
|
|
109
|
+
if self._next_cron_ts is None:
|
|
110
|
+
self._next_cron_ts = self._compute_next_cron_ts(datetime.now())
|
|
111
|
+
return
|
|
112
|
+
now_ts = datetime.now().timestamp()
|
|
113
|
+
if now_ts >= self._next_cron_ts:
|
|
114
|
+
await self._safe_run("scheduled")
|
|
115
|
+
self._next_cron_ts = self._compute_next_cron_ts(datetime.now())
|
|
116
|
+
|
|
117
|
+
async def _safe_run(self, trigger: str) -> None:
|
|
118
|
+
try:
|
|
119
|
+
await self._run_fn(trigger=trigger)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.warning("Scheduled evolution run failed ({}): {}", trigger, e)
|
|
122
|
+
|
|
123
|
+
def _compute_next_cron_ts(self, base: datetime) -> float | None:
|
|
124
|
+
if not self._cron_expression:
|
|
125
|
+
return None
|
|
126
|
+
try:
|
|
127
|
+
from croniter import croniter
|
|
128
|
+
|
|
129
|
+
cron = croniter(self._cron_expression, base)
|
|
130
|
+
return cron.get_next(datetime).timestamp()
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.warning("Invalid cron expression '{}': {}", self._cron_expression, e)
|
|
133
|
+
return None
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""TrajectoryStore — SQLite-backed persistence for the evolution subsystem.
|
|
2
|
+
|
|
3
|
+
Adds three tables to the shared SQLite backend (created via execute_sql at
|
|
4
|
+
init time, so it does not fight the global migration list in storage.sqlite):
|
|
5
|
+
|
|
6
|
+
- evolution_trajectories
|
|
7
|
+
- evolution_candidates
|
|
8
|
+
- evolution_runs
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from loguru import logger
|
|
18
|
+
|
|
19
|
+
from echo_agent.evolution.types import (
|
|
20
|
+
CandidateStatus,
|
|
21
|
+
EvolutionRun,
|
|
22
|
+
Outcome,
|
|
23
|
+
SkillCandidate,
|
|
24
|
+
Trajectory,
|
|
25
|
+
)
|
|
26
|
+
from echo_agent.storage.backend import StorageBackend
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_SCHEMA_STATEMENTS: list[str] = [
|
|
30
|
+
"""CREATE TABLE IF NOT EXISTS evolution_trajectories (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
session_id TEXT NOT NULL DEFAULT '',
|
|
33
|
+
chat_id TEXT NOT NULL DEFAULT '',
|
|
34
|
+
channel TEXT NOT NULL DEFAULT '',
|
|
35
|
+
task_type TEXT NOT NULL DEFAULT '',
|
|
36
|
+
outcome TEXT NOT NULL DEFAULT 'success',
|
|
37
|
+
score REAL,
|
|
38
|
+
iterations INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
duration_ms REAL NOT NULL DEFAULT 0,
|
|
40
|
+
data TEXT NOT NULL,
|
|
41
|
+
created_at TEXT NOT NULL,
|
|
42
|
+
consumed_run_id TEXT NOT NULL DEFAULT ''
|
|
43
|
+
)""",
|
|
44
|
+
"CREATE INDEX IF NOT EXISTS idx_evolution_traj_outcome "
|
|
45
|
+
"ON evolution_trajectories(outcome, created_at)",
|
|
46
|
+
"CREATE INDEX IF NOT EXISTS idx_evolution_traj_task_type "
|
|
47
|
+
"ON evolution_trajectories(task_type, outcome)",
|
|
48
|
+
"CREATE INDEX IF NOT EXISTS idx_evolution_traj_consumed "
|
|
49
|
+
"ON evolution_trajectories(consumed_run_id)",
|
|
50
|
+
"""CREATE TABLE IF NOT EXISTS evolution_candidates (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
run_id TEXT NOT NULL DEFAULT '',
|
|
53
|
+
operation TEXT NOT NULL DEFAULT 'create',
|
|
54
|
+
skill_name TEXT NOT NULL DEFAULT '',
|
|
55
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
56
|
+
data TEXT NOT NULL,
|
|
57
|
+
created_at TEXT NOT NULL,
|
|
58
|
+
updated_at TEXT NOT NULL
|
|
59
|
+
)""",
|
|
60
|
+
"CREATE INDEX IF NOT EXISTS idx_evolution_cand_status "
|
|
61
|
+
"ON evolution_candidates(status)",
|
|
62
|
+
"CREATE INDEX IF NOT EXISTS idx_evolution_cand_run "
|
|
63
|
+
"ON evolution_candidates(run_id)",
|
|
64
|
+
"CREATE INDEX IF NOT EXISTS idx_evolution_cand_skill "
|
|
65
|
+
"ON evolution_candidates(skill_name)",
|
|
66
|
+
"""CREATE TABLE IF NOT EXISTS evolution_runs (
|
|
67
|
+
id TEXT PRIMARY KEY,
|
|
68
|
+
triggered_by TEXT NOT NULL DEFAULT 'manual',
|
|
69
|
+
data TEXT NOT NULL,
|
|
70
|
+
started_at TEXT NOT NULL,
|
|
71
|
+
finished_at TEXT NOT NULL DEFAULT ''
|
|
72
|
+
)""",
|
|
73
|
+
"CREATE INDEX IF NOT EXISTS idx_evolution_runs_started "
|
|
74
|
+
"ON evolution_runs(started_at)",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TrajectoryStore:
|
|
79
|
+
"""Persistence layer for trajectories, candidates, and evolution runs.
|
|
80
|
+
|
|
81
|
+
Designed to be safe to call concurrently from background tasks: every
|
|
82
|
+
method opens a single execute/fetch round-trip on the shared backend.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, storage: StorageBackend):
|
|
86
|
+
self._storage = storage
|
|
87
|
+
self._initialized = False
|
|
88
|
+
|
|
89
|
+
async def init_schema(self) -> None:
|
|
90
|
+
if self._initialized:
|
|
91
|
+
return
|
|
92
|
+
for stmt in _SCHEMA_STATEMENTS:
|
|
93
|
+
try:
|
|
94
|
+
await self._storage.execute_sql(stmt)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error("Failed to apply evolution schema statement: {}", e)
|
|
97
|
+
raise
|
|
98
|
+
self._initialized = True
|
|
99
|
+
|
|
100
|
+
# ── Trajectories ────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
async def append_trajectory(self, trajectory: Trajectory) -> None:
|
|
103
|
+
payload = json.dumps(trajectory.to_dict(), ensure_ascii=False)
|
|
104
|
+
await self._storage.execute_sql(
|
|
105
|
+
"INSERT OR REPLACE INTO evolution_trajectories "
|
|
106
|
+
"(id, session_id, chat_id, channel, task_type, outcome, score, "
|
|
107
|
+
" iterations, duration_ms, data, created_at, consumed_run_id) "
|
|
108
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, "
|
|
109
|
+
" COALESCE((SELECT consumed_run_id FROM evolution_trajectories WHERE id = ?), ''))",
|
|
110
|
+
(
|
|
111
|
+
trajectory.id,
|
|
112
|
+
trajectory.session_id,
|
|
113
|
+
trajectory.chat_id,
|
|
114
|
+
trajectory.channel,
|
|
115
|
+
trajectory.task_type,
|
|
116
|
+
trajectory.outcome,
|
|
117
|
+
trajectory.reflection_score,
|
|
118
|
+
trajectory.iterations,
|
|
119
|
+
trajectory.duration_ms,
|
|
120
|
+
payload,
|
|
121
|
+
trajectory.created_at,
|
|
122
|
+
trajectory.id,
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
async def get_trajectory(self, trajectory_id: str) -> Trajectory | None:
|
|
127
|
+
rows = await self._storage.fetch_sql(
|
|
128
|
+
"SELECT data FROM evolution_trajectories WHERE id = ?",
|
|
129
|
+
(trajectory_id,),
|
|
130
|
+
)
|
|
131
|
+
if not rows:
|
|
132
|
+
return None
|
|
133
|
+
return Trajectory.from_dict(json.loads(rows[0]["data"]))
|
|
134
|
+
|
|
135
|
+
async def list_trajectories(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
limit: int = 200,
|
|
139
|
+
outcome: Outcome | None = None,
|
|
140
|
+
task_type: str | None = None,
|
|
141
|
+
only_unconsumed: bool = False,
|
|
142
|
+
since: str | None = None,
|
|
143
|
+
) -> list[Trajectory]:
|
|
144
|
+
clauses: list[str] = []
|
|
145
|
+
params: list[Any] = []
|
|
146
|
+
if outcome:
|
|
147
|
+
clauses.append("outcome = ?")
|
|
148
|
+
params.append(outcome)
|
|
149
|
+
if task_type:
|
|
150
|
+
clauses.append("task_type = ?")
|
|
151
|
+
params.append(task_type)
|
|
152
|
+
if only_unconsumed:
|
|
153
|
+
clauses.append("consumed_run_id = ''")
|
|
154
|
+
if since:
|
|
155
|
+
clauses.append("created_at >= ?")
|
|
156
|
+
params.append(since)
|
|
157
|
+
where = ""
|
|
158
|
+
if clauses:
|
|
159
|
+
where = " WHERE " + " AND ".join(clauses)
|
|
160
|
+
params.append(int(limit))
|
|
161
|
+
rows = await self._storage.fetch_sql(
|
|
162
|
+
f"SELECT data FROM evolution_trajectories{where} "
|
|
163
|
+
f"ORDER BY created_at DESC LIMIT ?",
|
|
164
|
+
tuple(params),
|
|
165
|
+
)
|
|
166
|
+
out: list[Trajectory] = []
|
|
167
|
+
for r in rows:
|
|
168
|
+
try:
|
|
169
|
+
out.append(Trajectory.from_dict(json.loads(r["data"])))
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.debug("Skipping malformed trajectory row: {}", e)
|
|
172
|
+
return out
|
|
173
|
+
|
|
174
|
+
async def count_unconsumed(self) -> int:
|
|
175
|
+
rows = await self._storage.fetch_sql(
|
|
176
|
+
"SELECT COUNT(*) AS n FROM evolution_trajectories WHERE consumed_run_id = ''",
|
|
177
|
+
)
|
|
178
|
+
if not rows:
|
|
179
|
+
return 0
|
|
180
|
+
try:
|
|
181
|
+
return int(rows[0].get("n", 0) or 0)
|
|
182
|
+
except (TypeError, ValueError):
|
|
183
|
+
return 0
|
|
184
|
+
|
|
185
|
+
async def mark_consumed(self, trajectory_ids: list[str], run_id: str) -> None:
|
|
186
|
+
if not trajectory_ids:
|
|
187
|
+
return
|
|
188
|
+
placeholders = ",".join(["?"] * len(trajectory_ids))
|
|
189
|
+
await self._storage.execute_sql(
|
|
190
|
+
f"UPDATE evolution_trajectories SET consumed_run_id = ? "
|
|
191
|
+
f"WHERE id IN ({placeholders})",
|
|
192
|
+
tuple([run_id, *trajectory_ids]),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
async def purge_older_than(self, retention_days: int) -> int:
|
|
196
|
+
if retention_days <= 0:
|
|
197
|
+
return 0
|
|
198
|
+
cutoff = (datetime.now() - timedelta(days=retention_days)).isoformat()
|
|
199
|
+
before = await self._storage.fetch_sql(
|
|
200
|
+
"SELECT COUNT(*) AS n FROM evolution_trajectories WHERE created_at < ?",
|
|
201
|
+
(cutoff,),
|
|
202
|
+
)
|
|
203
|
+
try:
|
|
204
|
+
count = int(before[0].get("n", 0) or 0) if before else 0
|
|
205
|
+
except (TypeError, ValueError):
|
|
206
|
+
count = 0
|
|
207
|
+
await self._storage.execute_sql(
|
|
208
|
+
"DELETE FROM evolution_trajectories WHERE created_at < ?",
|
|
209
|
+
(cutoff,),
|
|
210
|
+
)
|
|
211
|
+
return count
|
|
212
|
+
|
|
213
|
+
# ── Candidates ──────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
async def save_candidate(self, candidate: SkillCandidate) -> None:
|
|
216
|
+
now = datetime.now().isoformat()
|
|
217
|
+
payload = json.dumps(candidate.to_dict(), ensure_ascii=False)
|
|
218
|
+
await self._storage.execute_sql(
|
|
219
|
+
"INSERT OR REPLACE INTO evolution_candidates "
|
|
220
|
+
"(id, run_id, operation, skill_name, status, data, created_at, updated_at) "
|
|
221
|
+
"VALUES (?, ?, ?, ?, ?, ?, "
|
|
222
|
+
" COALESCE((SELECT created_at FROM evolution_candidates WHERE id = ?), ?), "
|
|
223
|
+
" ?)",
|
|
224
|
+
(
|
|
225
|
+
candidate.id,
|
|
226
|
+
candidate.run_id,
|
|
227
|
+
candidate.operation,
|
|
228
|
+
candidate.skill_name,
|
|
229
|
+
candidate.status,
|
|
230
|
+
payload,
|
|
231
|
+
candidate.id,
|
|
232
|
+
candidate.created_at or now,
|
|
233
|
+
now,
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
async def get_candidate(self, candidate_id: str) -> SkillCandidate | None:
|
|
238
|
+
rows = await self._storage.fetch_sql(
|
|
239
|
+
"SELECT data FROM evolution_candidates WHERE id = ?",
|
|
240
|
+
(candidate_id,),
|
|
241
|
+
)
|
|
242
|
+
if not rows:
|
|
243
|
+
return None
|
|
244
|
+
return SkillCandidate.from_dict(json.loads(rows[0]["data"]))
|
|
245
|
+
|
|
246
|
+
async def list_candidates(
|
|
247
|
+
self,
|
|
248
|
+
*,
|
|
249
|
+
status: CandidateStatus | None = None,
|
|
250
|
+
run_id: str | None = None,
|
|
251
|
+
skill_name: str | None = None,
|
|
252
|
+
limit: int = 100,
|
|
253
|
+
) -> list[SkillCandidate]:
|
|
254
|
+
clauses: list[str] = []
|
|
255
|
+
params: list[Any] = []
|
|
256
|
+
if status:
|
|
257
|
+
clauses.append("status = ?")
|
|
258
|
+
params.append(status)
|
|
259
|
+
if run_id:
|
|
260
|
+
clauses.append("run_id = ?")
|
|
261
|
+
params.append(run_id)
|
|
262
|
+
if skill_name:
|
|
263
|
+
clauses.append("skill_name = ?")
|
|
264
|
+
params.append(skill_name)
|
|
265
|
+
where = ""
|
|
266
|
+
if clauses:
|
|
267
|
+
where = " WHERE " + " AND ".join(clauses)
|
|
268
|
+
params.append(int(limit))
|
|
269
|
+
rows = await self._storage.fetch_sql(
|
|
270
|
+
f"SELECT data FROM evolution_candidates{where} "
|
|
271
|
+
f"ORDER BY updated_at DESC LIMIT ?",
|
|
272
|
+
tuple(params),
|
|
273
|
+
)
|
|
274
|
+
out: list[SkillCandidate] = []
|
|
275
|
+
for r in rows:
|
|
276
|
+
try:
|
|
277
|
+
out.append(SkillCandidate.from_dict(json.loads(r["data"])))
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.debug("Skipping malformed candidate row: {}", e)
|
|
280
|
+
return out
|
|
281
|
+
|
|
282
|
+
async def update_candidate(self, candidate: SkillCandidate) -> None:
|
|
283
|
+
await self.save_candidate(candidate)
|
|
284
|
+
|
|
285
|
+
async def latest_promoted_for_skill(self, skill_name: str) -> SkillCandidate | None:
|
|
286
|
+
rows = await self._storage.fetch_sql(
|
|
287
|
+
"SELECT data FROM evolution_candidates "
|
|
288
|
+
"WHERE skill_name = ? AND status = 'promoted' "
|
|
289
|
+
"ORDER BY updated_at DESC LIMIT 1",
|
|
290
|
+
(skill_name,),
|
|
291
|
+
)
|
|
292
|
+
if not rows:
|
|
293
|
+
return None
|
|
294
|
+
return SkillCandidate.from_dict(json.loads(rows[0]["data"]))
|
|
295
|
+
|
|
296
|
+
# ── Runs ────────────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
async def save_run(self, run: EvolutionRun) -> None:
|
|
299
|
+
payload = json.dumps(run.to_dict(), ensure_ascii=False)
|
|
300
|
+
await self._storage.execute_sql(
|
|
301
|
+
"INSERT OR REPLACE INTO evolution_runs "
|
|
302
|
+
"(id, triggered_by, data, started_at, finished_at) "
|
|
303
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
304
|
+
(run.id, run.triggered_by, payload, run.started_at, run.finished_at),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
async def get_run(self, run_id: str) -> EvolutionRun | None:
|
|
308
|
+
rows = await self._storage.fetch_sql(
|
|
309
|
+
"SELECT data FROM evolution_runs WHERE id = ?",
|
|
310
|
+
(run_id,),
|
|
311
|
+
)
|
|
312
|
+
if not rows:
|
|
313
|
+
return None
|
|
314
|
+
return EvolutionRun.from_dict(json.loads(rows[0]["data"]))
|
|
315
|
+
|
|
316
|
+
async def list_runs(self, *, limit: int = 25) -> list[EvolutionRun]:
|
|
317
|
+
rows = await self._storage.fetch_sql(
|
|
318
|
+
"SELECT data FROM evolution_runs ORDER BY started_at DESC LIMIT ?",
|
|
319
|
+
(int(limit),),
|
|
320
|
+
)
|
|
321
|
+
out: list[EvolutionRun] = []
|
|
322
|
+
for r in rows:
|
|
323
|
+
try:
|
|
324
|
+
out.append(EvolutionRun.from_dict(json.loads(r["data"])))
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.debug("Skipping malformed run row: {}", e)
|
|
327
|
+
return out
|
|
328
|
+
|
|
329
|
+
async def latest_run(self) -> EvolutionRun | None:
|
|
330
|
+
runs = await self.list_runs(limit=1)
|
|
331
|
+
return runs[0] if runs else None
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Agent-facing tools for the self-evolution subsystem.
|
|
2
|
+
|
|
3
|
+
These are intentionally small wrappers over :class:`EvolutionEngine` — they
|
|
4
|
+
let the agent itself inspect or trigger evolution, but every state change
|
|
5
|
+
goes through the same :class:`PromotionGate` as the CLI.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from echo_agent.agent.tools.base import Tool, ToolExecutionContext, ToolResult
|
|
14
|
+
from echo_agent.evolution.engine import EvolutionEngine
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EvolutionStatusTool(Tool):
|
|
18
|
+
name = "evolution_status"
|
|
19
|
+
description = (
|
|
20
|
+
"Show the status of the self-evolving skill harness — most recent run, "
|
|
21
|
+
"pending and promoted candidate counts, cooldowns, and unconsumed "
|
|
22
|
+
"trajectory count."
|
|
23
|
+
)
|
|
24
|
+
parameters: dict[str, Any] = {"type": "object", "properties": {}, "required": []}
|
|
25
|
+
risk_level = "read_only"
|
|
26
|
+
|
|
27
|
+
def __init__(self, engine: EvolutionEngine):
|
|
28
|
+
self._engine = engine
|
|
29
|
+
|
|
30
|
+
def execution_mode(self, params: dict[str, Any]) -> str:
|
|
31
|
+
return "read_only"
|
|
32
|
+
|
|
33
|
+
async def execute(self, params: dict[str, Any], ctx: ToolExecutionContext | None = None) -> ToolResult:
|
|
34
|
+
try:
|
|
35
|
+
summary = await self._engine.status_summary()
|
|
36
|
+
return ToolResult(success=True, output=json.dumps(summary, ensure_ascii=False, indent=2))
|
|
37
|
+
except Exception as e:
|
|
38
|
+
return ToolResult(success=False, error=f"evolution_status failed: {e}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class EvolutionRunTool(Tool):
|
|
42
|
+
name = "evolution_run"
|
|
43
|
+
description = (
|
|
44
|
+
"Trigger a self-evolution pass right now. Generates skill candidates from "
|
|
45
|
+
"recent trajectories and runs an A/B evaluation against the baseline "
|
|
46
|
+
"dataset before promoting any change. High-risk: requires approval."
|
|
47
|
+
)
|
|
48
|
+
parameters: dict[str, Any] = {"type": "object", "properties": {}, "required": []}
|
|
49
|
+
risk_level = "dangerous"
|
|
50
|
+
|
|
51
|
+
def __init__(self, engine: EvolutionEngine):
|
|
52
|
+
self._engine = engine
|
|
53
|
+
|
|
54
|
+
def execution_mode(self, params: dict[str, Any]) -> str:
|
|
55
|
+
return "side_effect"
|
|
56
|
+
|
|
57
|
+
async def execute(self, params: dict[str, Any], ctx: ToolExecutionContext | None = None) -> ToolResult:
|
|
58
|
+
try:
|
|
59
|
+
run = await self._engine.run_evolution(trigger="manual")
|
|
60
|
+
return ToolResult(
|
|
61
|
+
success=True,
|
|
62
|
+
output=json.dumps(run.to_dict(), ensure_ascii=False, indent=2),
|
|
63
|
+
)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return ToolResult(success=False, error=f"evolution_run failed: {e}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class EvolutionRollbackTool(Tool):
|
|
69
|
+
name = "evolution_rollback"
|
|
70
|
+
description = (
|
|
71
|
+
"Roll back the most recent promoted change for a given skill. "
|
|
72
|
+
"Use when a promoted skill turns out to be harmful. High-risk: "
|
|
73
|
+
"requires approval."
|
|
74
|
+
)
|
|
75
|
+
parameters: dict[str, Any] = {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"properties": {
|
|
78
|
+
"skill_name": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"description": "Name of the skill to roll back.",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
"required": ["skill_name"],
|
|
84
|
+
}
|
|
85
|
+
risk_level = "dangerous"
|
|
86
|
+
|
|
87
|
+
def __init__(self, engine: EvolutionEngine):
|
|
88
|
+
self._engine = engine
|
|
89
|
+
|
|
90
|
+
def execution_mode(self, params: dict[str, Any]) -> str:
|
|
91
|
+
return "side_effect"
|
|
92
|
+
|
|
93
|
+
async def execute(self, params: dict[str, Any], ctx: ToolExecutionContext | None = None) -> ToolResult:
|
|
94
|
+
skill_name = (params.get("skill_name") or "").strip()
|
|
95
|
+
if not skill_name:
|
|
96
|
+
return ToolResult(success=False, error="skill_name is required")
|
|
97
|
+
try:
|
|
98
|
+
ok, message = await self._engine.rollback_skill(skill_name)
|
|
99
|
+
return ToolResult(success=ok, output=message if ok else "", error="" if ok else message)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return ToolResult(success=False, error=f"evolution_rollback failed: {e}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_evolution_tools(engine: EvolutionEngine) -> list[Tool]:
|
|
105
|
+
"""Construct the set of evolution tools to register on the ToolRegistry."""
|
|
106
|
+
return [
|
|
107
|
+
EvolutionStatusTool(engine),
|
|
108
|
+
EvolutionRunTool(engine),
|
|
109
|
+
EvolutionRollbackTool(engine),
|
|
110
|
+
]
|