# yier-agents

Python Agent 系统,参考 OpenCode 架构设计,支持工具调用、子 Agent 和基于 finish_reason 的循环控制。

## 特性

- ✅ **finish_reason 驱动循环** - 自动处理工具调用和 token 限制
- ✅ **Pydantic 参数验证** - 类型安全的工具定义
- ✅ **ToolMiddleware** - 统一的工具管理和错误处理
- ✅ **SubAgent 支持** - Agent 可以作为工具被其他 Agent 调用
- ✅ **并发工具执行** - 使用 asyncio.gather 并行执行
- ✅ **OpenAI-compatible API** - 支持任何兼容 OpenAI 格式的 LLM
- ✅ **内置 Todo 工具** - AI 自动管理任务列表和进度跟踪
- ✅ **Verbose 调试模式** - 美观的彩色输出,实时查看执行过程
- ✅ **MCP 支持** - 通过 `MCPClient` 连接任意 MCP server,自动转换为 Tool 实例,支持 stdio 和 SSE 两种传输
- ✅ **会话记忆** - 自动保存和加载会话上下文,支持多轮对话
- ✅ **Skill 支持** - 通过 `Skill` 打包 system prompt 和 tools,复用 Agent 能力
- ✅ **上下文压缩** - 长对话可自动摘要旧消息,保留最近上下文
- ✅ **可持久化记忆** - 通过 `JSONSessionStore` 将 session memory 持久化到磁盘
- ✅ **思考过程支持** - 支持 reasoning_content 字段,兼容 o1、Claude thinking 等模型

## 安装

```bash
# 使用 uv
uv pip install -e .

# 或使用 pip
pip install -e .
```

## 快速开始

```python
import asyncio
from pydantic import BaseModel, Field
from yier_agents import Agent, LLM, Tool, ToolContext, ToolOutput


# 1. 定义工具参数
class ReadFileParams(BaseModel):
    path: str = Field(description="文件路径")


# 2. 定义工具执行函数
async def read_file(params: ReadFileParams, ctx: ToolContext) -> ToolOutput:
    with open(params.path) as f:
        content = f.read()
    return ToolOutput(content=f"File content:\n{content}")


# 3. 创建工具
read_tool = Tool(
    name="read_file",
    description="读取文件内容",
    parameters=ReadFileParams,
    execute=read_file
)

# 4. 创建 Agent
llm = LLM(
    base_url="https://api.openai.com/v1",
    api_key="your-key",
    model="gpt-4"
)

agent = Agent(
    llm=llm,
    tools=[read_tool],
    system_prompt="你是一个文件助手",
    verbose=True  # 启用详细的调试输出
)

# 5. 运行
result = await agent.run("读取 README.md 并总结")
print(result.final_message)
```

## 调试模式

使用 `verbose=True` 参数可以查看 Agent 执行的详细过程,方便调试:

```python
agent = Agent(
    llm=llm,
    tools=[tool1, tool2],
    verbose=True  # 启用详细输出
)

result = await agent.run("执行任务", "session-id")
```

**输出示例:**

```
ℹ ============================================================
ℹ Starting Agent execution (session: session-i...)
ℹ ============================================================
ℹ Added system prompt
ℹ User: 执行任务

ℹ ────────────────────────────────────────────────────────────
ℹ Iteration 1/20
ℹ ────────────────────────────────────────────────────────────
ℹ Calling LLM...
✓ LLM response received (finish_reason: tool_calls, usage: 150 tokens)
⚙ Executing 2 tool call(s)...
⚙   [1/2] read_file({"path": "README.md"})
⚙   [2/2] grep({"pattern": "Agent"})
✓ Received 2 tool result(s)
⚙   [1] File content: ...
⚙   [2] Found 5 matches

ℹ ────────────────────────────────────────────────────────────
ℹ Iteration 2/20
ℹ ────────────────────────────────────────────────────────────
ℹ Calling LLM...
✓ LLM response received (finish_reason: stop, usage: 200 tokens)
ℹ Content: 任务已完成
✓ Agent completed successfully
ℹ ============================================================
```

详见 [verbose 示例](examples/verbose_example.py)。

## 架构

```
Agent (执行循环)
  ├─> LLM (API 调用层)
  ├─> ToolMiddleware (工具管理和验证)
  │     ├─> Tool (工具定义)
  │     └─> Context (执行上下文)
  ├─> MCPClient (MCP server 连接)
  │     ├─> stdio transport (子进程)
  │     └─> SSE transport (HTTP)
  └─> Message (消息管理)
```

## Skills

`Skill` 用来打包一组工具和附加的 system prompt,适合构建你自己的可组合 Agent 能力层。

```python
from yier_agents import Agent, LLM, Skill

network_skill = Skill(
    name="network-basics",
    description="Basic network utilities",
    tools=[get_ip_tool],
    system_prompt="Use get_ip when the user asks about the local machine IP."
)

agent = Agent(
    llm=llm,
    tools=[],
    skills=[network_skill],
    system_prompt="你是一个轻量但可靠的终端助手"
)
```

## Lazy Skills

除了静态 `Skill` 之外,现在也支持 OpenCode 风格的按需加载 skills。它们从 `SKILL.md` 目录结构中发现,在系统提示里只暴露 `<available_skills>` 列表,真正需要时再通过内置 `skill_load` 工具加载完整内容。

支持的项目级目录:

- `.opencode/skill/<name>/SKILL.md`
- `.opencode/skills/<name>/SKILL.md`
- `.claude/skills/<name>/SKILL.md`
- `.agents/skills/<name>/SKILL.md`

