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/a2a/client.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""A2A client — discover and delegate to remote agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
|
|
7
|
+
from echo_agent.a2a.models import AgentCard, A2ATask, A2AMessage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class A2AClient:
|
|
11
|
+
def __init__(self, timeout: float = 60):
|
|
12
|
+
self._timeout = timeout
|
|
13
|
+
|
|
14
|
+
async def discover(self, base_url: str) -> AgentCard:
|
|
15
|
+
url = f"{base_url.rstrip('/')}/.well-known/agent.json"
|
|
16
|
+
async with aiohttp.ClientSession() as session:
|
|
17
|
+
async with session.get(url, timeout=aiohttp.ClientTimeout(total=self._timeout)) as resp:
|
|
18
|
+
data = await resp.json()
|
|
19
|
+
return AgentCard(
|
|
20
|
+
name=data.get("name", ""),
|
|
21
|
+
description=data.get("description", ""),
|
|
22
|
+
url=base_url,
|
|
23
|
+
version=data.get("version", ""),
|
|
24
|
+
capabilities=[],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
async def send_task(self, base_url: str, message: str, task_id: str = "") -> A2ATask:
|
|
28
|
+
url = f"{base_url.rstrip('/')}/a2a"
|
|
29
|
+
payload = {
|
|
30
|
+
"jsonrpc": "2.0",
|
|
31
|
+
"id": "1",
|
|
32
|
+
"method": "tasks/send",
|
|
33
|
+
"params": {
|
|
34
|
+
"id": task_id,
|
|
35
|
+
"message": A2AMessage.text("user", message).to_dict(),
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
async with aiohttp.ClientSession() as session:
|
|
39
|
+
async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=self._timeout)) as resp:
|
|
40
|
+
data = await resp.json()
|
|
41
|
+
result = data.get("result", {})
|
|
42
|
+
return A2ATask.from_dict(result)
|
|
43
|
+
|
|
44
|
+
async def get_task(self, base_url: str, task_id: str) -> A2ATask:
|
|
45
|
+
url = f"{base_url.rstrip('/')}/a2a"
|
|
46
|
+
payload = {
|
|
47
|
+
"jsonrpc": "2.0", "id": "1",
|
|
48
|
+
"method": "tasks/get",
|
|
49
|
+
"params": {"id": task_id},
|
|
50
|
+
}
|
|
51
|
+
async with aiohttp.ClientSession() as session:
|
|
52
|
+
async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=self._timeout)) as resp:
|
|
53
|
+
data = await resp.json()
|
|
54
|
+
return A2ATask.from_dict(data.get("result", {}))
|
|
55
|
+
|
|
56
|
+
async def cancel_task(self, base_url: str, task_id: str) -> A2ATask:
|
|
57
|
+
url = f"{base_url.rstrip('/')}/a2a"
|
|
58
|
+
payload = {
|
|
59
|
+
"jsonrpc": "2.0", "id": "1",
|
|
60
|
+
"method": "tasks/cancel",
|
|
61
|
+
"params": {"id": task_id},
|
|
62
|
+
}
|
|
63
|
+
async with aiohttp.ClientSession() as session:
|
|
64
|
+
async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=self._timeout)) as resp:
|
|
65
|
+
data = await resp.json()
|
|
66
|
+
return A2ATask.from_dict(data.get("result", {}))
|
echo_agent/a2a/models.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""A2A protocol data models — Agent Card, Task, Message, Artifact."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TaskState(str, Enum):
|
|
13
|
+
SUBMITTED = "submitted"
|
|
14
|
+
WORKING = "working"
|
|
15
|
+
INPUT_REQUIRED = "input-required"
|
|
16
|
+
COMPLETED = "completed"
|
|
17
|
+
FAILED = "failed"
|
|
18
|
+
CANCELED = "canceled"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class A2AMessage:
|
|
23
|
+
role: str # "user" or "agent"
|
|
24
|
+
parts: list[dict[str, Any]] = field(default_factory=list)
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def text(cls, role: str, content: str) -> A2AMessage:
|
|
28
|
+
return cls(role=role, parts=[{"type": "text", "text": content}])
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> dict[str, Any]:
|
|
31
|
+
return {"role": self.role, "parts": self.parts}
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_dict(cls, data: dict[str, Any]) -> A2AMessage:
|
|
35
|
+
return cls(role=data.get("role", "user"), parts=data.get("parts", []))
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def text_content(self) -> str:
|
|
39
|
+
return " ".join(p.get("text", "") for p in self.parts if p.get("type") == "text")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Artifact:
|
|
44
|
+
name: str = ""
|
|
45
|
+
content_type: str = "text/plain"
|
|
46
|
+
data: str = ""
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict[str, Any]:
|
|
49
|
+
return {"name": self.name, "contentType": self.content_type, "data": self.data}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class A2ATask:
|
|
54
|
+
id: str = field(default_factory=lambda: uuid.uuid4().hex[:16])
|
|
55
|
+
state: TaskState = TaskState.SUBMITTED
|
|
56
|
+
messages: list[A2AMessage] = field(default_factory=list)
|
|
57
|
+
artifacts: list[Artifact] = field(default_factory=list)
|
|
58
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
59
|
+
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
60
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
return {
|
|
64
|
+
"id": self.id, "state": self.state.value,
|
|
65
|
+
"messages": [m.to_dict() for m in self.messages],
|
|
66
|
+
"artifacts": [a.to_dict() for a in self.artifacts],
|
|
67
|
+
"metadata": self.metadata,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_dict(cls, data: dict[str, Any]) -> A2ATask:
|
|
72
|
+
return cls(
|
|
73
|
+
id=data.get("id", uuid.uuid4().hex[:16]),
|
|
74
|
+
state=TaskState(data.get("state", "submitted")),
|
|
75
|
+
messages=[A2AMessage.from_dict(m) for m in data.get("messages", [])],
|
|
76
|
+
artifacts=[],
|
|
77
|
+
metadata=data.get("metadata", {}),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class AgentCard:
|
|
83
|
+
name: str = "echo-agent"
|
|
84
|
+
description: str = "A modular AI agent framework"
|
|
85
|
+
url: str = ""
|
|
86
|
+
version: str = "0.1.0"
|
|
87
|
+
capabilities: list[str] = field(default_factory=lambda: ["chat", "tool_use"])
|
|
88
|
+
skills: list[str] = field(default_factory=list)
|
|
89
|
+
authentication: dict[str, Any] = field(default_factory=lambda: {"schemes": ["bearer"]})
|
|
90
|
+
|
|
91
|
+
def to_dict(self) -> dict[str, Any]:
|
|
92
|
+
return {
|
|
93
|
+
"name": self.name, "description": self.description,
|
|
94
|
+
"url": self.url, "version": self.version,
|
|
95
|
+
"capabilities": {"streaming": True, "pushNotifications": False},
|
|
96
|
+
"skills": [{"id": s, "name": s} for s in self.skills],
|
|
97
|
+
"authentication": self.authentication,
|
|
98
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""A2A JSON-RPC protocol handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Callable, Awaitable
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from echo_agent.a2a.models import A2ATask, A2AMessage, TaskState
|
|
11
|
+
|
|
12
|
+
# JSON-RPC 2.0 标准错误码
|
|
13
|
+
_JSONRPC_PARSE_ERROR = -32700
|
|
14
|
+
_JSONRPC_METHOD_NOT_FOUND = -32601
|
|
15
|
+
_JSONRPC_INTERNAL_ERROR = -32603
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class A2AProtocol:
|
|
19
|
+
"""Handles A2A JSON-RPC methods: tasks/send, tasks/get, tasks/cancel."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, process_fn: Callable[[A2ATask], Awaitable[A2ATask]]):
|
|
22
|
+
self._process = process_fn
|
|
23
|
+
self._tasks: dict[str, A2ATask] = {}
|
|
24
|
+
|
|
25
|
+
async def handle(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
26
|
+
"""分发 JSON-RPC 请求到对应的处理方法。
|
|
27
|
+
|
|
28
|
+
支持的方法: tasks/send(发送任务), tasks/get(查询任务), tasks/cancel(取消任务)。
|
|
29
|
+
"""
|
|
30
|
+
method = request.get("method", "")
|
|
31
|
+
params = request.get("params", {})
|
|
32
|
+
req_id = request.get("id")
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
if method == "tasks/send":
|
|
36
|
+
result = await self._handle_send(params)
|
|
37
|
+
elif method == "tasks/get":
|
|
38
|
+
result = self._handle_get(params)
|
|
39
|
+
elif method == "tasks/cancel":
|
|
40
|
+
result = self._handle_cancel(params)
|
|
41
|
+
else:
|
|
42
|
+
return self._error(req_id, _JSONRPC_METHOD_NOT_FOUND, f"Method not found: {method}")
|
|
43
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": result}
|
|
44
|
+
except Exception as e:
|
|
45
|
+
logger.error("A2A protocol error: {}", e)
|
|
46
|
+
return self._error(req_id, _JSONRPC_INTERNAL_ERROR, str(e))
|
|
47
|
+
|
|
48
|
+
async def _handle_send(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
49
|
+
"""处理任务发送请求。如果任务 ID 已存在则追加消息,否则创建新任务。"""
|
|
50
|
+
task_id = params.get("id", "")
|
|
51
|
+
message = params.get("message", {})
|
|
52
|
+
|
|
53
|
+
if task_id and task_id in self._tasks:
|
|
54
|
+
task = self._tasks[task_id]
|
|
55
|
+
task.messages.append(A2AMessage.from_dict(message))
|
|
56
|
+
else:
|
|
57
|
+
task = A2ATask()
|
|
58
|
+
if task_id:
|
|
59
|
+
task.id = task_id
|
|
60
|
+
task.messages.append(A2AMessage.from_dict(message))
|
|
61
|
+
self._tasks[task.id] = task
|
|
62
|
+
|
|
63
|
+
task.state = TaskState.WORKING
|
|
64
|
+
task = await self._process(task)
|
|
65
|
+
self._tasks[task.id] = task
|
|
66
|
+
return task.to_dict()
|
|
67
|
+
|
|
68
|
+
def _handle_get(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
69
|
+
task_id = params.get("id", "")
|
|
70
|
+
task = self._tasks.get(task_id)
|
|
71
|
+
if not task:
|
|
72
|
+
raise ValueError(f"Task not found: {task_id}")
|
|
73
|
+
return task.to_dict()
|
|
74
|
+
|
|
75
|
+
def _handle_cancel(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
76
|
+
task_id = params.get("id", "")
|
|
77
|
+
task = self._tasks.get(task_id)
|
|
78
|
+
if not task:
|
|
79
|
+
raise ValueError(f"Task not found: {task_id}")
|
|
80
|
+
task.state = TaskState.CANCELED
|
|
81
|
+
return task.to_dict()
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _error(req_id: Any, code: int, message: str) -> dict[str, Any]:
|
|
85
|
+
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}}
|
echo_agent/a2a/server.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""A2A HTTP server — agent card, task handling, SSE streaming."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from aiohttp import web
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from echo_agent.a2a.models import AgentCard, A2ATask, A2AMessage, TaskState
|
|
12
|
+
from echo_agent.a2a.protocol import A2AProtocol
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from echo_agent.agent.loop import AgentLoop
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class A2AServer:
|
|
19
|
+
def __init__(self, agent_loop: AgentLoop, agent_card: AgentCard):
|
|
20
|
+
self._loop = agent_loop
|
|
21
|
+
self._card = agent_card
|
|
22
|
+
self._protocol = A2AProtocol(self._process_task)
|
|
23
|
+
|
|
24
|
+
def register_routes(self, app: web.Application) -> None:
|
|
25
|
+
app.router.add_get("/.well-known/agent.json", self._handle_agent_card)
|
|
26
|
+
app.router.add_post("/a2a", self._handle_rpc)
|
|
27
|
+
logger.info("A2A routes registered: /.well-known/agent.json, /a2a")
|
|
28
|
+
|
|
29
|
+
async def _handle_agent_card(self, request: web.Request) -> web.Response:
|
|
30
|
+
return web.json_response(self._card.to_dict())
|
|
31
|
+
|
|
32
|
+
async def _handle_rpc(self, request: web.Request) -> web.Response:
|
|
33
|
+
try:
|
|
34
|
+
body = await request.json()
|
|
35
|
+
except json.JSONDecodeError:
|
|
36
|
+
return web.json_response(
|
|
37
|
+
{"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error"}},
|
|
38
|
+
status=400,
|
|
39
|
+
)
|
|
40
|
+
result = await self._protocol.handle(body)
|
|
41
|
+
return web.json_response(result)
|
|
42
|
+
|
|
43
|
+
async def _process_task(self, task: A2ATask) -> A2ATask:
|
|
44
|
+
"""Process an A2A task by routing through the agent loop."""
|
|
45
|
+
user_text = ""
|
|
46
|
+
for msg in task.messages:
|
|
47
|
+
if msg.role == "user":
|
|
48
|
+
user_text = msg.text_content
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
if not user_text:
|
|
52
|
+
task.state = TaskState.FAILED
|
|
53
|
+
task.messages.append(A2AMessage.text("agent", "No user message found"))
|
|
54
|
+
return task
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
from echo_agent.bus.events import InboundEvent
|
|
58
|
+
event = InboundEvent.text_message(
|
|
59
|
+
channel="a2a", sender_id="a2a-client",
|
|
60
|
+
chat_id=f"a2a:{task.id}", text=user_text,
|
|
61
|
+
)
|
|
62
|
+
import uuid
|
|
63
|
+
result = await self._loop._process_event(event, trace_id=uuid.uuid4().hex[:12])
|
|
64
|
+
task.state = TaskState.COMPLETED
|
|
65
|
+
task.messages.append(A2AMessage.text("agent", result.response_text or ""))
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.error("A2A task processing failed: {}", e)
|
|
68
|
+
task.state = TaskState.FAILED
|
|
69
|
+
task.messages.append(A2AMessage.text("agent", f"Error: {e}"))
|
|
70
|
+
|
|
71
|
+
return task
|
|
File without changes
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Approval and security gate for tool calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from echo_agent.agent.tools.base import ToolResult
|
|
11
|
+
from echo_agent.bus.events import InboundEvent, OutboundEvent
|
|
12
|
+
from echo_agent.bus.queue import MessageBus
|
|
13
|
+
from echo_agent.config.schema import Config
|
|
14
|
+
from echo_agent.models.inference import InferenceController
|
|
15
|
+
from echo_agent.permissions.allowlist import ApprovalAllowlist, ApprovalLevel, build_pattern_key
|
|
16
|
+
from echo_agent.permissions.manager import ApprovalManager, ApprovalStatus
|
|
17
|
+
from echo_agent.security.guards import GuardDecision, evaluate_tool_call
|
|
18
|
+
from echo_agent.security.risk_classifier import RiskLevel, classify_risk
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ApprovalCheck:
|
|
23
|
+
denial: ToolResult | None = None
|
|
24
|
+
approved_actions: frozenset[str] = frozenset()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ApprovalGate:
|
|
28
|
+
"""Combines risk classification, channel trust, smart approval, and manual approval."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
config: Config,
|
|
34
|
+
approval: ApprovalManager,
|
|
35
|
+
inference: InferenceController,
|
|
36
|
+
bus: MessageBus,
|
|
37
|
+
provider: Any = None,
|
|
38
|
+
allowlist: ApprovalAllowlist | None = None,
|
|
39
|
+
):
|
|
40
|
+
self._config = config
|
|
41
|
+
self._approval = approval
|
|
42
|
+
self._inference = inference
|
|
43
|
+
self._bus = bus
|
|
44
|
+
self._provider = provider
|
|
45
|
+
self._allowlist = allowlist or ApprovalAllowlist()
|
|
46
|
+
|
|
47
|
+
async def check(
|
|
48
|
+
self,
|
|
49
|
+
tool_name: str,
|
|
50
|
+
arguments: dict[str, Any],
|
|
51
|
+
sender_id: str,
|
|
52
|
+
*,
|
|
53
|
+
channel: str = "",
|
|
54
|
+
event: InboundEvent | None = None,
|
|
55
|
+
running: bool = True,
|
|
56
|
+
) -> ApprovalCheck:
|
|
57
|
+
approval_cfg = self._config.permissions.approval
|
|
58
|
+
|
|
59
|
+
# Step 1: Static guard (hard block dangerous patterns)
|
|
60
|
+
guard = evaluate_tool_call(self._config, tool_name, arguments)
|
|
61
|
+
if guard.denied:
|
|
62
|
+
return ApprovalCheck(ToolResult(
|
|
63
|
+
success=False,
|
|
64
|
+
error=f"Tool '{tool_name}' blocked by security policy: {guard.reason}",
|
|
65
|
+
metadata={"guard_pattern": guard.pattern_key},
|
|
66
|
+
))
|
|
67
|
+
|
|
68
|
+
# Step 2: Elevated rights check
|
|
69
|
+
if self._requires_elevated(tool_name) and not self._elevated_allowed(channel, sender_id):
|
|
70
|
+
return ApprovalCheck(ToolResult(
|
|
71
|
+
success=False,
|
|
72
|
+
error=(
|
|
73
|
+
f"Tool '{tool_name}' requires elevated execution rights for the configured "
|
|
74
|
+
"executor/security policy."
|
|
75
|
+
),
|
|
76
|
+
metadata={"requires_elevated": True},
|
|
77
|
+
))
|
|
78
|
+
|
|
79
|
+
# Step 3: Risk classification
|
|
80
|
+
risk = classify_risk(tool_name, arguments)
|
|
81
|
+
|
|
82
|
+
# Approved-pass: any path that lets the call through must tell the tool
|
|
83
|
+
# which actions were approved. The tool's own guard (e.g. CodeExecTool)
|
|
84
|
+
# re-checks exec/code policy and blocks unless ctx.approved_actions
|
|
85
|
+
# contains the action — so returning an empty ApprovalCheck() here would
|
|
86
|
+
# silently fail EXEC tools even though the gate "approved" them.
|
|
87
|
+
def _approved() -> ApprovalCheck:
|
|
88
|
+
return ApprovalCheck(approved_actions=self._approved_actions(tool_name, guard))
|
|
89
|
+
|
|
90
|
+
# Step 4: READ_ONLY and WRITE always pass — they are protected by sandbox/path restrictions
|
|
91
|
+
if risk in (RiskLevel.READ_ONLY, RiskLevel.WRITE):
|
|
92
|
+
return _approved()
|
|
93
|
+
|
|
94
|
+
# Step 5: Explicit auto_approve list
|
|
95
|
+
if tool_name in approval_cfg.auto_approve:
|
|
96
|
+
return _approved()
|
|
97
|
+
|
|
98
|
+
# Step 6: CLI auto-approve (all levels)
|
|
99
|
+
if self._should_auto_approve_cli(channel):
|
|
100
|
+
return _approved()
|
|
101
|
+
|
|
102
|
+
# Step 7: Channel trust — EXEC level auto-approved on trusted channels
|
|
103
|
+
if risk == RiskLevel.EXEC and self._is_trusted_channel(channel):
|
|
104
|
+
return _approved()
|
|
105
|
+
|
|
106
|
+
# Step 8: Approval mode "off" — bypass everything except hard blocks
|
|
107
|
+
if approval_cfg.mode == "off":
|
|
108
|
+
return _approved()
|
|
109
|
+
|
|
110
|
+
# Step 9: Check if this tool actually requires approval
|
|
111
|
+
if not self._approval_required(tool_name, guard, risk):
|
|
112
|
+
return _approved()
|
|
113
|
+
|
|
114
|
+
# Step 10: Allowlist check (EXEC level)
|
|
115
|
+
session_key = event.session_key if event else ""
|
|
116
|
+
pattern_key = build_pattern_key(tool_name, arguments)
|
|
117
|
+
if risk == RiskLevel.EXEC and self._allowlist.is_approved(session_key, pattern_key):
|
|
118
|
+
return _approved()
|
|
119
|
+
|
|
120
|
+
# Step 11: Unattended mode check
|
|
121
|
+
if self._is_unattended(event, channel):
|
|
122
|
+
return self._resolve_unattended(tool_name, risk, session_key, pattern_key, guard)
|
|
123
|
+
|
|
124
|
+
# Step 12: Smart approval (EXEC level only)
|
|
125
|
+
if risk == RiskLevel.EXEC and approval_cfg.mode == "smart" and self._provider:
|
|
126
|
+
verdict = await self._run_smart_approval(tool_name, arguments, guard)
|
|
127
|
+
if verdict == "approve":
|
|
128
|
+
self._allowlist.approve(session_key, pattern_key, ApprovalLevel.SESSION)
|
|
129
|
+
return _approved()
|
|
130
|
+
if verdict == "deny":
|
|
131
|
+
return ApprovalCheck(ToolResult(
|
|
132
|
+
success=False,
|
|
133
|
+
error=f"Smart approval denied '{tool_name}': {guard.reason or 'assessed as dangerous'}",
|
|
134
|
+
))
|
|
135
|
+
|
|
136
|
+
# Step 13: Manual approval flow
|
|
137
|
+
approved_actions = self._approved_actions(tool_name, guard)
|
|
138
|
+
return await self._manual_approval_flow(
|
|
139
|
+
tool_name, arguments, sender_id, channel, event, running,
|
|
140
|
+
guard, approved_actions, session_key, pattern_key,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# PLACEHOLDER_METHODS
|
|
144
|
+
|
|
145
|
+
async def _run_smart_approval(
|
|
146
|
+
self, tool_name: str, arguments: dict[str, Any], guard: GuardDecision,
|
|
147
|
+
) -> str:
|
|
148
|
+
from echo_agent.security.smart_approval import smart_approve
|
|
149
|
+
command = str(arguments.get("command", "") or arguments.get("code", "") or arguments)
|
|
150
|
+
description = guard.reason or f"tool '{tool_name}' requires approval"
|
|
151
|
+
model = self._config.permissions.approval.smart_model
|
|
152
|
+
return await smart_approve(tool_name, command, description, self._provider, model=model)
|
|
153
|
+
|
|
154
|
+
async def _manual_approval_flow(
|
|
155
|
+
self,
|
|
156
|
+
tool_name: str,
|
|
157
|
+
arguments: dict[str, Any],
|
|
158
|
+
sender_id: str,
|
|
159
|
+
channel: str,
|
|
160
|
+
event: InboundEvent | None,
|
|
161
|
+
running: bool,
|
|
162
|
+
guard: GuardDecision,
|
|
163
|
+
approved_actions: frozenset[str],
|
|
164
|
+
session_key: str,
|
|
165
|
+
pattern_key: str,
|
|
166
|
+
) -> ApprovalCheck:
|
|
167
|
+
approval_req = self._approval.request_approval(
|
|
168
|
+
tool_name, tool_name=tool_name, params=arguments, user_id=sender_id,
|
|
169
|
+
)
|
|
170
|
+
if approval_req.status == ApprovalStatus.DENIED:
|
|
171
|
+
return ApprovalCheck(ToolResult(
|
|
172
|
+
success=False,
|
|
173
|
+
error=f"Tool '{tool_name}' denied by approval policy: {approval_req.reason}",
|
|
174
|
+
))
|
|
175
|
+
if approval_req.status == ApprovalStatus.APPROVED:
|
|
176
|
+
return ApprovalCheck(approved_actions=approved_actions)
|
|
177
|
+
|
|
178
|
+
if event is not None:
|
|
179
|
+
await self._publish_approval_request(event, approval_req.id, tool_name, guard, pattern_key)
|
|
180
|
+
|
|
181
|
+
if not running and channel in {"cli", "direct", ""}:
|
|
182
|
+
return ApprovalCheck(ToolResult(
|
|
183
|
+
success=False,
|
|
184
|
+
error=(
|
|
185
|
+
f"Approval required before executing '{tool_name}'. "
|
|
186
|
+
f"Request id: {approval_req.id}."
|
|
187
|
+
),
|
|
188
|
+
metadata={"approval_request_id": approval_req.id},
|
|
189
|
+
))
|
|
190
|
+
|
|
191
|
+
decided = await self._approval.wait_for_decision(
|
|
192
|
+
approval_req.id,
|
|
193
|
+
timeout_seconds=self._config.permissions.approval.wait_timeout_seconds,
|
|
194
|
+
)
|
|
195
|
+
if decided and decided.status == ApprovalStatus.APPROVED:
|
|
196
|
+
level = self._parse_approval_level(decided.reason)
|
|
197
|
+
self._allowlist.approve(session_key, pattern_key, level)
|
|
198
|
+
return ApprovalCheck(approved_actions=approved_actions)
|
|
199
|
+
if decided and decided.status == ApprovalStatus.DENIED:
|
|
200
|
+
return ApprovalCheck(ToolResult(
|
|
201
|
+
success=False,
|
|
202
|
+
error=f"Tool '{tool_name}' denied: {decided.reason}",
|
|
203
|
+
metadata={"approval_request_id": approval_req.id},
|
|
204
|
+
))
|
|
205
|
+
return ApprovalCheck(ToolResult(
|
|
206
|
+
success=False,
|
|
207
|
+
error=(
|
|
208
|
+
f"Approval timed out for '{tool_name}'. "
|
|
209
|
+
f"Request id: {approval_req.id}. "
|
|
210
|
+
f"Reply `/approve {approval_req.id}` or `/deny {approval_req.id} <reason>`."
|
|
211
|
+
),
|
|
212
|
+
metadata={"approval_request_id": approval_req.id},
|
|
213
|
+
))
|
|
214
|
+
|
|
215
|
+
# PLACEHOLDER_HELPERS
|
|
216
|
+
|
|
217
|
+
async def _publish_approval_request(
|
|
218
|
+
self,
|
|
219
|
+
event: InboundEvent,
|
|
220
|
+
request_id: str,
|
|
221
|
+
tool_name: str,
|
|
222
|
+
guard: GuardDecision,
|
|
223
|
+
pattern_key: str,
|
|
224
|
+
) -> None:
|
|
225
|
+
reason = guard.reason or "approval policy requires confirmation"
|
|
226
|
+
text = (
|
|
227
|
+
f"⚠️ 需要确认执行: {tool_name}\n"
|
|
228
|
+
f"原因: {reason}\n\n"
|
|
229
|
+
f"回复:\n"
|
|
230
|
+
f" /approve {request_id} — 允许本次\n"
|
|
231
|
+
f" /approve {request_id} session — 本会话内允许同类操作\n"
|
|
232
|
+
f" /approve {request_id} always — 永久允许同类操作\n"
|
|
233
|
+
f" /deny {request_id} [原因] — 拒绝"
|
|
234
|
+
)
|
|
235
|
+
out = OutboundEvent.text_reply(
|
|
236
|
+
channel=event.channel,
|
|
237
|
+
chat_id=event.chat_id,
|
|
238
|
+
text=text,
|
|
239
|
+
reply_to_id=event.reply_to_id,
|
|
240
|
+
)
|
|
241
|
+
out.metadata = dict(event.metadata)
|
|
242
|
+
out.metadata["_approval_request"] = True
|
|
243
|
+
out.metadata["_inbound_event_id"] = event.event_id
|
|
244
|
+
await self._bus.publish_outbound(out)
|
|
245
|
+
|
|
246
|
+
def _approval_required(self, tool_name: str, guard: GuardDecision, risk: RiskLevel) -> bool:
|
|
247
|
+
approval_cfg = self._config.permissions.approval
|
|
248
|
+
if tool_name in approval_cfg.auto_deny:
|
|
249
|
+
return True
|
|
250
|
+
if guard.needs_approval:
|
|
251
|
+
return True
|
|
252
|
+
if self._inference.needs_confirmation(tool_name):
|
|
253
|
+
return True
|
|
254
|
+
if tool_name in approval_cfg.require_approval:
|
|
255
|
+
return True
|
|
256
|
+
if risk == RiskLevel.DANGEROUS:
|
|
257
|
+
return True
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
def _should_auto_approve_cli(self, channel: str) -> bool:
|
|
261
|
+
approval_cfg = self._config.permissions.approval
|
|
262
|
+
return (
|
|
263
|
+
self._config.security.profile == "personal_cli"
|
|
264
|
+
and approval_cfg.cli_auto_approve
|
|
265
|
+
and channel in {"cli", "direct", ""}
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def _is_trusted_channel(self, channel: str) -> bool:
|
|
269
|
+
return channel in self._config.permissions.approval.trusted_channels
|
|
270
|
+
|
|
271
|
+
def _is_unattended(self, event: InboundEvent | None, channel: str) -> bool:
|
|
272
|
+
if event and event.metadata.get("_unattended"):
|
|
273
|
+
return True
|
|
274
|
+
return channel in {"cron", "scheduler"}
|
|
275
|
+
|
|
276
|
+
def _resolve_unattended(
|
|
277
|
+
self, tool_name: str, risk: RiskLevel, session_key: str, pattern_key: str, guard: GuardDecision,
|
|
278
|
+
) -> ApprovalCheck:
|
|
279
|
+
policy = self._config.permissions.approval.unattended_policy
|
|
280
|
+
approved = self._approved_actions(tool_name, guard)
|
|
281
|
+
if policy == "allow_safe":
|
|
282
|
+
if risk == RiskLevel.WRITE:
|
|
283
|
+
return ApprovalCheck(approved_actions=approved)
|
|
284
|
+
if risk == RiskLevel.EXEC and self._allowlist.is_approved(session_key, pattern_key):
|
|
285
|
+
return ApprovalCheck(approved_actions=approved)
|
|
286
|
+
return ApprovalCheck(ToolResult(
|
|
287
|
+
success=False,
|
|
288
|
+
error=f"Tool '{tool_name}' requires approval but no user is available (unattended mode).",
|
|
289
|
+
))
|
|
290
|
+
|
|
291
|
+
@staticmethod
|
|
292
|
+
def _approved_actions(tool_name: str, guard: GuardDecision) -> frozenset[str]:
|
|
293
|
+
actions = {tool_name}
|
|
294
|
+
if guard.pattern_key:
|
|
295
|
+
actions.add(guard.pattern_key)
|
|
296
|
+
if guard.approval_action:
|
|
297
|
+
actions.add(guard.approval_action)
|
|
298
|
+
return frozenset(actions)
|
|
299
|
+
|
|
300
|
+
@staticmethod
|
|
301
|
+
def _parse_approval_level(reason: str) -> ApprovalLevel:
|
|
302
|
+
if reason:
|
|
303
|
+
lower = reason.strip().lower()
|
|
304
|
+
if lower == "always":
|
|
305
|
+
return ApprovalLevel.ALWAYS
|
|
306
|
+
if lower == "session":
|
|
307
|
+
return ApprovalLevel.SESSION
|
|
308
|
+
return ApprovalLevel.ONCE
|
|
309
|
+
|
|
310
|
+
def _requires_elevated(self, tool_name: str) -> bool:
|
|
311
|
+
if tool_name not in {"exec", "execute_code", "process"}:
|
|
312
|
+
return False
|
|
313
|
+
host = self._config.tools.exec.host
|
|
314
|
+
if host == "auto":
|
|
315
|
+
host = self._config.execution.default_executor
|
|
316
|
+
return host in {"local", "remote"} or self._config.tools.exec.security == "full"
|
|
317
|
+
|
|
318
|
+
def _elevated_allowed(self, channel: str, sender_id: str) -> bool:
|
|
319
|
+
elevated = self._config.permissions.elevated
|
|
320
|
+
if not elevated.enabled:
|
|
321
|
+
return False
|
|
322
|
+
allow_from = elevated.allow_from or {}
|
|
323
|
+
candidates = set(allow_from.get("*", [])) | set(allow_from.get(channel, []))
|
|
324
|
+
if "*" in candidates or sender_id in candidates:
|
|
325
|
+
return True
|
|
326
|
+
return sender_id in (self._config.permissions.admin_users or [])
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Context compression engine — multi-phase intelligent context management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from echo_agent.agent.compression.compressor import ConversationCompressor
|
|
6
|
+
from echo_agent.agent.compression.engine import ContextEngine
|
|
7
|
+
from echo_agent.agent.compression.types import CompressionResult, CompressionStats
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ContextEngine",
|
|
11
|
+
"ConversationCompressor",
|
|
12
|
+
"CompressionResult",
|
|
13
|
+
"CompressionStats",
|
|
14
|
+
]
|