Metadata-Version: 2.4
Name: yier_agents
Version: 0.4.0
Summary: Lightweight Python agents with tools, memory, MCP, and lazy skills.
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: environs>=14.6.0
Requires-Dist: httpx>=0.28.1
Requires-Dist: jsonschema>=4.23.0
Requires-Dist: pydantic>=2.0.3
Requires-Dist: prompt_toolkit>=3.0.51
Requires-Dist: questionary>=2.1.1
Requires-Dist: rich>=14.3.3
Requires-Dist: pyte>=0.8.2
Requires-Dist: pathspec>=0.12.1
Provides-Extra: mcp
Requires-Dist: mcp>=2.0.0; extra == "mcp"

# yier-agents

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

版本更新与不兼容变更见 [CHANGELOG · v0.4.0](https://github.com/Sube-py/yier_agents/blob/v0.4.0/CHANGELOG.md)。changelog 随源码包发布，并包含在 wheel 的 `yier_agents-0.4.0.data/data/share/yier_agents/CHANGELOG.md` 中（安装后位于 Python 环境的 `share/yier_agents/CHANGELOG.md`）。

## 特性

- ✅ **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 等模型
- ✅ **交互式 CLI 助手** - 内置 `yier-cli`,支持会话记忆、skills、MCP 和工作区工具
- ✅ **PTY 终端与任务排队** - 长任务可后台执行,完成后自动提醒并触发后续 follow-up

## 宿主应用集成

已针对 Bubu 的集成反馈重构公共 API：MCP 内存配置与租约、公开协议模型和工具适配器、JSON Schema 本地校验、统一 Agent 执行循环、真实 SSE token 流、取消/deadline、中间件与动态工具，以及异步会话存储适配和原子文件写入。

本轮进一步统一了事件信封与结果载荷、宿主日志、显式模型配置和 token 预算压缩，迁移见 [宿主契约加固](docs/HARDENING.md)。

完整示例、行为约定与兼容性变化见 [宿主集成与迁移说明](docs/HOST_INTEGRATION.md)。其中 `MCPManager()` 默认空配置，文件模式需显式传 `config_dir`；同名工具默认报错；LLM 库层不再读取 `.env` 或弹出配置询问。

## 安装

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

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

### 可选依赖：MCP

MCP 支持是**可选的**，需要额外安装：

```bash
pip install "yier_agents[mcp]"
```

之所以拆成 extra，是因为 `mcp` 包会带进 starlette、uvicorn、pyjwt、jsonschema 等一大串传递依赖，而它们经常和宿主应用已锁定的版本冲突。不装也不影响其它功能——`import yier_agents` 照常可用，只是真正去连接 MCP server 时会提示你安装。

## 打包与发版

推荐通过 `Makefile` 管理版本和构建产物，这样每次打包前都会先清掉旧的 `dist/`、`build/` 和 `*.egg-info`，避免安装到旧 wheel，或者看到过期的 `METADATA`。

```bash
# 查看当前版本
make version

# 只在本地完成 patch 发版准备
make tag-release-patch

# 本地准备后顺序推送分支和 tag
make publish-release-patch
```

常用命令：

- `make build` - 清理旧产物后重新生成 wheel 和 sdist，并执行 `twine check`
- `make metadata` - 查看最新 wheel 中的 `METADATA`
- `make pkg-info` - 查看最新 sdist 中的 `PKG-INFO`
- `make install-wheel` - 使用 `uv pip install --reinstall` 安装最新 wheel，避免同版本安装时残留旧元数据
- `make tag-release-patch` - 顺序执行 bump、build、test、commit 和 tag，但不推送
- `make publish-release-patch` - 在 `tag-release-patch` 基础上继续 push 当前分支和版本 tag

如果你刚改了 `README.md`、依赖或项目描述，建议至少跑一遍：

```bash
make build
make metadata
```

如果你要正式发版，推荐直接走 `publish-release-*`，这样 `commit`、`tag` 和 `push` 都在同一个顺序化流程里，不容易再出现 tag 指到错误提交的问题。

## 快速开始

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


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


# 2. 定义工具执行函数
async def read(params: ReadParams, 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",
    description="读取文件内容",
    parameters=ReadParams,
    execute=read
)

# 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)
```

模型接入仅使用 OpenAI-compatible Chat Completions 接口，配置为 `base_url`、`api_key` 和 `model`：

```python
from yier_agents import LLM

llm = LLM(
    base_url="https://gateway.example/v1",
    api_key="your-key",
    model="your-model-id",
)

# 或显式从环境变量加载：
llm = LLM.from_env()
```

`from_env()` 读取 `YIER_BASE_URL`、`YIER_API_KEY`、`YIER_MODEL`，显式参数优先于环境值。模型 ID 原样发送，不做厂商推断或前缀转换。使用完调用 `await llm.aclose()`，或使用 `async with LLM(...)`。

## 调试模式

使用 `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({"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.from_env(),
    tools=[],
    skill_catalog=catalog,
    system_prompt="你是一个支持 lazy skills 的编程助手",
)
```

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

内置文件/命令工具:

交互式 CLI 默认使用兼容 pi coding-agent 的短工具名，模型看到的是 `read`、`bash`、`edit`、`write`、`grep`、`find` 和 `ls`：

- `read` - 读取文件内容并返回行号
- `bash` - 在工作区执行 shell 命令
- `edit` - 对文件做精确文本替换
- `write` - 创建或更新文件
- `grep` - 按文本或正则搜索文件内容，支持 glob、上下文行和大小写选项
- `find` - 按 glob 查找文件和目录
- `ls` - 浏览目录内容

Python API 使用同一套命名：`create_read_tool`、`create_bash_tool`、`create_edit_tool`、`create_write_tool`、`create_grep_tool`、`create_find_tool`、`create_ls_tool`。旧工厂和旧工具单例已移除，迁移对照见 [CHANGELOG](CHANGELOG.md)。

```python
from pathlib import Path
from yier_agents import Agent, create_read_tool, create_grep_tool, create_find_tool

root = Path.cwd()
agent = Agent(
    llm=llm,
    tools=[factory([root], default_root=root) for factory in (
        create_read_tool, create_grep_tool, create_find_tool,
    )],
)
```

## ToolOutput 结构

所有内置工具都会返回同一个 `ToolOutput` 结构:

- `content`: 给模型和 CLI 看的可读摘要
- `metadata`: 轻量摘要字段,适合列表、状态栏和简单 UI 绑定
- `raw`: 给 UI 和外部调用方看的结构化原始载荷

推荐的消费顺序是:

1. 用 `raw` 驱动 UI 展示和交互
2. 用 `metadata` 做轻量状态判断或列表摘要
3. 把 `content` 当作日志、回退展示或给模型继续阅读的文本

```python
result = await tool.execute(params, ctx)

print(result.content)
print(result.metadata)
print(result.raw)
```

`raw.kind` 是最重要的分流字段。当前内置工具大致会返回这些类型:

- `shell_command`: `bash`
- `terminal`: `terminal`（窗口状态、增量输出、屏幕和等待结果）
- `file_read`: `read`
- `file_list`: `ls`
- `file_write`: `write`
- `grep`: `grep`
- `find`: `find`
- `file_replace`: `edit`
- `todo_list`: `todo_read`
- `todo_update`: `todo_write`
- `skill_load`: `skill_load`
- `mcp_tool_result`: 通过 `MCPClient` 代理的 MCP 工具
- `subagent_result`: `AgentTool`
- `queued_followup`: `queue_terminal_followup`
- `network_lookup`: `get_ip`

### Shell / Terminal

`bash` 返回 `raw.kind="shell_command"`，包含 `request`、`process`、`events` 和 `streams`。运行期间通过 `ToolCallUpdateEvent` 实时推送 stdout/stderr；支持取消、可选超时与进程组清理，CLI 可用 Ctrl-C 停止当前运行。输出截断保留尾部，完整日志路径见 `full_output_path`。默认无超时，非零退出和超时会标为工具错误。完整接入见 [Bash](docs/BASH.md)。

`terminal` 返回 `raw.kind="terminal"`。窗口操作返回 `window`、`name`、`running`、`state`、`exit_code`、尺寸和标题；读取另有 `output`、绝对字符游标 `offset`、丢弃字符数 `skipped`、`truncated`、`waited` 和 `timed_out`。PTY 将 stdout/stderr 合并为同一个有序输出流。

完整使用说明与迁移见 [Terminal](docs/TERMINAL.md)。

### File / Todo / Skill / MCP

其他工具也在 `raw` 中提供结构化结果：文件读取的 `lines`、目录列表的 `entries`、`grep` 的 `matches`、`find` 的 `paths`、Todo 的 `todos`、Skill 的 `sampled_files`、MCP 的 `contents` 和子 Agent 的 `final_message`。宿主 UI 可以直接使用这些字段。

## 会话消息调度

库提供 `SessionController`：`steer()` 在当前模型请求及工具批次完成后注入，不打断工具；`queue()` 在当前整轮完成后依次启动新回合。支持消息 ID、状态事件、撤回、清空和取消后的显式恢复。CLI 运行中普通输入默认 steer，`/queue <消息>` 排下一轮，`/messages` 查看，`/withdraw <id>` 撤回，`/resume` 恢复。终端完成触发改用 `/after <窗口> <消息>`。

宿主示例、边界及异常语义见 [SESSION_CONTROL.md](docs/SESSION_CONTROL.md)。

## Session Memory

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

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

agent = Agent(
    llm=LLM.from_env(),
    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
```

CLI 读取 `.env` 和 `YIER_BASE_URL`、`YIER_API_KEY`、`YIER_MODEL`。交互式启动时仅询问缺失的地址、密钥或模型 ID；非交互模式缺少配置会立即报错。

这个入口默认启用：

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

常用交互命令：

- `/skills [query]` - 查看或筛选已发现的 skills
- `/ps [running|all]` - 查看终端窗口
- `/tail <session_id>` - 查看终端窗口最近输出
- `/wait <session_id> [seconds]` - 等待终端退出一小段时间
- `/stop <session_id>` - 关闭终端窗口
- `/after [list|<session_id> <message>]` - 给终端挂一个完成后自动执行的 follow-up

一个典型流程是：

1. 让 Agent 调用 `terminal(command="new-window", shell_command="npm run dev", name="dev")`。
2. 通过 `terminal` 的 `read`、`send-keys`、`capture-pane` 和 `kill-window` 继续控制窗口。
3. 用 `/after 1 总结结果并告诉我下一步`，或者让 Agent 调用 `queue_terminal_followup`。
4. 终端退出后，CLI 将完成通知和输出摘要注入下一次 Agent 回合，并执行已排队的 follow-up。

也可以直接操作：`/terminal new-window -n dev 'npm run dev'`、`/terminal send-keys -t dev C-c`、`/terminal capture-pane -t dev`。`/ps`、`/tail`、`/wait` 和 `/stop` 继续作为终端窗口的快捷命令。

可以在 `.yier.json` 里为 CLI 覆盖这些默认值。默认行为和现在一致，只是搬进了配置层：

```json
{
  "assistant": {
    "workspaceRoot": ".",
    "sessionStoragePath": ".agent_storage/sessions",
    "promptHistoryPath": ".agent_storage/prompt_history.txt",
    "allowedRoots": [
      ".",
      ".agent_storage/sessions"
    ],
    "includeSkillDirectoriesInAllowedRoots": true,
    "maxIterations": 100,
    "runCommand": {
      "allowShell": false,
      "shellProgram": "/bin/bash"
    },
    "compaction": {
      "enabled": true,
      "contextWindow": 32768,
      "reserveTokens": 4096,
      "keepRecentTokens": 8192,
      "summaryMaxTokens": 1024
    }
  }
}
```

`create_bash_tool()` 默认允许 shell 语法；CLI 仍遵循配置，`runCommand.allowShell` 默认为 `false`，此时 `bash` 不支持 `|`、重定向和链式 shell 语法；这是为了避免把普通命令执行提升成完整 shell。只有在你明确开启它时，才会用 shell 模式执行。

`shellProgram` 是可选覆盖项：类 Unix 系统默认使用 `/bin/bash`，Windows 默认使用当前系统的 `ComSpec`。`terminal` 使用相同的 `shellProgram`，通过 PTY 始终运行真实 shell，独立于 `runCommand.allowShell`。终端支持 Linux、macOS 和 WSL；Windows 原生暂不提供 ConPTY 后端。

## MCP 支持

> 使用前请先安装可选依赖：`pip install "yier_agents[mcp]"`

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

基于 mcp 2.x SDK:自动协商协议版本——server 支持 2026-07-28 新协议时走 `server/discover` 无握手连接,否则回退到传统 `initialize` 握手,stdio / SSE / Streamable HTTP 三种传输都兼容。

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

llm = LLM.from_env()

# 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