```python
from pathlib import Path
from yier_agents import Agent, LLM, SkillCatalog

catalog = SkillCatalog.discover(
    Path.cwd(),
    include_project=True,
    include_global=False,
)

agent = Agent(
    llm=LLM(model="gpt-4"),
    tools=[],
    skill_catalog=catalog,
    system_prompt="你是一个支持 lazy skills 的编程助手",
)
```

如果某个 skill 目录里包含 `scripts/` 或 `references/`, `skill_load` 会把 skill 正文、base directory 和采样文件列表注入上下文。脚本不会自动执行,而是由后续普通工具去读取或运行。

内置文件/命令工具:

- `list_files` - 浏览 skill 目录或项目目录
- `read_file` - 读取脚本、参考资料和配置文件
- `replace_in_file` - 做精确文本替换式编辑
- `write_file` - 在允许的工作区内创建或更新文件
- `run_command` - 执行 skill 中提到的脚本或命令
- `search_files` - 进行 grep-like 文本搜索

## Session Memory

默认 memory 仍可直接使用,但现在也支持持久化 store 和上下文压缩。

```python
from pathlib import Path
from yier_agents import Agent, CompactionConfig, JSONSessionStore, LLM

agent = Agent(
    llm=LLM(model="gpt-4"),
    tools=[],
    session_store=JSONSessionStore(Path(".agent_storage/sessions")),
    compaction_config=CompactionConfig(
        enabled=True,
        trigger_message_count=24,
        preserve_recent_messages=8,
    ),
)
```

## Interactive Assistant CLI

如果你想直接启动一个交互式个人助手，而不是自己写一层循环，可以直接运行包内 CLI：

```bash
python -m yier_agents.yier_cli
```

安装为可执行脚本后也可以直接运行：

```bash
yier-cli
```

这个入口默认启用：

- 会话持久化记忆
- 长上下文摘要压缩
- lazy skills 发现与 `/skills` 浏览
- 受限的文件、搜索、写入与命令工具
- MCP 配置热重载

## MCP 支持

通过 `MCPClient` 连接任意 MCP server,所有工具自动转换为 `Tool` 实例,与本地工具用法完全一致。

```python
from yier_agents import MCPClient, Agent, LLM

llm = LLM(model="gpt-4")

# stdio 方式（本地子进程）
async with MCPClient.stdio("npx", ["-y", "@primevue/mcp"]) as client:
    tools = await client.get_tools()
    agent = Agent(llm=llm, tools=tools)
    result = await agent.run("DataTable 有哪些常用 props？", session_id="s1")

# SSE 方式（HTTP 远程）
async with MCPClient.sse("http://localhost:8000/sse") as client:
    tools = await client.get_tools()
    agent = Agent(llm=llm, tools=tools)
    result = await agent.run("执行任务", session_id="s2")

# 多个 MCP server 混合使用
async with (
    MCPClient.stdio("npx", ["-y", "@primevue/mcp"]) as mcp1,
    MCPClient.sse("http://localhost:8000/sse") as mcp2,
):
    tools = await mcp1.get_tools() + await mcp2.get_tools()
    agent = Agent(llm=llm, tools=tools)
```

**一次连接，多个 Agent 共享：**

```python
async with MCPClient.stdio("npx", ["-y", "@primevue/mcp"]) as client:
    tools = await client.get_tools()  # 只连接一次

    agent1 = Agent(llm=llm, tools=tools, system_prompt="你是前端专家")
    agent2 = Agent(llm=llm, tools=tools, system_prompt="你是无障碍专家")

    # 两个 agent 共享同一个 MCP 连接
    r1, r2 = await asyncio.gather(
        agent1.run("DataTable 怎么分页？", "session-1"),
        agent2.run("Button 的无障碍属性？", "session-2"),
    )
```

详见 [MCP 示例](examples/mcp_primevue_example.py)。

## SubAgent 示例

```python
# 创建专门的子 agent
search_agent = Agent(
    llm=llm,
    tools=[grep_tool, read_tool],
    system_prompt="你是代码搜索专家"
)

# 包装为工具
from yier_agents import AgentTool

search_tool = AgentTool(
    name="search_codebase",
    description="搜索和分析代码",
    agent=search_agent
)

# 主 agent 可以调用
main_agent = Agent(
    llm=llm,
    tools=[search_tool, edit_tool]
)

result = await main_agent.run("重构这个模块")
```

## Todo 工具

内置的任务跟踪工具,让 AI 自动管理多步骤任务的进度。

```python
from yier_agents import Agent, LLM, TodoWriteTool, TodoReadTool

# 创建带 todo 工具的 agent
agent = Agent(
    llm=llm,
    tools=[
        TodoWriteTool,  # AI 可以创建和更新任务列表
        TodoReadTool,   # AI 可以查看当前进度
        # ... 其他工具
    ],
    system_prompt="对于多步骤任务,使用 todo_write 工具跟踪进度。"
)

# AI 会自动使用 todo 工具管理任务
result = await agent.run("实现用户认证系统,包括登录、注册和密码重置")
```

**AI 自动管理 todos:**
- 接收多步骤任务时自动创建 todo 列表
- 开始每个任务前标记为 `in_progress`
- 完成后标记为 `completed`
- 实时跟踪进度

**Todo 状态:**
- `pending` ⏸ - 待执行
- `in_progress` ▶ - 执行中
- `completed` ✓ - 已完成

详见 [Todo 工具文档](docs/TODO_TOOLS.md) 和 [示例](examples/todo_example.py)。

## 开发

```bash
# 运行测试
pytest tests/ -v

# 运行特定测试
pytest tests/test_agent.py -v

# 查看覆盖率
pytest --cov=doc_agents tests/
```

## 设计文档

详细设计请参考:
- [设计文档](docs/plans/2026-02-08-python-agent-design.md)
- [实现计划](docs/plans/2026-02-08-python-agent-implementation.md)

## License

MIT
