Async event-driven Agent framework with tool orchestration and MCP support
Project description
MochiAgent
MochiAgent 是一个面向生产场景的异步 LLM Agent 框架,核心能力是「事件驱动会话 + 工具编排 + 安全约束 + MCP 集成」。
目录
环境要求
- Python
>=3.13
安装
从源码安装:
git clone <your-repo-url>
cd <repo-dir>
uv sync --extra dev
发布后安装:
pip install mochiagent
快速开始
import asyncio
from pathlib import Path
from agent import BaseAgent, Event, EventType, LLMConfig, Tool, get_agent, setup, shutdown
class EchoTool(Tool):
@property
def name(self) -> str:
return "echo"
@property
def description(self) -> str:
return "回显输入文本"
@property
def parameters_schema(self) -> dict:
return {
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
}
async def execute(self, text: str):
return {"text": text}
class DemoAgent(BaseAgent):
@property
def name(self) -> str:
return "demo_agent"
@property
def description(self) -> str:
return "示例 Agent"
@property
def skill_directory(self) -> Path | None:
return None
async def setup(self) -> None:
self.register_tool(EchoTool())
async def main() -> None:
await setup(agents=[DemoAgent()], max_concurrent=50, max_iterations=100)
agent = get_agent("demo_agent")
if agent is None:
raise RuntimeError("demo_agent not found")
llm_config = LLMConfig(
provider="openai",
model="gpt-4o-mini",
api_key="YOUR_API_KEY",
base_url="https://api.openai.com/v1",
stream=True,
openai_max_retries=2,
)
session = await agent.take_session("demo-session-1", llm_config)
queue: asyncio.Queue[Event] = asyncio.Queue()
async def listener(event: Event):
await queue.put(event)
session.add_listener(listener)
try:
await agent.push_message(session.session_id, "请调用 echo 工具并回复")
while True:
event = await queue.get()
if event.type == EventType.PART_CREATED:
part = event.data or {}
if part.get("type") == "text":
print(part.get("text", ""), end="", flush=True)
if event.type == EventType.LLM_ERROR:
print("\n[LLM_ERROR]", event.data)
break
if event.type == EventType.MESSAGE_DONE:
print("\n[DONE]")
break
finally:
session.remove_listener(listener)
await shutdown()
if __name__ == "__main__":
asyncio.run(main())
可运行示例
仓库提供可直接运行的最小示例,位于 examples/:
- 说明文档:
examples/README.md - 代码:
examples/minimal_chat.py
快速运行:
export OPENAI_API_KEY="your_key"
uv run python examples/minimal_chat.py "请调用 echo 工具并回复 hello"
事件系统
1) 统一事件包络(Event)
所有事件都使用同一个结构:
{
"type": "event.type",
"session_id": "sess_xxx",
"data": {},
"timestamp": "datetime对象",
"metadata": {}
}
字段说明:
type: 事件类型(见下方完整清单)session_id: 会话 IDdata: 事件负载(不同事件不同结构)timestamp: 事件创建时间(datetime)metadata: 预留元数据
2) 对外可订阅事件
对外稳定契约以 Session.add_listener() 为准,当前可收到 3 个事件:
part.createdmessage.donellm.error
3) 事件清单与 data 结构(对外)
part.created
触发时机:流式文本、思考片段、工具状态更新时。
data 有三种形态:
- 文本 Part(
type="text")
{
"id": "part_xxx",
"session_id": "sess_xxx",
"message_id": "msg_xxx",
"type": "text",
"text": "你好",
"synthetic": null,
"ignored": null,
"time": { "start": 1730000000000, "end": null },
"metadata": null
}
- 思考 Part(
type="reasoning")
{
"id": "part_xxx",
"session_id": "sess_xxx",
"message_id": "msg_xxx",
"type": "reasoning",
"text": "...",
"time": { "start": 1730000000000, "end": 1730000000123 },
"metadata": null
}
- 工具 Part(
type="tool")
{
"id": "part_xxx",
"session_id": "sess_xxx",
"message_id": "msg_xxx",
"type": "tool",
"call_id": "call_xxx",
"tool": "read_file",
"state": {
"status": "running",
"input": { "arguments": "{\"path\":\"README.md\"}" },
"title": "read_file",
"metadata": null,
"time": { "start": 1730000000000 }
},
"metadata": null
}
工具 state.status 可能值与字段:
running:status、input、title、metadata、time.startcompleted:status、input、output、summary、artifact_ref、artifact_path、raw_size_chars、truncated、title、metadata、time.start/enderror:status、input、error、metadata、time.start/end
message.done
触发时机:一轮助手消息结束时。
data 结构:
{
"message_id": "msg_xxx",
"cost": 0.0,
"tokens": { "input": 0, "output": 0, "reasoning": 0 },
"finish": "stop"
}
finish 常见值:
stop:正常结束error:异常结束(通常会先收到llm.error)max_iterations_exceeded:超过最大轮数- 其他 provider 返回的结束原因(原样透传)
llm.error
触发时机:LLM 调用异常、状态非法、超出最大轮数等。
data 基础结构:
{
"error": "错误描述",
"code": "可选错误码",
"hint": "可选修复建议"
}
当达到最大轮数时,还会包含:
{
"error": "Conversation stopped: maximum iterations exceeded (100)",
"code": "MAX_ITERATIONS_EXCEEDED",
"max_iterations": 100,
"iterations": 100
}
4) 常见事件顺序(对外)
正常路径:
part.created (text/reasoning/tool)
part.created (tool completed/error,可选)
message.done
异常路径:
llm.error
message.done
配置说明
1) 框架启动参数(setup)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
agents |
list[BaseAgent] |
None |
要注册的 Agent 实例列表 |
storage |
StorageProvider |
MemoryStorage() |
会话/消息/artifact 存储后端 |
max_concurrent |
int |
50 |
事件处理最大并发 |
max_iterations |
int |
100 |
单次对话最大 LLM 轮数 |
2) LLM 配置(LLMConfig)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
provider |
str |
- | 提供商标识(如 openai) |
model |
str |
- | 模型名 |
api_key |
str | None |
None |
API Key |
base_url |
str | None |
None |
OpenAI 兼容网关地址 |
temperature |
float |
0.7 |
采样温度 |
max_tokens |
int | None |
None |
最大生成 token |
stream |
bool |
True |
是否流式 |
timeout |
int |
60 |
请求超时(秒) |
openai_max_retries |
int | None |
None |
OpenAI SDK 重试次数 |
extra_params |
dict[str, Any] |
{} |
透传给 provider 的额外参数 |
3) 工具运行配置(ToolRuntimeConfig)
BaseAgent(tools=...) 支持细粒度工具运行约束(超时、白黑名单、工作区限制、命令 token 防护、输出截断等)。
推荐生产配置:
- 默认禁用
exec:policy.deny={"exec"} - 开启工作区约束:
workspace.restrict=True - 开启命令防护:
enforce_command_guard=True - 永远不要把原始用户输入直接拼进 shell 命令
Exec 风险与常见问题
exec 是高风险工具(基于 shell 执行命令),建议仅在受控环境启用。常见问题如下:
- 策略拦截:如果命中工具策略,会返回
TOOL_POLICY_DENIED。 - 安全拦截:如果命令包含禁止 token(如
`、$(、换行)或路径越界,会返回TOOL_SECURITY_DENIED。 - 执行超时:超过工具超时时间会返回
Tool execution timeout after <N>s。 - 输出被截断:
exec会保留完整stdout/stderr字段,但聚合输出会按exec_max_output_chars截断,truncated=true表示已截断。 - shell 语义差异:命令由 shell 解析,重定向、引用和通配符行为与直接进程调用不同,容易出现“本地可跑、线上失败”。
排查建议:
- 在
part.created的 tool 结果里优先看state.status、state.metadata.error、truncated。 - 先最小化命令(去掉管道和重定向)确认问题边界,再逐步恢复。
- 生产环境默认禁用
exec,仅在容器/低权限账户中按需放开。
LLM 接入
1) 默认方式:OpenAI 兼容协议
可接入 OpenAI 官方或任何兼容网关。
from agent import LLMConfig
llm_config = LLMConfig(
provider="openai",
model="gpt-4o-mini",
api_key="YOUR_API_KEY",
base_url="https://api.openai.com/v1",
stream=True,
timeout=60,
openai_max_retries=2,
)
2) 自定义 Provider
可实现 LLMProvider 并注册到 ProviderRegistry。
from typing import Any, AsyncIterator
from agent import get_framework
from agent.core.llm import LLMProvider
class MyProvider(LLMProvider):
async def stream_chat(self, messages: list[dict[str, Any]], tools=None, **kwargs: Any) -> AsyncIterator[dict[str, Any]]:
yield {"content": "hello", "finish_reason": "stop"}
async def complete(self, messages, tools=None, **kwargs):
return {"content": "hello", "finish_reason": "stop", "tool_calls": []}
framework = get_framework()
framework.provider_registry.register("my_provider", MyProvider)
MCP 集成
在 Agent 中覆盖 mcp_config_path,并在 setup() 里调用 register_mcp_tools()。
from pathlib import Path
class DemoAgent(BaseAgent):
@property
def mcp_config_path(self) -> Path | None:
return Path(__file__).with_name("mcp.json")
async def setup(self) -> None:
await self.register_mcp_tools()
mcp.json 示例:
{
"mcpServers": {
"docs": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
"connectTimeoutMs": 8000,
"maxRetries": 2,
"retryInitialMs": 300,
"retryMaxMs": 3000,
"failureThreshold": 3,
"cooldownSec": 20,
"toolTimeout": 30
}
}
}
Skills(技能系统)
Skills 用来复用领域提示词/流程模板。典型用法:
- 在 Agent
setup()里调用register_skill("skill-name") - 框架统一提供
skill工具 - 模型在运行时通过
skill(name, context)读取技能内容
目录示例:
my_agent/
├── agent.py
└── skills/
├── data-analysis/
│ └── SKILL.md
└── sql-query/
└── SKILL.md
存储扩展
默认 MemoryStorage 适合开发和测试,不适合生产持久化。
生产环境建议实现并注入 StorageProvider:
await setup(
agents=[DemoAgent()],
storage=MyStorage(),
)
发布流程
已配置自动发布工作流:.github/workflows/release.yml
- 触发方式:推送 tag(如
v0.1.0) - 自动执行:
- 构建
sdist/wheel - 创建 GitHub Release 并上传构建产物
- 发布到 PyPI(基于 GitHub OIDC Trusted Publishing)
- 构建
发布前请先在 PyPI 项目设置中配置 Trusted Publisher,绑定当前仓库与 workflow。
示例:
git tag v0.1.0
git push origin v0.1.0
核心 API
setup(agents, storage, max_concurrent, max_iterations)get_agent(agent_name)shutdown()BaseAgent.take_session(session_id, llm_config)BaseAgent.push_message(session_id, message)BaseAgent.register_skill(skill_name)Session.add_listener(listener)/Session.remove_listener(listener)
变更日志
发布变更记录见:CHANGELOG.md
许可证
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mochiagent-0.1.0.tar.gz.
File metadata
- Download URL: mochiagent-0.1.0.tar.gz
- Upload date:
- Size: 101.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23b7e99d9dfb671255a80b63b4a5eb397c6ae4314760eff7c8c433ca87c3e932
|
|
| MD5 |
bc1ba7ec2afda77cb299c547117fc8a3
|
|
| BLAKE2b-256 |
48929fa3df0a27a22b939878f09deb8a227900940a31c03b66f51e8f5c1a997b
|
Provenance
The following attestation bundles were made for mochiagent-0.1.0.tar.gz:
Publisher:
release.yml on Laelaps43/mochiagent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mochiagent-0.1.0.tar.gz -
Subject digest:
23b7e99d9dfb671255a80b63b4a5eb397c6ae4314760eff7c8c433ca87c3e932 - Sigstore transparency entry: 991406829
- Sigstore integration time:
-
Permalink:
Laelaps43/mochiagent@144c0de160b55f45a5d581f3a9d8f32cdaab39f7 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Laelaps43
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@144c0de160b55f45a5d581f3a9d8f32cdaab39f7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mochiagent-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mochiagent-0.1.0-py3-none-any.whl
- Upload date:
- Size: 76.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
77b20ab7ce910af24b1994474cf4d2383eb848ccc45e2e04100394ad74a24713
|
|
| MD5 |
caafe6e0a06eabc63f6350b13760d022
|
|
| BLAKE2b-256 |
5470f3fe4b5e08bcc3ec0773c6c93ef5032a5eede854eac013b540c321c9d9e1
|
Provenance
The following attestation bundles were made for mochiagent-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Laelaps43/mochiagent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mochiagent-0.1.0-py3-none-any.whl -
Subject digest:
77b20ab7ce910af24b1994474cf4d2383eb848ccc45e2e04100394ad74a24713 - Sigstore transparency entry: 991406838
- Sigstore integration time:
-
Permalink:
Laelaps43/mochiagent@144c0de160b55f45a5d581f3a9d8f32cdaab39f7 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Laelaps43
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@144c0de160b55f45a5d581f3a9d8f32cdaab39f7 -
Trigger Event:
push
-
Statement type: