AbdulElahGwaith commited on
Commit
dc893fb
·
verified ·
1 Parent(s): e195705

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +28 -0
  2. .gitignore +62 -0
  3. .gitmodules +3 -0
  4. CODE_OF_CONDUCT.md +45 -0
  5. CODE_OF_CONDUCT_CN.md +48 -0
  6. CONTRIBUTING.md +200 -0
  7. CONTRIBUTING_CN.md +201 -0
  8. LICENSE +22 -0
  9. MANIFEST.in +5 -0
  10. README.md +344 -0
  11. README_CN.md +345 -0
  12. docs/DEVELOPMENT_GUIDE.md +417 -0
  13. docs/DEVELOPMENT_GUIDE_CN.md +420 -0
  14. docs/PRODUCTION_GUIDE.md +162 -0
  15. docs/PRODUCTION_GUIDE_CN.md +161 -0
  16. docs/assets/demo1-task-execution.gif +3 -0
  17. docs/assets/demo2-claude-skill.gif +3 -0
  18. docs/assets/demo3-web-search.gif +3 -0
  19. examples/01_basic_tools.py +137 -0
  20. examples/02_simple_agent.py +212 -0
  21. examples/03_session_notes.py +250 -0
  22. examples/04_full_agent.py +276 -0
  23. examples/05_provider_selection.py +190 -0
  24. examples/06_tool_schema_demo.py +285 -0
  25. examples/README.md +219 -0
  26. examples/README_CN.md +219 -0
  27. mini_agent/__init__.py +17 -0
  28. mini_agent/acp/__init__.py +197 -0
  29. mini_agent/acp/server.py +6 -0
  30. mini_agent/agent.py +523 -0
  31. mini_agent/cli.py +834 -0
  32. mini_agent/config.py +220 -0
  33. mini_agent/config/config-example.yaml +60 -0
  34. mini_agent/config/mcp-example.json +29 -0
  35. mini_agent/config/system_prompt.md +75 -0
  36. mini_agent/llm/__init__.py +9 -0
  37. mini_agent/llm/anthropic_client.py +293 -0
  38. mini_agent/llm/base.py +84 -0
  39. mini_agent/llm/llm_wrapper.py +127 -0
  40. mini_agent/llm/openai_client.py +295 -0
  41. mini_agent/logger.py +178 -0
  42. mini_agent/retry.py +138 -0
  43. mini_agent/schema/__init__.py +19 -0
  44. mini_agent/schema/schema.py +55 -0
  45. mini_agent/skills/.claude-plugin/marketplace.json +43 -0
  46. mini_agent/skills/.gitignore +2 -0
  47. mini_agent/skills/README.md +123 -0
  48. mini_agent/skills/THIRD_PARTY_NOTICES.md +405 -0
  49. mini_agent/skills/agent_skills_spec.md +55 -0
  50. mini_agent/skills/algorithmic-art/LICENSE.txt +202 -0
.gitattributes CHANGED
@@ -33,3 +33,31 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ docs/assets/demo1-task-execution.gif filter=lfs diff=lfs merge=lfs -text
37
+ docs/assets/demo2-claude-skill.gif filter=lfs diff=lfs merge=lfs -text
38
+ docs/assets/demo3-web-search.gif filter=lfs diff=lfs merge=lfs -text
39
+ mini_agent/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf filter=lfs diff=lfs merge=lfs -text
40
+ mini_agent/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf filter=lfs diff=lfs merge=lfs -text
41
+ mini_agent/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf filter=lfs diff=lfs merge=lfs -text
42
+ mini_agent/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf filter=lfs diff=lfs merge=lfs -text
43
+ mini_agent/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf filter=lfs diff=lfs merge=lfs -text
44
+ mini_agent/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf filter=lfs diff=lfs merge=lfs -text
45
+ mini_agent/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf filter=lfs diff=lfs merge=lfs -text
46
+ mini_agent/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf filter=lfs diff=lfs merge=lfs -text
47
+ mini_agent/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf filter=lfs diff=lfs merge=lfs -text
48
+ mini_agent/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf filter=lfs diff=lfs merge=lfs -text
49
+ mini_agent/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf filter=lfs diff=lfs merge=lfs -text
50
+ mini_agent/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf filter=lfs diff=lfs merge=lfs -text
51
+ mini_agent/skills/canvas-design/canvas-fonts/Jura-Light.ttf filter=lfs diff=lfs merge=lfs -text
52
+ mini_agent/skills/canvas-design/canvas-fonts/Jura-Medium.ttf filter=lfs diff=lfs merge=lfs -text
53
+ mini_agent/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf filter=lfs diff=lfs merge=lfs -text
54
+ mini_agent/skills/canvas-design/canvas-fonts/Lora-Bold.ttf filter=lfs diff=lfs merge=lfs -text
55
+ mini_agent/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf filter=lfs diff=lfs merge=lfs -text
56
+ mini_agent/skills/canvas-design/canvas-fonts/Lora-Italic.ttf filter=lfs diff=lfs merge=lfs -text
57
+ mini_agent/skills/canvas-design/canvas-fonts/Lora-Regular.ttf filter=lfs diff=lfs merge=lfs -text
58
+ mini_agent/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf filter=lfs diff=lfs merge=lfs -text
59
+ mini_agent/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf filter=lfs diff=lfs merge=lfs -text
60
+ mini_agent/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf filter=lfs diff=lfs merge=lfs -text
61
+ mini_agent/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf filter=lfs diff=lfs merge=lfs -text
62
+ mini_agent/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf filter=lfs diff=lfs merge=lfs -text
63
+ mini_agent/skills/theme-factory/theme-showcase.pdf filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ venv/
25
+ ENV/
26
+ env/
27
+ .venv
28
+
29
+ # IDE
30
+ .vscode/
31
+ .idea/
32
+ *.swp
33
+ *.swo
34
+ *~
35
+
36
+ # Workspace
37
+ workspace/
38
+ *.log
39
+
40
+ # OS
41
+ .DS_Store
42
+ Thumbs.db
43
+
44
+ # Config (contains API keys)
45
+ config.yaml
46
+ mcp.json
47
+
48
+ # test file
49
+ demo.py
50
+ hello.py
51
+ test.txt
52
+ hello_world.py
53
+ demo_test.txt
54
+ demo_commands.txt
55
+ mcp.json.bak
56
+
57
+ docs/assets/backup/
58
+ docs/assets/preview.html
59
+
60
+ claude.md
61
+
62
+ .agent_memory.json
.gitmodules ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ [submodule "mini_agent/skills"]
2
+ path = mini_agent/skills
3
+ url = https://github.com/anthropics/skills.git
CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ ## Our Standards
8
+
9
+ Examples of behavior that contributes to creating a positive environment include:
10
+
11
+ - Using welcoming and inclusive language
12
+ - Being respectful of differing viewpoints and experiences
13
+ - Gracefully accepting constructive criticism
14
+ - Focusing on what is best for the community
15
+ - Showing empathy towards other community members
16
+
17
+ Examples of unacceptable behavior by participants include:
18
+
19
+ - The use of sexualized language or imagery and unwelcome sexual attention or advances
20
+ - Trolling, insulting/derogatory comments, and personal or political attacks
21
+ - Public or private harassment
22
+ - Publishing others' private information, such as a physical or electronic address, without explicit permission
23
+ - Other conduct which could reasonably be considered inappropriate in a professional setting
24
+
25
+ ## Our Responsibilities
26
+
27
+ Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28
+
29
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30
+
31
+ ## Scope
32
+
33
+ This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34
+
35
+ ## Enforcement
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38
+
39
+ Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40
+
41
+ ## Attribution
42
+
43
+ This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
44
+
45
+ For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
CODE_OF_CONDUCT_CN.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 行为准则
2
+
3
+ ## 我们的承诺
4
+
5
+ 为了营造开放和友好的环境,我们作为贡献者和维护者承诺:无论年龄、体型、残疾、种族、性别认同和表达、经验水平、教育程度、社会经济地位、国籍、外貌、种族、宗教或性取向如何,参与我们的项目和社区的每个人都不会受到骚扰。
6
+
7
+ ## 我们的标准
8
+
9
+ 有助于创建积极环境的行为包括:
10
+
11
+ - 使用友好和包容的语言
12
+ - 尊重不同的观点和经验
13
+ - 优雅地接受建设性批评
14
+ - 专注于对社区最有利的事情
15
+ - 对其他社区成员表示同理心
16
+
17
+ 不可接受的行为包括:
18
+
19
+ - 使用性暗示的语言或图像,以及不受欢迎的性关注或挑逗
20
+ - 发表侮辱性/贬损性评论,进行人身攻击或政治攻击
21
+ - 公开或私下骚扰
22
+ - 未经明确许可,发布他人的私人信息(如地址、电子邮件地址)
23
+ - 在专业环境中可被合理认为不适当的其他行为
24
+
25
+ ## 我们的责任
26
+
27
+ 项目维护者有责任澄清可接受行为的标准,并应对任何不可接受的行为采取适当和公平的纠正措施。
28
+
29
+ 项目维护者有权利和责任删除、编辑或拒绝不符合本行为准则的评论、提交、代码、wiki 编辑、问题和其他贡献,或暂时或永久禁止任何他们认为有不适当、威胁、冒犯或有害行为的贡献者。
30
+
31
+ ## 范围
32
+
33
+ 本行为准则适用于项目空间和公共空间,当个人代表项目或其社区时。代表项目或社区的示例包括:使用官方项目电子邮件地址、通过官方社交媒体账户发帖,或在在线或离线活动中担任指定代表。项目维护者可以进一步定义和阐明项目的代表性。
34
+
35
+ ## 执行
36
+
37
+ 可以通过联系项目团队来报告滥用、骚扰或其他不可接受的行为。所有投诉都将被审查和调查,并将做出被认为必要和适当的回应。项目团队有义务对事件报告者保密。具体执行政策的更多细节可能会单独发布。
38
+
39
+ 不善意遵守或执行行为准则的项目维护者可能会面临项目领导层其他成员决定的临时或永久性后果。
40
+
41
+ ## 归属
42
+
43
+ 本行为准则改编自 [Contributor Covenant](https://www.contributor-covenant.org/) 2.0 版本,可在 https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 获取。
44
+
45
+ 社区影响指南受到 [Mozilla 的行为准则执行阶梯](https://github.com/mozilla/diversity)的启发。
46
+
47
+ 有关本行为准则的常见问题的答案,请参阅 https://www.contributor-covenant.org/faq。翻译版本可在 https://www.contributor-covenant.org/translations 获取。
48
+
CONTRIBUTING.md ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing Guide
2
+
3
+ Thank you for your interest in the Mini Agent project! We welcome contributions of all forms.
4
+
5
+ ## How to Contribute
6
+
7
+ ### Reporting Bugs
8
+
9
+ If you find a bug, please create an Issue and include the following information:
10
+
11
+ - **Problem Description**: A clear description of the problem.
12
+ - **Steps to Reproduce**: Detailed steps to reproduce the issue.
13
+ - **Expected Behavior**: What you expected to happen.
14
+ - **Actual Behavior**: What actually happened.
15
+ - **Environment Information**:
16
+ - Python version
17
+ - Operating system
18
+ - Versions of relevant dependencies
19
+
20
+ ### Suggesting New Features
21
+
22
+ If you have an idea for a new feature, please create an Issue first to discuss it:
23
+
24
+ - Describe the purpose and value of the feature.
25
+ - Explain the intended use case.
26
+ - Provide a design proposal if possible.
27
+
28
+ ### Submitting Code
29
+
30
+ #### Getting Started
31
+
32
+ 1. Fork this repository.
33
+ 2. Clone your fork:
34
+ ```bash
35
+ git clone https://github.com/MiniMax-AI/Mini-Agent mini-agent
36
+ cd mini-agent
37
+ ```
38
+
39
+ 3. Create a new branch:
40
+ ```bash
41
+ git checkout -b feature/your-feature-name
42
+ # or
43
+ git checkout -b fix/your-bug-fix
44
+ ```
45
+
46
+ 4. Install development dependencies:
47
+ ```bash
48
+ uv sync
49
+ ```
50
+
51
+ #### Development Process
52
+
53
+ 1. **Write Code**
54
+ - Follow the project's code style (see the [Development Guide](docs/DEVELOPMENT.md#code-style-guide)).
55
+ - Add necessary comments and docstrings.
56
+ - Keep your code clean and concise.
57
+
58
+ 2. **Add Tests**
59
+ - Add test cases for new features.
60
+ - Ensure all tests pass:
61
+ ```bash
62
+ pytest tests/ -v
63
+ ```
64
+
65
+ 3. **Update Documentation**
66
+ - If you add a new feature, update the README or relevant documentation.
67
+ - Keep documentation in sync with your code.
68
+
69
+ 4. **Commit Changes**
70
+ - Use clear commit messages:
71
+ ```bash
72
+ git commit -m "feat(tools): Add new file search tool"
73
+ # or
74
+ git commit -m "fix(agent): Fix error handling for tool calls"
75
+ ```
76
+
77
+ - Commit message format:
78
+ - `feat`: A new feature
79
+ - `fix`: A bug fix
80
+ - `docs`: Documentation updates
81
+ - `style`: Code style adjustments
82
+ - `refactor`: Code refactoring
83
+ - `test`: Test-related changes
84
+ - `chore`: Build or auxiliary tools
85
+
86
+ 5. **Push to Your Fork**
87
+ ```bash
88
+ git push origin feature/your-feature-name
89
+ ```
90
+
91
+ 6. **Create a Pull Request**
92
+ - Create a Pull Request on GitHub.
93
+ - Clearly describe your changes.
94
+ - Reference any related Issues if applicable.
95
+
96
+ #### Pull Request Checklist
97
+
98
+ Before submitting a PR, please ensure:
99
+
100
+ - [ ] The code follows the project's style guide.
101
+ - [ ] All tests pass.
102
+ - [ ] Necessary tests have been added.
103
+ - [ ] Relevant documentation has been updated.
104
+ - [ ] The commit message is clear and concise.
105
+ - [ ] There are no unrelated changes.
106
+
107
+ ### Code Review
108
+
109
+ All Pull Requests will be reviewed:
110
+
111
+ - We will review your code as soon as possible.
112
+ - We may request some changes.
113
+ - Please be patient and responsive to feedback.
114
+ - Once approved, your PR will be merged into the main branch.
115
+
116
+ ## Code Style Guide
117
+
118
+ ### Python Code Style
119
+
120
+ Follow PEP 8 and the Google Python Style Guide:
121
+
122
+ ```python
123
+ # Good example ✅
124
+ class MyClass:
125
+ """A brief description of the class.
126
+
127
+ A more detailed description...
128
+ """
129
+
130
+ def my_method(self, param1: str, param2: int = 10) -> str:
131
+ """A brief description of the method.
132
+
133
+ Args:
134
+ param1: Description of parameter 1.
135
+ param2: Description of parameter 2.
136
+
137
+ Returns:
138
+ Description of the return value.
139
+ """
140
+ pass
141
+
142
+ # Bad example ❌
143
+ class myclass: # Class names should be PascalCase
144
+ def MyMethod(self,param1,param2=10): # Method names should be snake_case
145
+ pass # Missing docstring
146
+ ```
147
+
148
+ ### Type Hinting
149
+
150
+ Use Python type hints:
151
+
152
+ ```python
153
+ from typing import List, Dict, Optional, Any
154
+
155
+ async def process_messages(
156
+ messages: List[Dict[str, Any]],
157
+ max_tokens: Optional[int] = None
158
+ ) -> str:
159
+ """Process a list of messages."""
160
+ pass
161
+ ```
162
+
163
+ ### Testing
164
+
165
+ - Write tests for new features.
166
+ - Keep tests simple and clear.
167
+ - Ensure tests cover critical paths.
168
+
169
+ ```python
170
+ import pytest
171
+ from mini_agent.tools.my_tool import MyTool
172
+
173
+ @pytest.mark.asyncio
174
+ async def test_my_tool():
175
+ """Test the custom tool."""
176
+ tool = MyTool()
177
+ result = await tool.execute(param="test")
178
+ assert result.success
179
+ assert "expected" in result.content
180
+ ```
181
+
182
+ ## Community Guidelines
183
+
184
+ Please follow our [Code of Conduct](CODE_OF_CONDUCT.md) and be friendly and respectful.
185
+
186
+ ## Questions and Help
187
+
188
+ If you have any questions:
189
+
190
+ - Check the [README](README.md) and [documentation](docs/).
191
+ - Search existing Issues.
192
+ - Create a new Issue to ask a question.
193
+
194
+ ## License
195
+
196
+ By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).
197
+
198
+ ---
199
+
200
+ Thank you again for your contribution! 🎉
CONTRIBUTING_CN.md ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 贡献指南
2
+
3
+ 感谢你对 Mini Agent 项目的兴趣!我们欢迎各种形式的贡献。
4
+
5
+ ## 如何贡献
6
+
7
+ ### 报告 Bug
8
+
9
+ 如果你发现了 bug,请创建一个 Issue 并包含以下信息:
10
+
11
+ - **问题描述**:清晰描述问题
12
+ - **复现步骤**:详细的复现步骤
13
+ - **预期行为**:你期望发生什么
14
+ - **实际行为**:实际发生了什么
15
+ - **环境信息**:
16
+ - Python 版本
17
+ - 操作系统
18
+ - 相关依赖版本
19
+
20
+ ### 提出新功能
21
+
22
+ 如果你有新功能的想法,请先创建一个 Issue 讨论:
23
+
24
+ - 描述功能的用途和价值
25
+ - 说明预期的使用场景
26
+ - 如果可能,提供设计思路
27
+
28
+ ### 提交代码
29
+
30
+ #### 准备工作
31
+
32
+ 1. Fork 本仓库
33
+ 2. 克隆你的 fork:
34
+ ```bash
35
+ git clone https://github.com/MiniMax-AI/Mini-Agent mini-agent
36
+ cd mini-agent
37
+ ```
38
+
39
+ 3. 创建新分支:
40
+ ```bash
41
+ git checkout -b feature/your-feature-name
42
+ # 或
43
+ git checkout -b fix/your-bug-fix
44
+ ```
45
+
46
+ 4. 安装开发依赖:
47
+ ```bash
48
+ uv sync
49
+ ```
50
+
51
+ #### 开发流程
52
+
53
+ 1. **编写代码**
54
+ - 遵循项目的代码风格(参考 [开发指南](docs/DEVELOPMENT.md#代码规范))
55
+ - 添加必要的注释和文档字符串
56
+ - 保持代码简洁清晰
57
+
58
+ 2. **添加测试**
59
+ - 为新功能添加测试用例
60
+ - 确保所有测试通过:
61
+ ```bash
62
+ pytest tests/ -v
63
+ ```
64
+
65
+ 3. **更新文档**
66
+ - 如果添加了新功能,更新 README 或相关文档
67
+ - 保持文档与代码同步
68
+
69
+ 4. **提交更改**
70
+ - 使用清晰的提交消息:
71
+ ```bash
72
+ git commit -m "feat(tools): 添加新的文件搜索工具"
73
+ # 或
74
+ git commit -m "fix(agent): 修复工具调用错误处理"
75
+ ```
76
+
77
+ - 提交消息格式:
78
+ - `feat`: 新功能
79
+ - `fix`: Bug 修复
80
+ - `docs`: 文档更新
81
+ - `style`: 代码格式调整
82
+ - `refactor`: 代码重构
83
+ - `test`: 测试相关
84
+ - `chore`: 构建或辅助工具
85
+
86
+ 5. **推送到你的 fork**
87
+ ```bash
88
+ git push origin feature/your-feature-name
89
+ ```
90
+
91
+ 6. **创建 Pull Request**
92
+ - 在 GitHub 上创建 Pull Request
93
+ - 清楚描述你的更改
94
+ - 引用相关的 Issue(如果有)
95
+
96
+ #### Pull Request 检查清单
97
+
98
+ 在提交 PR 之前,请确保:
99
+
100
+ - [ ] 代码遵循项目规范
101
+ - [ ] 所有测试通过
102
+ - [ ] 添加了必要的测试
103
+ - [ ] 更新了相关文档
104
+ - [ ] 提交消息清晰明确
105
+ - [ ] 没有不相关的更改
106
+
107
+ ### 代码审查
108
+
109
+ 所有 Pull Request 需要经过代码审查:
110
+
111
+ - 我们会尽快审查你的代码
112
+ - 可能会要求一些修改
113
+ - 请保持耐心并及时响应反馈
114
+ - 审查通过后会被合并到主分支
115
+
116
+ ## 代码规范
117
+
118
+ ### Python 代码风格
119
+
120
+ 遵循 PEP 8 和 Google Python Style Guide:
121
+
122
+ ```python
123
+ # 好的示例 ✅
124
+ class MyClass:
125
+ """类的简短描述。
126
+
127
+ 详细描述...
128
+ """
129
+
130
+ def my_method(self, param1: str, param2: int = 10) -> str:
131
+ """方法的简短描述。
132
+
133
+ Args:
134
+ param1: 参数1的描述
135
+ param2: 参数2的描述
136
+
137
+ Returns:
138
+ 返回值的描述
139
+ """
140
+ pass
141
+
142
+ # 不好的示例 ❌
143
+ class myclass: # 类名应该用 PascalCase
144
+ def MyMethod(self,param1,param2=10): # 方法名应该用 snake_case
145
+ pass # 缺少 docstring
146
+ ```
147
+
148
+ ### 类型注解
149
+
150
+ 使用 Python 类型注解:
151
+
152
+ ```python
153
+ from typing import List, Dict, Optional
154
+
155
+ async def process_messages(
156
+ messages: List[Dict[str, Any]],
157
+ max_tokens: Optional[int] = None
158
+ ) -> str:
159
+ """处理消息列表"""
160
+ pass
161
+ ```
162
+
163
+ ### 测试
164
+
165
+ - 为新功能编写测试
166
+ - 保持测试简单清晰
167
+ - 测试覆盖关键路径
168
+
169
+ ```python
170
+ import pytest
171
+ from mini_agent.tools.my_tool import MyTool
172
+
173
+ @pytest.mark.asyncio
174
+ async def test_my_tool():
175
+ """测试自定义工具"""
176
+ tool = MyTool()
177
+ result = await tool.execute(param="test")
178
+ assert result.success
179
+ assert "expected" in result.content
180
+ ```
181
+
182
+ ## 社区准则
183
+
184
+ 请遵守我们的[行为准则](CODE_OF_CONDUCT.md),保持友好和尊重。
185
+
186
+ ## 问题和帮助
187
+
188
+ 如果有任何问题:
189
+
190
+ - 查看 [README](README.md) 和 [文档](docs/)
191
+ - 搜索现有的 Issues
192
+ - 创建新的 Issue 提问
193
+
194
+ ## 许可证
195
+
196
+ 提交代码即表示你同意将代码以 [MIT License](LICENSE) 发布。
197
+
198
+ ---
199
+
200
+ 再次感谢你的贡献! 🎉
201
+
LICENSE ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 MiniMax
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
MANIFEST.in ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ recursive-include mini_agent/skills *
2
+ include mini_agent/skills/README.md
3
+ include mini_agent/skills/THIRD_PARTY_NOTICES.md
4
+ include mini_agent/skills/agent_skills_spec.md
5
+
README.md ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mini Agent
2
+
3
+ English | [中文](./README_CN.md)
4
+
5
+ **Mini Agent** is a minimal yet professional demo project that showcases the best practices for building agents with the MiniMax M2.1 model. Leveraging an Anthropic-compatible API, it fully supports interleaved thinking to unlock M2's powerful reasoning capabilities for long, complex tasks.
6
+
7
+ This project comes packed with features designed for a robust and intelligent agent development experience:
8
+
9
+ * ✅ **Full Agent Execution Loop**: A complete and reliable foundation with a basic toolset for file system and shell operations.
10
+ * ✅ **Persistent Memory**: An active **Session Note Tool** ensures the agent retains key information across multiple sessions.
11
+ * ✅ **Intelligent Context Management**: Automatically summarizes conversation history to handle contexts up to a configurable token limit, enabling infinitely long tasks.
12
+ * ✅ **Claude Skills Integration**: Comes with 15 professional skills for documents, design, testing, and development.
13
+ * ✅ **MCP Tool Integration**: Natively supports MCP for tools like knowledge graph access and web search.
14
+ * ✅ **Comprehensive Logging**: Detailed logs for every request, response, and tool execution for easy debugging.
15
+ * ✅ **Clean & Simple Design**: A beautiful CLI and a codebase that is easy to understand, making it the perfect starting point for building advanced agents.
16
+
17
+ ## Table of Contents
18
+
19
+ - [Mini Agent](#mini-agent)
20
+ - [Table of Contents](#table-of-contents)
21
+ - [Quick Start](#quick-start)
22
+ - [1. Get API Key](#1-get-api-key)
23
+ - [2. Choose Your Usage Mode](#2-choose-your-usage-mode)
24
+ - [🚀 Quick Start Mode (Recommended for Beginners)](#-quick-start-mode-recommended-for-beginners)
25
+ - [🔧 Development Mode](#-development-mode)
26
+ - [ACP \& Zed Editor Integration(optional)](#acp--zed-editor-integrationoptional)
27
+ - [Usage Examples](#usage-examples)
28
+ - [Task Execution](#task-execution)
29
+ - [Using a Claude Skill (e.g., PDF Generation)](#using-a-claude-skill-eg-pdf-generation)
30
+ - [Web Search \& Summarization (MCP Tool)](#web-search--summarization-mcp-tool)
31
+ - [Testing](#testing)
32
+ - [Quick Run](#quick-run)
33
+ - [Test Coverage](#test-coverage)
34
+ - [Troubleshooting](#troubleshooting)
35
+ - [SSL Certificate Error](#ssl-certificate-error)
36
+ - [Module Not Found Error](#module-not-found-error)
37
+ - [Related Documentation](#related-documentation)
38
+ - [Community](#community)
39
+ - [Contributing](#contributing)
40
+ - [License](#license)
41
+ - [References](#references)
42
+
43
+ ## Quick Start
44
+
45
+ ### 1. Get API Key
46
+
47
+ MiniMax provides both global and China platforms. Choose based on your network environment:
48
+
49
+ | Version | Platform | API Base |
50
+ | ---------- | -------------------------------------------------------------- | -------------------------- |
51
+ | **Global** | [https://platform.minimax.io](https://platform.minimax.io) | `https://api.minimax.io` |
52
+ | **China** | [https://platform.minimaxi.com](https://platform.minimaxi.com) | `https://api.minimaxi.com` |
53
+
54
+ **Steps to get API Key:**
55
+ 1. Visit the corresponding platform to register and login
56
+ 2. Go to **Account Management > API Keys**
57
+ 3. Click **"Create New Key"**
58
+ 4. Copy and save it securely (key is only shown once)
59
+
60
+ > 💡 **Tip**: Remember the API Base address corresponding to your chosen platform, you'll need it for configuration
61
+
62
+ ### 2. Choose Your Usage Mode
63
+
64
+ **Prerequisites: Install uv**
65
+
66
+ Both usage modes require uv. If you don't have it installed:
67
+
68
+ ```bash
69
+ # macOS/Linux/WSL
70
+ curl -LsSf https://astral.sh/uv/install.sh | sh
71
+
72
+ # Windows (PowerShell)
73
+ python -m pip install --user pipx
74
+ python -m pipx ensurepath
75
+ # Restart PowerShell after installation
76
+
77
+ # After installation, restart your terminal or run:
78
+ source ~/.bashrc # or ~/.zshrc (macOS/Linux)
79
+ ```
80
+
81
+ We offer two usage modes - choose based on your needs:
82
+
83
+ #### 🚀 Quick Start Mode (Recommended for Beginners)
84
+
85
+ Perfect for users who want to quickly try Mini Agent without cloning the repository or modifying code.
86
+
87
+ **Installation:**
88
+
89
+ ```bash
90
+ # 1. Install directly from GitHub
91
+ uv tool install git+https://github.com/MiniMax-AI/Mini-Agent.git
92
+
93
+ # 2. Run setup script (automatically creates config files)
94
+ # macOS/Linux:
95
+ curl -fsSL https://raw.githubusercontent.com/MiniMax-AI/Mini-Agent/main/scripts/setup-config.sh | bash
96
+
97
+ # Windows (PowerShell):
98
+ Invoke-WebRequest -Uri "https://raw.githubusercontent.com/MiniMax-AI/Mini-Agent/main/scripts/setup-config.ps1" -OutFile "$env:TEMP\setup-config.ps1"
99
+ powershell -ExecutionPolicy Bypass -File "$env:TEMP\setup-config.ps1"
100
+ ```
101
+
102
+ > 💡 **Tip**: If you want to develop locally or modify code, use "Development Mode" below
103
+
104
+ **Configuration:**
105
+
106
+ The setup script creates config files in `~/.mini-agent/config/`. Edit the config file:
107
+
108
+ ```bash
109
+ # Edit config file
110
+ nano ~/.mini-agent/config/config.yaml
111
+ ```
112
+
113
+ Fill in your API Key and corresponding API Base:
114
+
115
+ ```yaml
116
+ api_key: "YOUR_API_KEY_HERE" # API Key from step 1
117
+ api_base: "https://api.minimax.io" # Global
118
+ # api_base: "https://api.minimaxi.com" # China
119
+ model: "MiniMax-M2.1"
120
+ ```
121
+
122
+ **Start Using:**
123
+
124
+ ```bash
125
+ mini-agent # Use current directory as workspace
126
+ mini-agent --workspace /path/to/your/project # Specify workspace directory
127
+ mini-agent --version # Check version
128
+
129
+ # Management commands
130
+ uv tool upgrade mini-agent # Upgrade to latest version
131
+ uv tool uninstall mini-agent # Uninstall if needed
132
+ uv tool list # View all installed tools
133
+ ```
134
+
135
+ #### 🔧 Development Mode
136
+
137
+ For developers who need to modify code, add features, or debug.
138
+
139
+ **Installation & Configuration:**
140
+
141
+ ```bash
142
+ # 1. Clone the repository
143
+ git clone https://github.com/MiniMax-AI/Mini-Agent.git
144
+ cd Mini-Agent
145
+
146
+ # 2. Install uv (if you haven't)
147
+ # macOS/Linux:
148
+ curl -LsSf https://astral.sh/uv/install.sh | sh
149
+ # Windows (PowerShell):
150
+ irm https://astral.sh/uv/install.ps1 | iex
151
+ # Restart terminal after installation
152
+
153
+ # 3. Sync dependencies
154
+ uv sync
155
+
156
+ # Alternative: Install dependencies manually (if not using uv)
157
+ # pip install -r requirements.txt
158
+ # Or install required packages:
159
+ # pip install tiktoken pyyaml httpx pydantic requests prompt-toolkit mcp
160
+
161
+ # 4. Initialize Claude Skills (Optional)
162
+ git submodule update --init --recursive
163
+
164
+ # 5. Copy config template
165
+ ```
166
+
167
+ **macOS/Linux:**
168
+ ```bash
169
+ cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml
170
+ ```
171
+
172
+ **Windows:**
173
+ ```powershell
174
+ Copy-Item mini_agent\config\config-example.yaml mini_agent\config\config.yaml
175
+
176
+ # 6. Edit config file
177
+ vim mini_agent/config/config.yaml # Or use your preferred editor
178
+ ```
179
+
180
+ Fill in your API Key and corresponding API Base:
181
+
182
+ ```yaml
183
+ api_key: "YOUR_API_KEY_HERE" # API Key from step 1
184
+ api_base: "https://api.minimax.io" # Global
185
+ # api_base: "https://api.minimaxi.com" # China
186
+ model: "MiniMax-M2.1"
187
+ max_steps: 100
188
+ workspace_dir: "./workspace"
189
+ ```
190
+
191
+ > 📖 Full configuration guide: See [config-example.yaml](mini_agent/config/config-example.yaml)
192
+
193
+ **Run Methods:**
194
+
195
+ Choose your preferred run method:
196
+
197
+ ```bash
198
+ # Method 1: Run as module directly (good for debugging)
199
+ uv run python -m mini_agent.cli
200
+
201
+ # Method 2: Install in editable mode (recommended)
202
+ uv tool install -e .
203
+ # After installation, run from anywhere and code changes take effect immediately
204
+ mini-agent
205
+ mini-agent --workspace /path/to/your/project
206
+ ```
207
+
208
+ > 📖 For more development guidance, see [Development Guide](docs/DEVELOPMENT_GUIDE.md)
209
+
210
+ > 📖 For more production deployment guidance, see [Production Guide](docs/PRODUCTION_GUIDE.md)
211
+
212
+ ## ACP & Zed Editor Integration(optional)
213
+
214
+ Mini Agent supports the [Agent Communication Protocol (ACP)](https://github.com/modelcontextprotocol/protocol) for integration with code editors like Zed.
215
+
216
+ **Setup in Zed Editor:**
217
+
218
+ 1. Install Mini Agent in development mode or as a tool
219
+ 2. Add to your Zed `settings.json`:
220
+
221
+ ```json
222
+ {
223
+ "agent_servers": {
224
+ "mini-agent": {
225
+ "command": "/path/to/mini-agent-acp"
226
+ }
227
+ }
228
+ }
229
+ ```
230
+
231
+ The command path should be:
232
+ - If installed via `uv tool install`: Use the output of `which mini-agent-acp`
233
+ - If in development mode: `./mini_agent/acp/server.py`
234
+
235
+ **Usage:**
236
+ - Open Zed's agent panel with `Ctrl+Shift+P` → "Agent: Toggle Panel"
237
+ - Select "mini-agent" from the agent dropdown
238
+ - Start conversations with Mini Agent directly in your editor
239
+
240
+ ## Usage Examples
241
+
242
+ Here are a few examples of what Mini Agent can do.
243
+
244
+ ### Task Execution
245
+
246
+ *In this demo, the agent is asked to create a simple, beautiful webpage and display it in the browser, showcasing the basic tool-use loop.*
247
+
248
+ ![Demo GIF 1: Basic Task Execution](docs/assets/demo1-task-execution.gif "Basic Task Execution Demo")
249
+
250
+ ### Using a Claude Skill (e.g., PDF Generation)
251
+
252
+ *Here, the agent leverages a Claude Skill to create a professional document (like a PDF or DOCX) based on the user's request, demonstrating its advanced capabilities.*
253
+
254
+ ![Demo GIF 2: Claude Skill Usage](docs/assets/demo2-claude-skill.gif "Claude Skill Usage Demo")
255
+
256
+ ### Web Search & Summarization (MCP Tool)
257
+
258
+ *This demo shows the agent using its web search tool to find up-to-date information online and summarize it for the user.*
259
+
260
+ ![Demo GIF 3: Web Search](docs/assets/demo3-web-search.gif "Web Search Demo")
261
+
262
+ ## Testing
263
+
264
+ The project includes comprehensive test cases covering unit tests, functional tests, and integration tests.
265
+
266
+ ### Quick Run
267
+
268
+ ```bash
269
+ # Run all tests
270
+ pytest tests/ -v
271
+
272
+ # Run core functionality tests
273
+ pytest tests/test_agent.py tests/test_note_tool.py -v
274
+ ```
275
+
276
+ ### Test Coverage
277
+
278
+ - ✅ **Unit Tests** - Tool classes, LLM client
279
+ - ✅ **Functional Tests** - Session Note Tool, MCP loading
280
+ - ✅ **Integration Tests** - Agent end-to-end execution
281
+ - ✅ **External Services** - Git MCP Server loading
282
+
283
+
284
+ ## Troubleshooting
285
+
286
+ ### SSL Certificate Error
287
+
288
+ If you encounter `[SSL: CERTIFICATE_VERIFY_FAILED]` error:
289
+
290
+ **Quick fix for testing** (modify `mini_agent/llm.py`):
291
+ ```python
292
+ # Line 50: Add verify=False to AsyncClient
293
+ async with httpx.AsyncClient(timeout=120.0, verify=False) as client:
294
+ ```
295
+
296
+ **Production solution**:
297
+ ```bash
298
+ # Update certificates
299
+ pip install --upgrade certifi
300
+
301
+ # Or configure system proxy/certificates
302
+ ```
303
+
304
+ ### Module Not Found Error
305
+
306
+ Make sure you're running from the project directory:
307
+ ```bash
308
+ cd Mini-Agent
309
+ python -m mini_agent.cli
310
+ ```
311
+
312
+ ## Related Documentation
313
+
314
+ - [Development Guide](docs/DEVELOPMENT_GUIDE.md) - Detailed development and configuration guidance
315
+ - [Production Guide](docs/PRODUCTION_GUIDE.md) - Best practices for production deployment
316
+
317
+ ## Community
318
+
319
+ Join the MiniMax official community to get help, share ideas, and stay updated:
320
+
321
+ - **WeChat Group**: Scan the QR code on [Contact Us](https://platform.minimaxi.com/docs/faq/contact-us) page to join
322
+
323
+ ## Contributing
324
+
325
+ Issues and Pull Requests are welcome!
326
+
327
+ - [Contributing Guide](CONTRIBUTING.md) - How to contribute
328
+ - [Code of Conduct](CODE_OF_CONDUCT.md) - Community guidelines
329
+
330
+ ## License
331
+
332
+ This project is licensed under the [MIT License](LICENSE).
333
+
334
+ ## References
335
+
336
+ - MiniMax API: https://platform.minimaxi.com/document
337
+ - MiniMax-M2: https://github.com/MiniMax-AI/MiniMax-M2
338
+ - Anthropic API: https://docs.anthropic.com/claude/reference
339
+ - Claude Skills: https://github.com/anthropics/skills
340
+ - MCP Servers: https://github.com/modelcontextprotocol/servers
341
+
342
+ ---
343
+
344
+ **⭐ If this project helps you, please give it a Star!**
README_CN.md ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mini Agent
2
+
3
+ [English](./README.md) | 中文
4
+
5
+ **Mini Agent** 是一个极简但专业的演示项目,旨在展示使用 MiniMax M2.1 模型构建 Agent 的最佳实践。项目通过兼容 Anthropic 的 API,完全支持交错思维(interleaved thinking),从而解锁 M2 模型在处理长而复杂的任务时强大的推理能力。
6
+
7
+ 该项目具备一系列为稳健、智能的 Agent 开发而设计的特性:
8
+
9
+ * ✅ **完整的 Agent 执行循环**:一个完整可靠的执行框架,配备了文件系统和 Shell 操作的基础工具集。
10
+ * ✅ **持久化记忆**:通过内置的 **Session Note Tool**,Agent 能够在多个会话中保留关键信息。
11
+ * ✅ **智能上下文管理**:自动对会话历史进行摘要,可处理长达可配置 Token 上限的上下文,从而支持无限长的任务。
12
+ * ✅ **集成 Claude Skills**:内置 15 种专业技能,涵盖文档处理、设计、测试和开发等领域。
13
+ * ✅ **集成 MCP 工具**:原生支持 MCP 协议,可轻松接入知识图谱、网页搜索等工具。
14
+ * ✅ **全面的日志记录**:为每个请求、响应和工具执行提供详细日志,便于调试。
15
+ * ✅ **简洁明了的设计**:美观的命令行界面和易于理解的代码库,使其成为构建高级 Agent 的理想起点。
16
+
17
+ ## 目录
18
+
19
+ - [Mini Agent](#mini-agent)
20
+ - [目录](#目录)
21
+ - [快速开始](#快速开始)
22
+ - [1. 获取 API Key](#1-获取-api-key)
23
+ - [2. 选择使用模式](#2-选择使用模式)
24
+ - [🚀 快速上手模式(推荐新手)](#-快速上手模式推荐新手)
25
+ - [🔧 开发模式](#-开发模式)
26
+ - [ACP \& Zed Editor 集成(可选)](#acp--zed-editor-集成可选)
27
+ - [使用示例](#使用示例)
28
+ - [任务执行](#任务执行)
29
+ - [使用 Claude Skill(例如:PDF 生成)](#使用-claude-skill例如pdf-生成)
30
+ - [网页搜索与摘要(MCP 工具)](#网页搜索与摘要mcp-工具)
31
+ - [测试](#测试)
32
+ - [快速运行](#快速运行)
33
+ - [测试覆盖范围](#测试覆盖范围)
34
+ - [常见问题](#常见问题)
35
+ - [SSL 证书错误](#ssl-证书错误)
36
+ - [模块未找到错误](#模块未找到错误)
37
+ - [相关文档](#相关文档)
38
+ - [社区](#社区)
39
+ - [贡献](#贡献)
40
+ - [许可证](#许可证)
41
+ - [参考资源](#参考资源)
42
+
43
+ ## 快速开始
44
+
45
+ ### 1. 获取 API Key
46
+
47
+ MiniMax 提供国内和海外两个平台,请根据您的网络环境选择:
48
+
49
+ | 版本 | 平台地址 | API Base |
50
+ | ---------- | -------------------------------------------------------------- | -------------------------- |
51
+ | **国内版** | [https://platform.minimaxi.com](https://platform.minimaxi.com) | `https://api.minimaxi.com` |
52
+ | **海外版** | [https://platform.minimax.io](https://platform.minimax.io) | `https://api.minimax.io` |
53
+
54
+ **获取步骤:**
55
+ 1. 访问相应平台注册并登录
56
+ 2. 进入 **账户管理 > API 密钥**
57
+ 3. 点击 **"创建新密钥"**
58
+ 4. 复制并妥善保存(密钥仅显示一次)
59
+
60
+ > 💡 **提示**:请记住您所选平台对应的 API Base 地址,后续配置时会用到。
61
+
62
+ ### 2. 选择使用模式
63
+
64
+ **前置要求:安装 uv**
65
+
66
+ 两种使用模式都需要 uv。如果您尚未安装:
67
+
68
+ ```bash
69
+ # macOS/Linux/WSL
70
+ curl -LsSf https://astral.sh/uv/install.sh | sh
71
+
72
+ # Windows (PowerShell)
73
+ python -m pip install --user pipx
74
+ python -m pipx ensurepath
75
+ # 安装后需要重启 PowerShell
76
+
77
+ # 安装完成后,重启终端或运行:
78
+ source ~/.bashrc # 或 ~/.zshrc (macOS/Linux)
79
+ ```
80
+
81
+ 我们提供两种使用模式,请根据您的需求选择:
82
+
83
+ #### 🚀 快速上手模式(推荐新手)
84
+
85
+ 此模式适合希望快速体验 Mini Agent,而无需克隆代码仓库或修改代码的用户。
86
+
87
+ **安装步骤:**
88
+
89
+ ```bash
90
+ # 1. 直接从 GitHub 安装
91
+ uv tool install git+https://github.com/MiniMax-AI/Mini-Agent.git
92
+
93
+ # 2. 运行配置脚本(自动创建配置文件)
94
+ # macOS/Linux:
95
+ curl -fsSL https://raw.githubusercontent.com/MiniMax-AI/Mini-Agent/main/scripts/setup-config.sh | bash
96
+
97
+ # Windows (PowerShell):
98
+ Invoke-WebRequest -Uri "https://raw.githubusercontent.com/MiniMax-AI/Mini-Agent/main/scripts/setup-config.ps1" -OutFile "$env:TEMP\setup-config.ps1"
99
+ powershell -ExecutionPolicy Bypass -File "$env:TEMP\setup-config.ps1"
100
+ ```
101
+
102
+ > 💡 **提示**:如果您希望在本地进行开发或修改代码,请使用下方的"开发模式"。
103
+
104
+ **配置步骤:**
105
+
106
+ 配置脚本会在 `~/.mini-agent/config/` 目录下创建配置文件,请编辑该文件:
107
+
108
+ ```bash
109
+ # 编辑配置文件
110
+ nano ~/.mini-agent/config/config.yaml
111
+ ```
112
+
113
+ 填入您的 API Key 和对应的 API Base:
114
+
115
+ ```yaml
116
+ api_key: "YOUR_API_KEY_HERE" # 填入第 1 步获取的 API Key
117
+ api_base: "https://api.minimaxi.com" # 国内版
118
+ # api_base: "https://api.minimax.io" # 海外版(如使用海外平台,请取消本行注释)
119
+ model: "MiniMax-M2.1"
120
+ ```
121
+
122
+ **���始使用:**
123
+
124
+ ```bash
125
+ mini-agent # 使用当前目录作为工作空间
126
+ mini-agent --workspace /path/to/your/project # 指定工作空间目录
127
+ mini-agent --version # 查看版本信息
128
+
129
+ # 管理命令
130
+ uv tool upgrade mini-agent # 升级到最新版本
131
+ uv tool uninstall mini-agent # 卸载工具(如需要)
132
+ uv tool list # 查看所有已安装的工具
133
+ ```
134
+
135
+ #### 🔧 开发模式
136
+
137
+ 此模式适合需要修改代码、添加功能或进行调试的开发者。
138
+
139
+ **安装与配置步骤:**
140
+
141
+ ```bash
142
+ # 1. 克隆仓库
143
+ git clone https://github.com/MiniMax-AI/Mini-Agent.git
144
+ cd Mini-Agent
145
+
146
+ # 2. 安装 uv(如果尚未安装)
147
+ # macOS/Linux:
148
+ curl -LsSf https://astral.sh/uv/install.sh | sh
149
+ # Windows (PowerShell):
150
+ irm https://astral.sh/uv/install.ps1 | iex
151
+ # 安装后需要重启终端
152
+
153
+ # 3. 同步依赖
154
+ uv sync
155
+
156
+ # 替代方案: 手动安装依赖(如果不使用 uv)
157
+ # pip install -r requirements.txt
158
+ # 或者安装必需的包:
159
+ # pip install tiktoken pyyaml httpx pydantic requests prompt-toolkit mcp
160
+
161
+ # 4. 初始化 Claude Skills(可选)
162
+ git submodule update --init --recursive
163
+
164
+ # 5. 复制配置模板
165
+ ```
166
+
167
+ **macOS/Linux:**
168
+ ```bash
169
+ cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml
170
+ ```
171
+
172
+ **Windows:**
173
+ ```powershell
174
+ Copy-Item mini_agent\config\config-example.yaml mini_agent\config\config.yaml
175
+
176
+ # 6. 编辑配置文件
177
+ vim mini_agent/config/config.yaml # 或使用您偏好的编辑器
178
+ ```
179
+
180
+ 填入您的 API Key 和对应的 API Base:
181
+
182
+ ```yaml
183
+ api_key: "YOUR_API_KEY_HERE" # 填入第 1 步获取的 API Key
184
+ api_base: "https://api.minimaxi.com" # 国内版
185
+ # api_base: "https://api.minimax.io" # 海外版(如使用海外平台,请修改此行)
186
+ model: "MiniMax-M2.1"
187
+ max_steps: 100
188
+ workspace_dir: "./workspace"
189
+ ```
190
+
191
+ > 📖 完整的配置指南,请参阅 [config-example.yaml](mini_agent/config/config-example.yaml)
192
+
193
+ **运行方式:**
194
+
195
+ 选择您偏好的方式运行:
196
+
197
+ ```bash
198
+ # 方式 1:作为模块直接运行(适合调试)
199
+ uv run python -m mini_agent.cli
200
+
201
+ # 方式 2:以可编辑模式安装(推荐)
202
+ uv tool install -e .
203
+ # 安装后,您可以在任何路径下运行,且代码更改会立即生效
204
+ mini-agent
205
+ mini-agent --workspace /path/to/your/project
206
+ ```
207
+
208
+ > 📖 更多开发指引,请参阅 [开发指南](docs/DEVELOPMENT_GUIDE_CN.md)
209
+
210
+ > 📖 更多生产部署指引,请参阅 [生产指南](docs/PRODUCTION_GUIDE_CN.md)
211
+
212
+ ## ACP & Zed Editor 集成(可选)
213
+
214
+ Mini Agent 支持 [Agent Communication Protocol (ACP)](https://github.com/modelcontextprotocol/protocol),可与 Zed 等代码编辑器集成。
215
+
216
+ **在 Zed Editor 中设置:**
217
+
218
+ 1. 以开发模式或工具模式安装 Mini Agent
219
+ 2. 在您的 Zed `settings.json` 中添加:
220
+
221
+ ```json
222
+ {
223
+ "agent_servers": {
224
+ "mini-agent": {
225
+ "command": "/path/to/mini-agent-acp"
226
+ }
227
+ }
228
+ }
229
+ ```
230
+
231
+ 命令路径应为:
232
+ - 通过 `uv tool install` 安装:使用 `which mini-agent-acp` 的输出结果
233
+ - 开发模式:`./mini_agent/acp/server.py`
234
+
235
+ **使用方法:**
236
+ - 使用 `Ctrl+Shift+P` → "Agent: Toggle Panel" 打开 Zed 的 Agent 面板
237
+ - 从 Agent 下拉列表中选择 "mini-agent"
238
+ - 直接在编辑器中开始与 Mini Agent 对话
239
+
240
+ ## 使用示例
241
+
242
+ 这里有几个 Mini Agent 能力的演示。
243
+
244
+ ### 任务执行
245
+
246
+ *在这个演示中,我们要求 Agent 创建一个简洁美观的网页并在浏览器中显示它,以此展示基础的工具使用循环。*
247
+
248
+ ![演示动图 1: 基础任务执行](docs/assets/demo1-task-execution.gif "基础任务执行演示")
249
+
250
+ ### 使用 Claude Skill(例如:PDF 生成)
251
+
252
+ *这里,Agent 利用 Claude Skill 根据用户请求创建专业文档(如 PDF 或 DOCX),展示了其强大的高级能力。*
253
+
254
+ ![演示动图 2: Claude Skill 使用](docs/assets/demo2-claude-skill.gif "Claude Skill 使用演示")
255
+
256
+ ### 网页搜索与摘要(MCP 工具)
257
+
258
+ *此演示展示了 Agent 如何使用其网页搜索工具在线查找最新信息,并为用户进行总结。*
259
+
260
+ ![演示动图 3: 网页搜索](docs/assets/demo3-web-search.gif "网页搜索演示")
261
+
262
+
263
+ ## 测试
264
+
265
+ 项目包含了覆盖单元测试、功能测试和集成测试的全面测试用例。
266
+
267
+ ### 快速运行
268
+
269
+ ```bash
270
+ # 运行所有测试
271
+ pytest tests/ -v
272
+
273
+ # 仅运行核心功能测试
274
+ pytest tests/test_agent.py tests/test_note_tool.py -v
275
+ ```
276
+
277
+ ### 测试覆盖范围
278
+
279
+ - ✅ **单元测试** - 工具类、LLM 客户端
280
+ - ✅ **功能测试** - Session Note Tool、MCP 加载
281
+ - ✅ **集成测试** - Agent 端到端执行
282
+ - ✅ **外部服务** - Git MCP 服务器加载
283
+
284
+
285
+ ## 常见问题
286
+
287
+ ### SSL 证书错误
288
+
289
+ 如果遇到 `[SSL: CERTIFICATE_VERIFY_FAILED]` 错误:
290
+
291
+ **测试环境快速修复** (修改 `mini_agent/llm.py`):
292
+ ```python
293
+ # 第 50 行: 给 AsyncClient 添加 verify=False
294
+ async with httpx.AsyncClient(timeout=120.0, verify=False) as client:
295
+ ```
296
+
297
+ **生产环境解决方案**:
298
+ ```bash
299
+ # 更新证书
300
+ pip install --upgrade certifi
301
+
302
+ # 或配置系统代理/证书
303
+ ```
304
+
305
+ ### 模块未找到错误
306
+
307
+ 确保从项目目录运行:
308
+ ```bash
309
+ cd Mini-Agent
310
+ python -m mini_agent.cli
311
+ ```
312
+
313
+ ## 相关文档
314
+
315
+ - [开发指南](docs/DEVELOPMENT_GUIDE_CN.md) - 详细的开发和配置指引
316
+ - [生产环境指南](docs/PRODUCTION_DEPLOYMENT_GUIDE_CN.md) - 生产部署最佳实践
317
+
318
+ ## 社区
319
+
320
+ 加入 MiniMax 官方社区,获取帮助、分享想法、了解最新动态:
321
+
322
+ - **微信交流群**:扫描 [联系我们](https://platform.minimaxi.com/docs/faq/contact-us) 页面的二维码加入官方交流群
323
+
324
+ ## 贡献
325
+
326
+ 我们欢迎并鼓励您提交 Issue 和 Pull Request!
327
+
328
+ - [贡献指南](CONTRIBUTING.md) - 如何为项目做贡献
329
+ - [行为准则](CODE_OF_CONDUCT.md) - 社区行为准则
330
+
331
+ ## 许可证
332
+
333
+ 本项目采用 [MIT 许可证](LICENSE) 授权。
334
+
335
+ ## 参考资源
336
+
337
+ - MiniMax API: https://platform.minimaxi.com/document
338
+ - MiniMax-M2: https://github.com/MiniMax-AI/MiniMax-M2
339
+ - Anthropic API: https://docs.anthropic.com/claude/reference
340
+ - Claude Skills: https://github.com/anthropics/skills
341
+ - MCP Servers: https://github.com/modelcontextprotocol/servers
342
+
343
+ ---
344
+
345
+ **⭐ 如果这个项目对您有帮助,请给它一个 Star!**
docs/DEVELOPMENT_GUIDE.md ADDED
@@ -0,0 +1,417 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Development Guide
2
+
3
+ ## Table of Contents
4
+
5
+ - [Development Guide](#development-guide)
6
+ - [Table of Contents](#table-of-contents)
7
+ - [1. Project Architecture](#1-project-architecture)
8
+ - [2. Basic Usage](#2-basic-usage)
9
+ - [2.1 Interactive Commands](#21-interactive-commands)
10
+ - [2.2 Integrated MCP Tools](#22-integrated-mcp-tools)
11
+ - [Memory - Knowledge Graph Memory System](#memory---knowledge-graph-memory-system)
12
+ - [MiniMax Search - Web Search and Browse](#minimax-search---web-search-and-browse)
13
+ - [3. Extended Abilities](#3-extended-abilities)
14
+ - [3.1 Adding Custom Tools](#31-adding-custom-tools)
15
+ - [Steps](#steps)
16
+ - [Example](#example)
17
+ - [3.2 Adding MCP Tools](#32-adding-mcp-tools)
18
+ - [3.3 Customizing Note Storage](#33-customizing-note-storage)
19
+ - [3.4 Initialize Claude Skills (Recommended)](#34-initialize-claude-skills-recommended)
20
+ - [3.5 Adding a New Skill](#35-adding-a-new-skill)
21
+ - [3.6 Customizing System Prompt](#36-customizing-system-prompt)
22
+ - [What You Can Customize](#what-you-can-customize)
23
+ - [4. Troubleshooting](#4-troubleshooting)
24
+ - [4.1 Common Issues](#41-common-issues)
25
+ - [API Key Configuration Error](#api-key-configuration-error)
26
+ - [Dependency Installation Failure](#dependency-installation-failure)
27
+ - [MCP Tool Loading Failure](#mcp-tool-loading-failure)
28
+ - [4.2 Debugging Tips](#42-debugging-tips)
29
+ - [Enable Verbose Logging](#enable-verbose-logging)
30
+ - [Using the Python Debugger](#using-the-python-debugger)
31
+ - [Inspecting Tool Calls](#inspecting-tool-calls)
32
+
33
+ ---
34
+
35
+ ## 1. Project Architecture
36
+
37
+ ```
38
+ mini-agent/
39
+ ├── mini_agent/ # Core source code
40
+ │ ├── agent.py # Main agent loop
41
+ │ ├── llm.py # LLM client
42
+ │ ├── cli.py # Command-line interface
43
+ │ ├── config.py # Configuration loading
44
+ │ ├── tools/ # Tool implementations (file, bash, MCP, skills, etc.)
45
+ │ └── skills/ # Claude Skills (submodule)
46
+ ├── tests/ # Test code
47
+ ├── docs/ # Documentation
48
+ ├── workspace/ # Working directory
49
+ └── pyproject.toml # Project configuration
50
+ ```
51
+
52
+ ## 2. Basic Usage
53
+
54
+ ### 2.1 Interactive Commands
55
+
56
+ When running the agent in interactive mode (`mini-agent`), the following commands are available:
57
+
58
+ | Command | Description |
59
+ | ---------------------- | ----------------------------------------------------------- |
60
+ | `/exit`, `/quit`, `/q` | Exit the agent and display session statistics |
61
+ | `/help` | Display help information and available commands |
62
+ | `/clear` | Clear message history and start a new session |
63
+ | `/history` | Show the current session message count |
64
+ | `/stats` | Display session statistics (steps, tool calls, tokens used) |
65
+
66
+ ### 2.2 Integrated MCP Tools
67
+
68
+ This project comes with pre-configured MCP (Model Context Protocol) tools that extend the agent's capabilities:
69
+
70
+ #### Memory - Knowledge Graph Memory System
71
+
72
+ **Function**: Provides long-term memory storage and retrieval based on graph database
73
+
74
+ **Status**: Enabled by default (`disabled: false`)
75
+
76
+ **Configuration**: No API Key required, works out of the box
77
+
78
+ **Capabilities**:
79
+ - Store and retrieve information across sessions
80
+ - Build knowledge graphs from conversations
81
+ - Semantic search through stored memories
82
+
83
+ ---
84
+
85
+ #### MiniMax Search - Web Search and Browse
86
+
87
+ **Function**: Provides three powerful tools:
88
+ - `search` - Web search capability
89
+ - `parallel_search` - Execute multiple searches simultaneously
90
+ - `browse` - Intelligent web browsing and content extraction
91
+
92
+ **Status**: Disabled by default, needs configuration to enable
93
+
94
+ **Configuration Example**
95
+
96
+ ```json
97
+ {
98
+ "mcpServers": {
99
+ "minimax_search": {
100
+ "disabled": false,
101
+ "env": {
102
+ "JINA_API_KEY": "your-jina-api-key",
103
+ "SERPER_API_KEY": "your-serper-api-key",
104
+ "MINIMAX_TOKEN": "your-minimax-token"
105
+ }
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ ## 3. Extended Abilities
112
+
113
+ ### 3.1 Adding Custom Tools
114
+
115
+ #### Steps
116
+
117
+ 1. Create a new tool file under `mini_agent/tools/`.
118
+ 2. Inherit from the `Tool` base class.
119
+ 3. Implement the required properties and methods.
120
+ 4. Register the tool during Agent initialization.
121
+
122
+ #### Example
123
+
124
+ ```python
125
+ # mini_agent/tools/my_tool.py
126
+ from mini_agent.tools.base import Tool, ToolResult
127
+ from typing import Dict, Any
128
+
129
+ class MyTool(Tool):
130
+ @property
131
+ def name(self) -> str:
132
+ """A unique name for the tool."""
133
+ return "my_tool"
134
+
135
+ @property
136
+ def description(self) -> str:
137
+ """A description for the LLM to understand the tool's purpose."""
138
+ return "My custom tool for doing something useful"
139
+
140
+ @property
141
+ def parameters(self) -> Dict[str, Any]:
142
+ """Parameter schema in JSON Schema format."""
143
+ return {
144
+ "type": "object",
145
+ "properties": {
146
+ "param1": {
147
+ "type": "string",
148
+ "description": "First parameter"
149
+ },
150
+ "param2": {
151
+ "type": "integer",
152
+ "description": "Second parameter",
153
+ "default": 10
154
+ }
155
+ },
156
+ "required": ["param1"]
157
+ }
158
+
159
+ async def execute(self, param1: str, param2: int = 10) -> ToolResult:
160
+ """
161
+ The main logic of the tool.
162
+
163
+ Args:
164
+ param1: The first parameter.
165
+ param2: The second parameter, with a default value.
166
+
167
+ Returns:
168
+ A ToolResult object.
169
+ """
170
+ try:
171
+ # Implement your logic here
172
+ result = f"Processed {param1} with param2={param2}"
173
+
174
+ return ToolResult(
175
+ success=True,
176
+ content=result
177
+ )
178
+ except Exception as e:
179
+ return ToolResult(
180
+ success=False,
181
+ content=f"Error: {str(e)}"
182
+ )
183
+
184
+ # In cli.py or agent initialization code
185
+ from mini_agent.tools.my_tool import MyTool
186
+
187
+ # Add the new tool when creating the Agent
188
+ tools = [
189
+ ReadTool(workspace_dir),
190
+ WriteTool(workspace_dir),
191
+ MyTool(), # Add your custom tool
192
+ ]
193
+
194
+ agent = Agent(
195
+ llm=llm,
196
+ tools=tools,
197
+ max_steps=50
198
+ )
199
+ ```
200
+
201
+ ### 3.2 Adding MCP Tools
202
+
203
+ Edit `mcp.json` to add a new MCP Server:
204
+
205
+ ```json
206
+ {
207
+ "mcpServers": {
208
+ "my_custom_mcp": {
209
+ "description": "My custom MCP server",
210
+ "type": "stdio",
211
+ "command": "npx",
212
+ "args": ["-y", "@my-org/my-mcp-server"],
213
+ "env": {
214
+ "API_KEY": "your-api-key"
215
+ },
216
+ "disabled": false,
217
+ "notes": {
218
+ "description": "This is a custom MCP server.",
219
+ "api_key_url": "https://example.com/api-keys"
220
+ }
221
+ }
222
+ }
223
+ }
224
+ ```
225
+
226
+ ### 3.3 Customizing Note Storage
227
+
228
+ To replace the storage backend for the `SessionNoteTool`:
229
+
230
+ ```python
231
+ # Current implementation: JSON file
232
+ class SessionNoteTool:
233
+ def __init__(self, memory_file: str = "./workspace/.agent_memory.json"):
234
+ self.memory_file = Path(memory_file)
235
+
236
+ async def _save_notes(self, notes: List[Dict]):
237
+ with open(self.memory_file, 'w') as f:
238
+ json.dump(notes, f, indent=2, ensure_ascii=False)
239
+
240
+ # Example extension: PostgreSQL
241
+ class PostgresNoteTool(Tool):
242
+ def __init__(self, db_url: str):
243
+ self.db = PostgresDB(db_url)
244
+
245
+ async def _save_notes(self, notes: List[Dict]):
246
+ await self.db.execute(
247
+ "INSERT INTO notes (content, category, timestamp) VALUES ($1, $2, $3)",
248
+ notes
249
+ )
250
+
251
+ # Example extension: Vector Database
252
+ class MilvusNoteTool(Tool):
253
+ def __init__(self, milvus_host: str):
254
+ self.vector_db = MilvusClient(host=milvus_host)
255
+
256
+ async def _save_notes(self, notes: List[Dict]):
257
+ # Generate embeddings
258
+ embeddings = await self.get_embeddings([n["content"] for n in notes])
259
+
260
+ # Store in the vector database
261
+ await self.vector_db.insert(
262
+ collection="agent_notes",
263
+ data=notes,
264
+ embeddings=embeddings
265
+ )
266
+ ```
267
+
268
+ ### 3.4 Initialize Claude Skills (Recommended)
269
+
270
+ This project integrates Claude's official skills repository via git submodule. Initialize it after first clone:
271
+
272
+ ```bash
273
+ # Initialize submodule
274
+ git submodule update --init --recursive
275
+ ```
276
+
277
+ Skills provide 20+ professional capabilities, making the Agent work like a professional:
278
+
279
+ - 📄 **Document Processing**: Create and edit PDF, DOCX, XLSX, PPTX
280
+ - 🎨 **Design Creation**: Generate artwork, posters, GIF animations
281
+ - 🧪 **Development & Testing**: Web automation testing (Playwright), MCP server development
282
+ - 🏢 **Enterprise Applications**: Internal communication, brand guidelines, theme customization
283
+
284
+ ✨ **This is one of the core highlights of this project.** For details, see the "Configure Skills" section below.
285
+
286
+ **More information:**
287
+
288
+ - [Claude Skills Official Documentation](https://docs.claude.com/zh-CN/docs/agents-and-tools/agent-skills)
289
+ - [Anthropic Blog: Equipping agents for the real world](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)
290
+
291
+ ### 3.5 Adding a New Skill
292
+
293
+ Create a custom Skill:
294
+
295
+ ```bash
296
+ # Create a new skill directory under skills/
297
+ mkdir skills/my-custom-skill
298
+ cd skills/my-custom-skill
299
+
300
+ # Create the SKILL.md file
301
+ cat > SKILL.md << 'EOF'
302
+ ---
303
+ name: my-custom-skill
304
+ description: My custom skill for handling specific tasks.
305
+ ---
306
+
307
+ # Overview
308
+
309
+ This skill provides the following capabilities:
310
+ - Capability 1
311
+ - Capability 2
312
+
313
+ # Usage
314
+
315
+ 1. Step one...
316
+ 2. Step two...
317
+
318
+ # Best Practices
319
+
320
+ - Practice 1
321
+ - Practice 2
322
+
323
+ # FAQ
324
+
325
+ Q: Question 1
326
+ A: Answer 1
327
+ ```
328
+
329
+ The new Skill will be automatically loaded and recognized by the Agent.
330
+
331
+ ### 3.6 Customizing System Prompt
332
+
333
+ The system prompt (`system_prompt.md`) defines the Agent's behavior, capabilities, and working guidelines. You can customize it to tailor the Agent for specific use cases.
334
+
335
+ #### What You Can Customize
336
+
337
+ 1. **Core Capabilities**: Add or modify tool descriptions
338
+ 2. **Working Guidelines**: Define custom workflows and best practices
339
+ 3. **Domain-Specific Knowledge**: Add expertise in specific areas
340
+ 4. **Communication Style**: Adjust how the Agent interacts with users
341
+ 5. **Task Priorities**: Set preferences for how tasks should be approached
342
+
343
+ After modifying `system_prompt.md`, be sure to restart the Agent to apply changes
344
+
345
+ ## 4. Troubleshooting
346
+
347
+ ### 4.1 Common Issues
348
+
349
+ #### API Key Configuration Error
350
+
351
+ ```bash
352
+ # Error message
353
+ Error: Invalid API key
354
+
355
+ # Solution
356
+ 1. Check that the API key in `config.yaml` is correct.
357
+ 2. Ensure there are no extra spaces or quotes.
358
+ 3. Verify that the API key has not expired.
359
+ ```
360
+
361
+ #### Dependency Installation Failure
362
+
363
+ ```bash
364
+ # Error message
365
+ uv sync failed
366
+
367
+ # Solution
368
+ 1. Update uv to the latest version: `uv self update`
369
+ 2. Clear the cache: `uv cache clean`
370
+ 3. Try syncing again: `uv sync`
371
+ ```
372
+
373
+ #### MCP Tool Loading Failure
374
+
375
+ ```bash
376
+ # Error message
377
+ Failed to load MCP server
378
+
379
+ # Solution
380
+ 1. Check the configuration in `mcp.json` is correct.
381
+ 2. Ensure Node.js is installed (required for most MCP tools).
382
+ 3. Verify that any required API keys are configured.
383
+ 4. View detailed logs: `pytest tests/test_mcp.py -v -s`
384
+ ```
385
+
386
+ ### 4.2 Debugging Tips
387
+
388
+ #### Enable Verbose Logging
389
+
390
+ ```python
391
+ # At the beginning of cli.py or a test file
392
+ import logging
393
+
394
+ logging.basicConfig(
395
+ level=logging.DEBUG,
396
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
397
+ )
398
+ ```
399
+
400
+ #### Using the Python Debugger
401
+
402
+ ```python
403
+ # Set a breakpoint in your code
404
+ import pdb; pdb.set_trace()
405
+
406
+ # Or use ipdb for a better experience
407
+ import ipdb; ipdb.set_trace()
408
+ ```
409
+
410
+ #### Inspecting Tool Calls
411
+
412
+ ```python
413
+ # Add logging in the Agent to see tool interactions
414
+ logger.debug(f"Tool call: {tool_call.name}")
415
+ logger.debug(f"Tool arguments: {tool_call.arguments}")
416
+ logger.debug(f"Tool result: {result.content[:200]}")
417
+ ```
docs/DEVELOPMENT_GUIDE_CN.md ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 开发指南
2
+
3
+
4
+ ## 目录
5
+
6
+ - [开发指南](#开发指南)
7
+ - [目录](#目录)
8
+ - [1. 项目架构](#1-项目架构)
9
+ - [2. 基础使用](#2-基础使用)
10
+ - [2.1 交互式命令](#21-交互式命令)
11
+ - [2.2 已集成的 MCP 工具](#22-已集成的-mcp-工具)
12
+ - [Memory - 知识图谱记忆系统](#memory---知识图谱记忆系统)
13
+ - [MiniMax Search - 网页搜索与浏览](#minimax-search---网页搜索与浏览)
14
+ - [3. 扩展能力](#3-扩展能力)
15
+ - [3.1 添加自定义工具](#31-添加自定义工具)
16
+ - [步骤](#步骤)
17
+ - [示例](#示例)
18
+ - [3.2 添加 MCP 工具](#32-添加-mcp-工具)
19
+ - [3.3 自定义存储](#33-自定义存储)
20
+ - [3.4 初始化 Claude Skills(推荐)](#34-初始化-claude-skills推荐)
21
+ - [3.5 添加新的Skill](#35-添加新的skill)
22
+ - [3.6 自定义系统提示词](#36-自定义系统提示词)
23
+ - [可定制内容包括:](#可定制内容包括)
24
+ - [4. 故障排查](#4-故障排查)
25
+ - [4.1 常见问题](#41-常见问题)
26
+ - [API 密钥配置错误](#api-密钥配置错误)
27
+ - [依赖安装失败](#依赖安装失败)
28
+ - [MCP 工具加载失败](#mcp-工具加载失败)
29
+ - [4.2 调试技巧](#42-调试技巧)
30
+ - [启用 Debug 日志](#启用-debug-日志)
31
+ - [使用 Python 调试器](#使用-python-调试器)
32
+ - [监控工具调用](#监控工具调用)
33
+
34
+ ---
35
+
36
+ ## 1. 项目架构
37
+
38
+ ```
39
+ mini-agent/
40
+ ├── mini_agent/ # 核心源代码
41
+ │ ├── agent.py # 主 Agent 循环
42
+ │ ├── llm.py # LLM 客户端
43
+ │ ├── cli.py # 命令行接口
44
+ │ ├── config.py # 配置加载
45
+ │ ├── tools/ # 工具实现(文件、Bash、MCP、技能等)
46
+ │ └── skills/ # Claude 技能集(子模块)
47
+ ├── tests/ # 测试代码
48
+ ├── docs/ # 文档
49
+ ├── workspace/ # 工作目录
50
+ └── pyproject.toml # 项目配置
51
+ ```
52
+
53
+ ## 2. 基础使用
54
+
55
+ ### 2.1 交互式命令
56
+
57
+ 在交互模式 (通过 `mini-agent` 启动) 下运行 Agent 时,您可以使用以下命令:
58
+
59
+ | 命令 | 说明 |
60
+ | ---------------------- | ------------------------------------------------ |
61
+ | `/exit`, `/quit`, `/q` | 退出 Agent 并显示会话统计信息 |
62
+ | `/help` | 显示帮助信息和可用命令 |
63
+ | `/clear` | 清除消息历史并开始新会话 |
64
+ | `/history` | 显示当前会话的消息数量 |
65
+ | `/stats` | 显示会话统计信息(步数、工具调用、使用的 Token) |
66
+
67
+ ### 2.2 已集成的 MCP 工具
68
+
69
+ 本项目预先集成了以下 MCP (模型上下文协议) 工具,用以扩展 Agent 的能力:
70
+
71
+ #### Memory - 知识图谱记忆系统
72
+
73
+ **功能**:基于图数据库,为 Agent 提供长期记忆的存储与检索能力。
74
+
75
+ **状态**:默认启用
76
+
77
+ **配置**:无需 API Key,开箱即用
78
+
79
+ **能力**:
80
+ - 跨会话存储并检索信息
81
+ - 根据对话内容构建知识图谱
82
+ - 对已存储的记忆进行语义搜索
83
+
84
+ ---
85
+
86
+ #### MiniMax Search - 网页搜索与浏览
87
+
88
+ **功能**:提供三大强大工具:
89
+ - `search` - 网页搜索
90
+ - `parallel_search` - 并行执行多个搜索任务
91
+ - `browse` - 智能网页浏览与内容提取
92
+
93
+ **状态**:默认禁用,需要配置 API Key 后方可启用。
94
+
95
+ **配置示例**:
96
+
97
+ ```json
98
+ {
99
+ "mcpServers": {
100
+ "minimax_search": {
101
+ "disabled": false,
102
+ "env": {
103
+ "JINA_API_KEY": "your-jina-api-key",
104
+ "SERPER_API_KEY": "your-serper-api-key",
105
+ "MINIMAX_API_KEY": "your-minimax-token"
106
+ }
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ ## 3. 扩展能力
113
+
114
+ ### 3.1 添加自定义工具
115
+
116
+ #### 步骤
117
+
118
+ 1. 在 `mini_agent/tools/` 目录下创建一个新的 Python 文件。
119
+ 2. 在文件中定义一个新类,并继承 `Tool` 基类。
120
+ 3. 在类中实现所需的属性和方法。
121
+ 4. 在 Agent 初始化时注册你的新工具。
122
+
123
+ #### 示例
124
+
125
+ ```python
126
+ # mini_agent/tools/my_tool.py
127
+ from mini_agent.tools.base import Tool, ToolResult
128
+ from typing import Dict, Any
129
+
130
+ class MyTool(Tool):
131
+ @property
132
+ def name(self) -> str:
133
+ """工具的唯一名称,需保持独一无二。"""
134
+ return "my_tool"
135
+
136
+ @property
137
+ def description(self) -> str:
138
+ """工具用途的详细描述,帮助 LLM 理解其功能。"""
139
+ return "我的自定义工具,用于完成特定任务"
140
+
141
+ @property
142
+ def parameters(self) -> Dict[str, Any]:
143
+ """参数模式(JSON Schema 格式)。"""
144
+ return {
145
+ "type": "object",
146
+ "properties": {
147
+ "param1": {
148
+ "type": "string",
149
+ "description": "第一���参数"
150
+ },
151
+ "param2": {
152
+ "type": "integer",
153
+ "description": "第二个参数",
154
+ "default": 10
155
+ }
156
+ },
157
+ "required": ["param1"]
158
+ }
159
+
160
+ async def execute(self, param1: str, param2: int = 10) -> ToolResult:
161
+ """
162
+ 工具执行的核心逻辑。
163
+
164
+ Args:
165
+ param1: 参数一。
166
+ param2: 参数二,包含默认值。
167
+
168
+ Returns:
169
+ 返回一个 ToolResult 对象。
170
+ """
171
+ try:
172
+ # 在此实现你的逻辑
173
+ result = f"处理了 {param1},param2={param2}"
174
+
175
+ return ToolResult(
176
+ success=True,
177
+ content=result
178
+ )
179
+ except Exception as e:
180
+ return ToolResult(
181
+ success=False,
182
+ content=f"错误: {str(e)}"
183
+ )
184
+
185
+ # 在 cli.py 或 Agent 的初始化代码中
186
+ from mini_agent.tools.my_tool import MyTool
187
+
188
+ # 创建 Agent 实例时,将新工具加入列表
189
+ tools = [
190
+ ReadTool(workspace_dir),
191
+ WriteTool(workspace_dir),
192
+ MyTool(), # 添加您的自定义工具
193
+ ]
194
+
195
+ agent = Agent(
196
+ llm=llm,
197
+ tools=tools,
198
+ max_steps=50
199
+ )
200
+ ```
201
+
202
+ ### 3.2 添加 MCP 工具
203
+
204
+ 编辑 `mcp.json` 文件,即可添加新的 MCP 服务器:
205
+
206
+ ```json
207
+ {
208
+ "mcpServers": {
209
+ "my_custom_mcp": {
210
+ "description": "我的自定义 MCP 服务器",
211
+ "type": "stdio",
212
+ "command": "npx",
213
+ "args": ["-y", "@my-org/my-mcp-server"],
214
+ "env": {
215
+ "API_KEY": "your-api-key"
216
+ },
217
+ "disabled": false,
218
+ "notes": {
219
+ "description": "这是一个自定义 MCP 服务器。",
220
+ "api_key_url": "https://example.com/api-keys"
221
+ }
222
+ }
223
+ }
224
+ }
225
+ ```
226
+
227
+ ### 3.3 自定义存储
228
+
229
+ 您可以替换 `SessionNoteTool` 的默认存储实现,以对接不同的数据后端:
230
+
231
+ ```python
232
+ # 默认实现:JSON 文件
233
+ class SessionNoteTool:
234
+ def __init__(self, memory_file: str = "./workspace/.agent_memory.json"):
235
+ self.memory_file = Path(memory_file)
236
+
237
+ async def _save_notes(self, notes: List[Dict]):
238
+ with open(self.memory_file, 'w') as f:
239
+ json.dump(notes, f, indent=2, ensure_ascii=False)
240
+
241
+ # 扩展示例:使用 PostgreSQL 存储
242
+ class PostgresNoteTool(Tool):
243
+ def __init__(self, db_url: str):
244
+ self.db = PostgresDB(db_url)
245
+
246
+ async def _save_notes(self, notes: List[Dict]):
247
+ await self.db.execute(
248
+ "INSERT INTO notes (content, category, timestamp) VALUES ($1, $2, $3)",
249
+ notes
250
+ )
251
+
252
+ # 扩展示例:使用向量数据库存储
253
+ class MilvusNoteTool(Tool):
254
+ def __init__(self, milvus_host: str):
255
+ self.vector_db = MilvusClient(host=milvus_host)
256
+
257
+ async def _save_notes(self, notes: List[Dict]):
258
+ # 生成内容的嵌入向量
259
+ embeddings = await self.get_embeddings([n["content"] for n in notes])
260
+
261
+ # 将笔记和向量存入向量数据库
262
+ await self.vector_db.insert(
263
+ collection="agent_notes",
264
+ data=notes,
265
+ embeddings=embeddings
266
+ )
267
+ ```
268
+
269
+ ### 3.4 初始化 Claude Skills(推荐)
270
+
271
+ 本项目通过 Git Submodule 的方式集成了 Claude 官方技能库。首次克隆项目后,请执行以下命令来初始化技能库:
272
+
273
+ ```bash
274
+ # 初始化并拉取技能库子模块
275
+ git submodule update --init --recursive
276
+ ```
277
+
278
+ Skills 库提供了超过20种专业能力,能让 Agent 如同行业专家般处理复杂任务:
279
+
280
+ - 📄 **文档处理**:轻松创建和编辑 PDF、DOCX、XLSX、PPTX 等格式的文档。
281
+ - 🎨 **设计创作**:生成富有创意的艺术作品、海报和 GIF 动画。
282
+ - 🧪 **开发与测试**:支持 Web 自动化测试 (Playwright) 和 MCP 服务器开发。
283
+ - 🏢 **企业应用**:高效处理内部沟通、品牌指南应用和主题定制等任务。
284
+
285
+ ✨ **这是本项目的核心亮点之一。**
286
+
287
+ **更多信息:**
288
+
289
+ - [Claude Skills 官方文档](https://docs.claude.com/zh-CN/docs/agents-and-tools/agent-skills)
290
+ - [Anthropic 博客:为真实世界装备智能体](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)
291
+
292
+ ### 3.5 添加新的Skill
293
+
294
+ 您可以按照以下步骤创建自定义 Skill:
295
+
296
+ ```bash
297
+ # 在 skills/ 目录下为您的新技能创建一个目录
298
+ mkdir skills/my-custom-skill
299
+ cd skills/my-custom-skill
300
+
301
+ # 创建技能描述文件 SKILL.md
302
+ cat > SKILL.md << 'EOF'
303
+ ---
304
+ name: my-custom-skill
305
+ description: 这是一个自定义技能,用于处理特定任务。
306
+ ---
307
+
308
+ # 概述
309
+
310
+ 该技能主要提供以下功能:
311
+ - 功能 1
312
+ - 功能 2
313
+
314
+ # 使用方法
315
+
316
+ 1. 第一步...
317
+ 2. 第二步...
318
+
319
+ # 最佳实践
320
+
321
+ - 实践 1
322
+ - 实践 2
323
+
324
+ # 常见问题
325
+
326
+ 问:问题 1
327
+ 答:答案 1
328
+ EOF
329
+ ```
330
+
331
+ 完成以上步骤后,Agent 将在下次启动时自动加载并识别这项新技能。
332
+
333
+ ### 3.6 自定义系统提示词
334
+
335
+ 系统提示词文件 (`system_prompt.md`) 定义了 Agent 的核心行为、能力边界和工作指南。您可以根据具体应用场景,对其进行深度定制。
336
+
337
+ #### 可定制内容包括:
338
+
339
+ 1. **核心能力**:添加或修改工具的描述,以影响 Agent 的工具选择。
340
+ 2. **工作指南**:定义特定的工作流程或决策偏好。
341
+ 3. **领域专业知识**:注入特定领域的知识,提升 Agent 的专业性。
342
+ 4. **沟通风格**:调整 Agent 与用户交互时的语气和风格。
343
+ 5. **任务优先级**:设定处理任务时的优先级和策略。
344
+
345
+ 完成修改后,请重启 Agent 以使新配置生效。
346
+
347
+ ## 4. 故障排查
348
+
349
+ ### 4.1 常见问题
350
+
351
+ #### API 密钥配置错误
352
+
353
+ ```bash
354
+ # 错误消息
355
+ Error: Invalid API key
356
+
357
+ # 解决方法
358
+ 1. 检查 `config.yaml` 文件中的 API 密钥是否填写正确。
359
+ 2. 确保密钥前后没有多余的空格或引号。
360
+ 3. 确认该 API 密钥是否仍在有效期内。
361
+ ```
362
+
363
+ #### 依赖安装失败
364
+
365
+ ```bash
366
+ # 错误消息
367
+ uv sync failed
368
+
369
+ # 解决方法
370
+ 1. 升级 uv 至最新版本:`uv self update`
371
+ 2. 清理 uv 缓存:`uv cache clean`
372
+ 3. 再次尝试同步依赖:`uv sync`
373
+ ```
374
+
375
+ #### MCP 工具加载失败
376
+
377
+ ```bash
378
+ # 错误消息
379
+ Failed to load MCP server
380
+
381
+ # 解决方法
382
+ 1. 检查 `mcp.json` 文件中的服务器配置是否正确。
383
+ 2. 确保您的开发环境已安装 Node.js (大部分 MCP 工具的运行需要)。
384
+ 3. 确认所需服务的 API 密钥已正确配置。
385
+ 4. 运行 MCP 测试并查看详细日志:`pytest tests/test_mcp.py -v -s`
386
+ ```
387
+
388
+ ### 4.2 调试技巧
389
+
390
+ #### 启用 Debug 日志
391
+
392
+ ```python
393
+ # 在 cli.py 或相关测试文件的开头添加以下代码:
394
+ import logging
395
+
396
+ logging.basicConfig(
397
+ level=logging.DEBUG,
398
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
399
+ )
400
+ ```
401
+
402
+ #### 使用 Python 调试器
403
+
404
+ ```python
405
+ # 在需要暂停执行的代码行处插入断点:
406
+ import pdb; pdb.set_trace()
407
+
408
+ # 或者使用 ipdb 以获得更佳的调试体验:
409
+ import ipdb; ipdb.set_trace()
410
+ ```
411
+
412
+ #### 监控工具调用
413
+
414
+ ```python
415
+ # 在 Agent 代码中添加日志,以便实时查看工具的调用详情:
416
+ logger.debug(f"工具调用: {tool_call.name}")
417
+ logger.debug(f"工具参数: {tool_call.arguments}")
418
+ logger.debug(f"工具结果: {result.content[:200]}")
419
+ ```
420
+
docs/PRODUCTION_GUIDE.md ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agent Production Guide
2
+
3
+ > A Complete Guide from Demo to Production
4
+
5
+ ## Table of Contents
6
+
7
+ - [1. Demo Features](#1-demo-features)
8
+ - [2. Upgrade Directions](#2-upgrade-directions)
9
+ - [3. Production Deployment](#3-production-deployment)
10
+
11
+ ---
12
+
13
+ ## 1. Demo Features
14
+
15
+ This project is a **teaching-level demo** that demonstrates the core concepts and execution flow of an Agent. To reach production level, many complex issues still need to be addressed.
16
+
17
+ ### What We've Implemented (Demo Level)
18
+
19
+ | Feature | Demo Implementation |
20
+ | --------------------- | --------------------------- |
21
+ | **Context Management** | ✅ Simple persistence via SessionNoteTool with file storage; basic summarization when approaching context window limit |
22
+ | **Tool Calling** | ✅ Basic Read/Write/Edit/Bash |
23
+ | **Error Handling** | ✅ Basic exception catching |
24
+ | **Logging** | ✅ Simple print output |
25
+
26
+
27
+ ## 2. Upgrade Directions
28
+
29
+ ### 2.1 Advanced Context Management
30
+
31
+ - Introduce distributed file systems for unified context persistence management and backup
32
+ - Use more precise methods for token counting
33
+ - Introduce more strategies for message compression, including keeping the most recent N messages, preserving fixed metadata, prompt optimization for summarization, introducing recall systems, etc.
34
+
35
+ ### 2.2 Model Fallback Mechanism
36
+
37
+ Currently using a single fixed model (MiniMax-M2.1), which will directly report errors on failure.
38
+
39
+ - Introduce a model pool by configuring multiple model accounts to improve availability
40
+ - Introduce automatic health checks, failure removal, circuit breaker strategies for the model pool
41
+
42
+ ### 2.3 Model Hallucination Detection and Correction
43
+
44
+ Currently directly trusts model output without validation mechanism
45
+
46
+ - Perform security checks on input parameters for certain tool calls to prevent high-risk actions
47
+ - Perform reflection on results from certain tool calls to check if they are reasonable
48
+
49
+ ## 3. Production Deployment
50
+
51
+ ### 3.1 Container Deployment Recommendations
52
+
53
+ We recommend using K8s/Docker environments for Agent deployment. Containerized deployment has the following advantages:
54
+
55
+ - **Resource Isolation**: Each Agent instance runs in an independent container without interference
56
+ - **Elastic Scaling**: Automatically adjust instance count based on load
57
+ - **Version Management**: Easy rollback and canary releases
58
+ - **Environment Consistency**: Development, testing, and production environments are completely consistent
59
+
60
+ ### 3.2 Resource Limit Configuration
61
+
62
+ #### 3.2.1 CPU and Memory Limits
63
+
64
+ To prevent the Agent from consuming excessive CPU/Memory resources and affecting the host, CPU and memory limits must be set:
65
+
66
+ **Docker Configuration Example**:
67
+ ```yaml
68
+ # docker-compose.yml
69
+ services:
70
+ agent:
71
+ image: agent-demo:latest
72
+ deploy:
73
+ resources:
74
+ limits:
75
+ cpus: '2.0' # Maximum 2 CPU cores
76
+ memory: 2G # Maximum 2GB memory
77
+ reservations:
78
+ cpus: '0.5' # Guarantee at least 0.5 cores
79
+ memory: 512M # Guarantee at least 512MB
80
+ ```
81
+
82
+ #### 3.2.2 Disk Limits
83
+
84
+ Agents may generate large amounts of temporary files and log files, so disk usage needs to be limited:
85
+
86
+ **Docker Volume Configuration**:
87
+ ```yaml
88
+ # docker-compose.yml
89
+ services:
90
+ agent:
91
+ volumes:
92
+ - type: tmpfs
93
+ target: /tmp
94
+ tmpfs:
95
+ size: 1G # Maximum 1GB for temporary files
96
+ - type: volume
97
+ source: agent-data
98
+ target: /app/data
99
+ volume:
100
+ driver_opts:
101
+ size: 5G # Maximum 5GB for data volume
102
+ ```
103
+
104
+
105
+ ### 3.3 Linux Account Permission Restrictions
106
+
107
+ #### 3.3.1 Principle of Least Privilege
108
+
109
+ **Never run the Agent as root user**, as this poses serious security risks.
110
+
111
+ **Dockerfile Best Practices**:
112
+ ```dockerfile
113
+ FROM python:3.11-slim
114
+
115
+ # Install necessary system tools
116
+ RUN apt-get update && apt-get install -y \
117
+ git \
118
+ curl \
119
+ && rm -rf /var/lib/apt/lists/*
120
+
121
+ # Install uv
122
+ RUN curl -LsSf https://astral.sh/uv/install.sh | sh
123
+ ENV PATH="/root/.cargo/bin:$PATH"
124
+
125
+ # Create non-privileged user
126
+ RUN groupadd -r agent && useradd -r -g agent agent
127
+
128
+ # Set working directory
129
+ WORKDIR /app
130
+
131
+ # Option 1: Clone from Git repository (for public repos)
132
+ RUN git clone https://github.com/MiniMax-AI/agent-demo.git . && \
133
+ chown -R agent:agent /app
134
+
135
+ # Option 2: Copy code from local (for private deployments)
136
+ # COPY --chown=agent:agent . /app
137
+
138
+ # Switch to non-privileged user before installing dependencies
139
+ USER agent
140
+
141
+ # Sync dependencies using uv
142
+ RUN uv sync
143
+
144
+ # Start the application
145
+ CMD ["uv", "run", "mini-agent"]
146
+ ```
147
+
148
+ #### 3.3.2 File System Permissions
149
+
150
+ Restrict the Agent to only access necessary directories:
151
+
152
+ ```bash
153
+ # Create restricted workspace directory
154
+ mkdir -p /app/workspace
155
+ chown agent:agent /app/workspace
156
+ chmod 750 /app/workspace # Owner: read/write/execute, Group: read/execute
157
+
158
+ # Restrict access to sensitive directories
159
+ chmod 700 /etc/agent # Config directory only accessible by owner
160
+ chmod 600 /etc/agent/*.yaml # Config files only readable/writable by owner
161
+ ```
162
+
docs/PRODUCTION_GUIDE_CN.md ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agent 生产环境指南
2
+
3
+ > 从 Demo 到生产环境的实践指南
4
+
5
+ ## 目录
6
+
7
+ - [1. Demo功能实现](#1-demo功能)
8
+ - [2. 可升级方向](#2-可升级方向)
9
+ - [3. 生产部署](#3-生产部署)
10
+
11
+ ---
12
+
13
+ ## 1. Demo 功能概述
14
+
15
+ 本项目是一个**教学级 Demo**,旨在演示 Agent 的核心概念与基本执行流程。要在生产环境中使用,还需要解决一系列复杂问题。
16
+
17
+ ### Demo 实现的功能
18
+
19
+ | 功能 | Demo 实现 |
20
+ | --------------------- | --------------------------- |
21
+ | **上下文管理** | ✅ 通过 `SessionNoteTool` 以文件形式实现了简单的上下文持久化;当接近上下文窗口上限时,会进行简单的摘要处理。 |
22
+ | **工具调用** | ✅ 提供了基础的 Read/Write/Edit/Bash 工具。 |
23
+ | **错误处理** | ✅ 实现了基础的异常捕获机制。 |
24
+ | **日志** | ✅ 使用简单的 `print` 函数输出日志。 |
25
+
26
+
27
+ ## 2. 升级与拓展方向
28
+
29
+ ### 2.1 高级上下文管理
30
+
31
+ - **引入分布式文件系统**:对上下文进行统一的持久化管理和备份。
32
+ - **优化 Token 计算**:使用更精确的方式计算 Token 数量。
33
+ - **丰富消息压缩策略**:引入更丰富的消息压缩策略,例如保留最近 N 条消息、保留核心元信息、优化摘要 Prompt,或集成召回系统等。
34
+
35
+ ### 2.2 模型回退机制
36
+
37
+ 当前 Demo 固定使用单一模型(MiniMax-M2.1),调用失败时会直接报错。
38
+
39
+ - **建立模型池**:配置多个模型账号,建立模型池以提高服务可用性。
40
+ - **引入高可用策略**:为模型池引入自动健康检测、故障节点切换、熔断等高可用策略。
41
+
42
+ ### 2.3 模型幻觉的检测与修正
43
+
44
+ 当前 Demo 完全信任模型输出,缺乏验证机制。
45
+
46
+ - **输入参数安全检查**:对部分工具的调用参数进行安全性检查,防止执行高危操作。
47
+ - **输出结果合理性检查**:对部分工具的调用结果进行反思(Self-reflection),检查其合理性。
48
+
49
+ ## 3. 生产环境部署
50
+
51
+ ### 3.1 容器化部署建议
52
+
53
+ 我们推荐使用 Kubernetes 或 Docker 环境来部署 Agent。容器化部署具有以下优势:
54
+
55
+ - **资源隔离**:每个 Agent 实例运行在独立的容器中,互不干扰。
56
+ - **弹性扩展**:根据负载自动调整实例数量。
57
+ - **版本管理**:便于快速回滚和灰度发布。
58
+ - **环境一致性**:开发、测试、生产环境完全一致。
59
+
60
+ ### 3.2 资源限制
61
+
62
+ #### 3.2.1 CPU 与内存限制
63
+
64
+ 为防止 Agent 实例占用过多资源而影响宿主机,您必须为其设置 CPU 和内存的限制:
65
+
66
+ **Docker 配置示例**:
67
+ ```yaml
68
+ # docker-compose.yml
69
+ services:
70
+ agent:
71
+ image: agent-demo:latest
72
+ deploy:
73
+ resources:
74
+ limits:
75
+ cpus: '2.0' # 最多使用 2 个 CPU 核心
76
+ memory: 2G # 最多使用 2GB 内存
77
+ reservations:
78
+ cpus: '0.5' # 保证至少 0.5 个核心
79
+ memory: 512M # 保证至少 512MB
80
+ ```
81
+
82
+ #### 3.2.2 磁盘限制
83
+
84
+ Agent 运行过程中可能会产生大量的临时文件和日志,因此需要限制其磁盘使用量:
85
+
86
+ **Docker Volume 配置**:
87
+ ```yaml
88
+ # docker-compose.yml
89
+ services:
90
+ agent:
91
+ volumes:
92
+ - type: tmpfs
93
+ target: /tmp
94
+ tmpfs:
95
+ size: 1G # 临时文件最多 1GB
96
+ - type: volume
97
+ source: agent-data
98
+ target: /app/data
99
+ volume:
100
+ driver_opts:
101
+ size: 5G # 数据卷最多 5GB
102
+ ```
103
+
104
+
105
+ ### 3.3 Linux 账户权限限制
106
+
107
+ #### 3.3.1 最小权限原则
108
+
109
+ **请勿使用 root 用户运行 Agent**,这会带来严重的安全风险。
110
+
111
+ **Dockerfile 最佳实践**:
112
+ ```dockerfile
113
+ FROM python:3.11-slim
114
+
115
+ # 安装必要的系统工具
116
+ RUN apt-get update && apt-get install -y \
117
+ git \
118
+ curl \
119
+ && rm -rf /var/lib/apt/lists/*
120
+
121
+ # 安装 uv
122
+ RUN curl -LsSf https://astral.sh/uv/install.sh | sh
123
+ ENV PATH="/root/.cargo/bin:$PATH"
124
+
125
+ # 创建非特权用户
126
+ RUN groupadd -r agent && useradd -r -g agent agent
127
+
128
+ # 设置工作目录
129
+ WORKDIR /app
130
+
131
+ # 方案1:从 Git 仓库克隆(适用于公开仓库)
132
+ RUN git clone https://github.com/MiniMax-AI/agent-demo.git . && \
133
+ chown -R agent:agent /app
134
+
135
+ # 方案2:从本地复制代码(适用于私有部署)
136
+ # COPY --chown=agent:agent . /app
137
+
138
+ # 切换到非特权用户后安装依赖
139
+ USER agent
140
+
141
+ # 使用 uv 同步依赖
142
+ RUN uv sync
143
+
144
+ # 启动应用
145
+ CMD ["uv", "run", "mini-agent"]
146
+ ```
147
+
148
+ #### 3.3.2 文件系统权限
149
+
150
+ 您应限制 Agent 只能访问必要的目录:
151
+
152
+ ```bash
153
+ # 创建受限的工作目录
154
+ mkdir -p /app/workspace
155
+ chown agent:agent /app/workspace
156
+ chmod 750 /app/workspace # 所有者读写执行,组只读执行
157
+
158
+ # 限制敏感目录的访问
159
+ chmod 700 /etc/agent # 配置目录只有所有者能访问
160
+ chmod 600 /etc/agent/*.yaml # 配置文件只有所有者能读写
161
+ ```
docs/assets/demo1-task-execution.gif ADDED

Git LFS Details

  • SHA256: 597a8154cd3848564e718d3464b334daa47c4f3f08d6d7b1609c9d35653c82ce
  • Pointer size: 131 Bytes
  • Size of remote file: 290 kB
docs/assets/demo2-claude-skill.gif ADDED

Git LFS Details

  • SHA256: fab77c7ca470b975cebca45350ed921025ec964133c18325e3268522cdabb7e2
  • Pointer size: 131 Bytes
  • Size of remote file: 282 kB
docs/assets/demo3-web-search.gif ADDED

Git LFS Details

  • SHA256: c9db84ba061b738df834bf53ab3c6671a284e07e26a7bdbfa4f1f1e35ece42ad
  • Pointer size: 131 Bytes
  • Size of remote file: 443 kB
examples/01_basic_tools.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Example 1: Basic Tools Usage
2
+
3
+ This example demonstrates how to use the basic tools:
4
+ - ReadTool: Read file contents
5
+ - WriteTool: Create new files
6
+ - EditTool: Edit existing files
7
+ - BashTool: Execute bash commands
8
+
9
+ Based on: tests/test_tools.py
10
+ """
11
+
12
+ import asyncio
13
+ import tempfile
14
+ from pathlib import Path
15
+
16
+ from mini_agent.tools import BashTool, EditTool, ReadTool, WriteTool
17
+
18
+
19
+ async def demo_write_tool():
20
+ """Demo: Write a new file."""
21
+ print("\n" + "=" * 60)
22
+ print("Demo 1: WriteTool - Create a new file")
23
+ print("=" * 60)
24
+
25
+ with tempfile.TemporaryDirectory() as tmpdir:
26
+ file_path = Path(tmpdir) / "hello.txt"
27
+
28
+ tool = WriteTool()
29
+ result = await tool.execute(
30
+ path=str(file_path), content="Hello, Mini Agent!\nThis is a test file."
31
+ )
32
+
33
+ if result.success:
34
+ print(f"✅ File created: {file_path}")
35
+ print(f"Content:\n{file_path.read_text()}")
36
+ else:
37
+ print(f"❌ Failed: {result.error}")
38
+
39
+
40
+ async def demo_read_tool():
41
+ """Demo: Read a file."""
42
+ print("\n" + "=" * 60)
43
+ print("Demo 2: ReadTool - Read file contents")
44
+ print("=" * 60)
45
+
46
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as f:
47
+ f.write("Line 1: Hello\nLine 2: World\nLine 3: Mini Agent")
48
+ temp_path = f.name
49
+
50
+ try:
51
+ tool = ReadTool()
52
+ result = await tool.execute(path=temp_path)
53
+
54
+ if result.success:
55
+ print(f"✅ File read successfully")
56
+ print(f"Content:\n{result.content}")
57
+ else:
58
+ print(f"❌ Failed: {result.error}")
59
+ finally:
60
+ Path(temp_path).unlink()
61
+
62
+
63
+ async def demo_edit_tool():
64
+ """Demo: Edit an existing file."""
65
+ print("\n" + "=" * 60)
66
+ print("Demo 3: EditTool - Edit file content")
67
+ print("=" * 60)
68
+
69
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as f:
70
+ f.write("Python is great!\nI love Python programming.")
71
+ temp_path = f.name
72
+
73
+ try:
74
+ print(f"Original content:\n{Path(temp_path).read_text()}\n")
75
+
76
+ tool = EditTool()
77
+ result = await tool.execute(
78
+ path=temp_path, old_str="Python", new_str="Agent"
79
+ )
80
+
81
+ if result.success:
82
+ print(f"✅ File edited successfully")
83
+ print(f"New content:\n{Path(temp_path).read_text()}")
84
+ else:
85
+ print(f"❌ Failed: {result.error}")
86
+ finally:
87
+ Path(temp_path).unlink()
88
+
89
+
90
+ async def demo_bash_tool():
91
+ """Demo: Execute bash commands."""
92
+ print("\n" + "=" * 60)
93
+ print("Demo 4: BashTool - Execute bash commands")
94
+ print("=" * 60)
95
+
96
+ tool = BashTool()
97
+
98
+ # Example 1: List files
99
+ print("\nCommand: ls -la")
100
+ result = await tool.execute(command="ls -la")
101
+ if result.success:
102
+ print(f"✅ Command executed successfully")
103
+ print(f"Output:\n{result.content[:200]}...")
104
+
105
+ # Example 2: Get current directory
106
+ print("\nCommand: pwd")
107
+ result = await tool.execute(command="pwd")
108
+ if result.success:
109
+ print(f"✅ Current directory: {result.content.strip()}")
110
+
111
+ # Example 3: Echo
112
+ print("\nCommand: echo 'Hello from BashTool!'")
113
+ result = await tool.execute(command="echo 'Hello from BashTool!'")
114
+ if result.success:
115
+ print(f"✅ Output: {result.content.strip()}")
116
+
117
+
118
+ async def main():
119
+ """Run all demos."""
120
+ print("=" * 60)
121
+ print("Basic Tools Usage Examples")
122
+ print("=" * 60)
123
+ print("\nThese examples show how to use the core tools directly.")
124
+ print("In a real agent scenario, the LLM decides which tools to use.\n")
125
+
126
+ await demo_write_tool()
127
+ await demo_read_tool()
128
+ await demo_edit_tool()
129
+ await demo_bash_tool()
130
+
131
+ print("\n" + "=" * 60)
132
+ print("All demos completed! ✅")
133
+ print("=" * 60)
134
+
135
+
136
+ if __name__ == "__main__":
137
+ asyncio.run(main())
examples/02_simple_agent.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Example 2: Simple Agent Usage
2
+
3
+ This example demonstrates how to create and run a basic agent
4
+ to perform simple file operations.
5
+
6
+ Based on: tests/test_agent.py
7
+ """
8
+
9
+ import asyncio
10
+ import tempfile
11
+ from pathlib import Path
12
+
13
+ from mini_agent import LLMClient
14
+ from mini_agent.agent import Agent
15
+ from mini_agent.config import Config
16
+ from mini_agent.tools import BashTool, EditTool, ReadTool, WriteTool
17
+
18
+
19
+ async def demo_file_creation():
20
+ """Demo: Agent creates a file based on user request."""
21
+ print("\n" + "=" * 60)
22
+ print("Demo: Agent-Driven File Creation")
23
+ print("=" * 60)
24
+
25
+ # Load configuration
26
+ config_path = Path("mini_agent/config/config.yaml")
27
+ if not config_path.exists():
28
+ print("❌ config.yaml not found. Please set up your API key first.")
29
+ print(" Run: cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml")
30
+ return
31
+
32
+ config = Config.from_yaml(config_path)
33
+
34
+ # Check API key
35
+ if not config.llm.api_key or config.llm.api_key.startswith("YOUR_"):
36
+ print("❌ API key not configured in config.yaml")
37
+ return
38
+
39
+ # Create temporary workspace
40
+ with tempfile.TemporaryDirectory() as workspace_dir:
41
+ print(f"📁 Workspace: {workspace_dir}\n")
42
+
43
+ # Load system prompt (Agent will auto-inject workspace info)
44
+ system_prompt_path = Path("mini_agent/config/system_prompt.md")
45
+ if system_prompt_path.exists():
46
+ system_prompt = system_prompt_path.read_text(encoding="utf-8")
47
+ else:
48
+ system_prompt = "You are a helpful AI assistant that can use tools."
49
+
50
+ # Initialize LLM client
51
+ llm_client = LLMClient(
52
+ api_key=config.llm.api_key,
53
+ api_base=config.llm.api_base,
54
+ model=config.llm.model,
55
+ )
56
+
57
+ # Initialize tools
58
+ tools = [
59
+ ReadTool(workspace_dir=workspace_dir),
60
+ WriteTool(workspace_dir=workspace_dir),
61
+ EditTool(workspace_dir=workspace_dir),
62
+ BashTool(),
63
+ ]
64
+
65
+ # Create agent
66
+ agent = Agent(
67
+ llm_client=llm_client,
68
+ system_prompt=system_prompt,
69
+ tools=tools,
70
+ max_steps=10,
71
+ workspace_dir=workspace_dir,
72
+ )
73
+
74
+ # Task: Create a Python hello world file
75
+ task = """
76
+ Create a Python file named 'hello.py' that:
77
+ 1. Defines a function called greet(name)
78
+ 2. The function prints "Hello, {name}!"
79
+ 3. Calls the function with name="Mini Agent"
80
+ """
81
+
82
+ print("📝 Task:")
83
+ print(task)
84
+ print("\n" + "=" * 60)
85
+ print("🤖 Agent is working...\n")
86
+
87
+ agent.add_user_message(task)
88
+
89
+ try:
90
+ result = await agent.run()
91
+
92
+ print("\n" + "=" * 60)
93
+ print("✅ Agent completed the task!")
94
+ print("=" * 60)
95
+ print(f"\nAgent's response:\n{result}\n")
96
+
97
+ # Check if file was created
98
+ hello_file = Path(workspace_dir) / "hello.py"
99
+ if hello_file.exists():
100
+ print("=" * 60)
101
+ print("📄 Created file content:")
102
+ print("=" * 60)
103
+ print(hello_file.read_text())
104
+ print("=" * 60)
105
+ else:
106
+ print("⚠️ File was not created (but agent may have completed differently)")
107
+
108
+ except Exception as e:
109
+ print(f"❌ Error: {e}")
110
+ import traceback
111
+
112
+ traceback.print_exc()
113
+
114
+
115
+ async def demo_bash_task():
116
+ """Demo: Agent executes bash commands."""
117
+ print("\n" + "=" * 60)
118
+ print("Demo: Agent-Driven Bash Commands")
119
+ print("=" * 60)
120
+
121
+ # Load configuration
122
+ config_path = Path("mini_agent/config/config.yaml")
123
+ if not config_path.exists():
124
+ print("❌ config.yaml not found")
125
+ return
126
+
127
+ config = Config.from_yaml(config_path)
128
+
129
+ if not config.llm.api_key or config.llm.api_key.startswith("YOUR_"):
130
+ print("❌ API key not configured")
131
+ return
132
+
133
+ with tempfile.TemporaryDirectory() as workspace_dir:
134
+ print(f"📁 Workspace: {workspace_dir}\n")
135
+
136
+ # Load system prompt (Agent will auto-inject workspace info)
137
+ system_prompt_path = Path("mini_agent/config/system_prompt.md")
138
+ if system_prompt_path.exists():
139
+ system_prompt = system_prompt_path.read_text(encoding="utf-8")
140
+ else:
141
+ system_prompt = "You are a helpful AI assistant that can use tools."
142
+
143
+ # Initialize LLM
144
+ llm_client = LLMClient(
145
+ api_key=config.llm.api_key,
146
+ api_base=config.llm.api_base,
147
+ model=config.llm.model,
148
+ )
149
+
150
+ # Tools
151
+ tools = [
152
+ ReadTool(workspace_dir=workspace_dir),
153
+ WriteTool(workspace_dir=workspace_dir),
154
+ BashTool(),
155
+ ]
156
+
157
+ # Create agent
158
+ agent = Agent(
159
+ llm_client=llm_client,
160
+ system_prompt=system_prompt,
161
+ tools=tools,
162
+ max_steps=10,
163
+ workspace_dir=workspace_dir,
164
+ )
165
+
166
+ # Task: Use bash to get system info
167
+ task = """
168
+ Use bash commands to:
169
+ 1. Show the current date and time
170
+ 2. List all Python files in the current directory
171
+ 3. Count how many Python files exist
172
+ """
173
+
174
+ print("📝 Task:")
175
+ print(task)
176
+ print("\n" + "=" * 60)
177
+ print("🤖 Agent is working...\n")
178
+
179
+ agent.add_user_message(task)
180
+
181
+ try:
182
+ result = await agent.run()
183
+
184
+ print("\n" + "=" * 60)
185
+ print("✅ Agent completed!")
186
+ print("=" * 60)
187
+ print(f"\nAgent's response:\n{result}\n")
188
+
189
+ except Exception as e:
190
+ print(f"❌ Error: {e}")
191
+
192
+
193
+ async def main():
194
+ """Run all demos."""
195
+ print("=" * 60)
196
+ print("Simple Agent Usage Examples")
197
+ print("=" * 60)
198
+ print("\nThese examples show how to create an agent and give it tasks.")
199
+ print("The agent uses LLM to decide which tools to call.\n")
200
+
201
+ # Run demos
202
+ await demo_file_creation()
203
+ print("\n" * 2)
204
+ await demo_bash_task()
205
+
206
+ print("\n" + "=" * 60)
207
+ print("All demos completed! ✅")
208
+ print("=" * 60)
209
+
210
+
211
+ if __name__ == "__main__":
212
+ asyncio.run(main())
examples/03_session_notes.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Example 3: Session Note Tool Usage
2
+
3
+ This example demonstrates the Session Note Tool - one of the core features
4
+ that allows agents to maintain memory across sessions.
5
+
6
+ Based on: tests/test_note_tool.py, tests/test_integration.py
7
+ """
8
+
9
+ import asyncio
10
+ import json
11
+ import tempfile
12
+ from pathlib import Path
13
+
14
+ from mini_agent import LLMClient
15
+ from mini_agent.agent import Agent
16
+ from mini_agent.config import Config
17
+ from mini_agent.tools import BashTool, ReadTool, WriteTool
18
+ from mini_agent.tools.note_tool import RecallNoteTool, SessionNoteTool
19
+
20
+
21
+ async def demo_direct_note_usage():
22
+ """Demo: Direct usage of Session Note tools."""
23
+ print("\n" + "=" * 60)
24
+ print("Demo 1: Direct Session Note Tool Usage")
25
+ print("=" * 60)
26
+
27
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as f:
28
+ note_file = f.name
29
+
30
+ try:
31
+ # Create tools
32
+ record_tool = SessionNoteTool(memory_file=note_file)
33
+ recall_tool = RecallNoteTool(memory_file=note_file)
34
+
35
+ # Record some notes
36
+ print("\n📝 Recording notes...")
37
+
38
+ result = await record_tool.execute(
39
+ content="User is a Python developer working on agent systems",
40
+ category="user_info",
41
+ )
42
+ print(f" ✓ {result.content}")
43
+
44
+ result = await record_tool.execute(
45
+ content="Project name: mini-agent, Tech: Python 3.12 + async",
46
+ category="project_info",
47
+ )
48
+ print(f" ✓ {result.content}")
49
+
50
+ result = await record_tool.execute(
51
+ content="User prefers concise, well-documented code",
52
+ category="user_preference",
53
+ )
54
+ print(f" ✓ {result.content}")
55
+
56
+ # Recall all notes
57
+ print("\n🔍 Recalling all notes...")
58
+ result = await recall_tool.execute()
59
+ print(result.content)
60
+
61
+ # Recall filtered notes
62
+ print("\n🔍 Recalling user preferences only...")
63
+ result = await recall_tool.execute(category="user_preference")
64
+ print(result.content)
65
+
66
+ # Show the memory file
67
+ print("\n📄 Memory file content:")
68
+ print("=" * 60)
69
+ notes = json.loads(Path(note_file).read_text())
70
+ print(json.dumps(notes, indent=2, ensure_ascii=False))
71
+ print("=" * 60)
72
+
73
+ finally:
74
+ Path(note_file).unlink(missing_ok=True)
75
+
76
+
77
+ async def demo_agent_with_notes():
78
+ """Demo: Agent using Session Notes to remember context."""
79
+ print("\n" + "=" * 60)
80
+ print("Demo 2: Agent with Session Memory")
81
+ print("=" * 60)
82
+
83
+ # Load configuration
84
+ config_path = Path("mini_agent/config/config.yaml")
85
+ if not config_path.exists():
86
+ print("❌ config.yaml not found")
87
+ return
88
+
89
+ config = Config.from_yaml(config_path)
90
+
91
+ if not config.llm.api_key or config.llm.api_key.startswith("YOUR_"):
92
+ print("❌ API key not configured")
93
+ return
94
+
95
+ with tempfile.TemporaryDirectory() as workspace_dir:
96
+ print(f"📁 Workspace: {workspace_dir}\n")
97
+
98
+ # Load system prompt (Agent will auto-inject workspace info)
99
+ system_prompt_path = Path("mini_agent/config/system_prompt.md")
100
+ if system_prompt_path.exists():
101
+ system_prompt = system_prompt_path.read_text(encoding="utf-8")
102
+ else:
103
+ system_prompt = "You are a helpful AI assistant."
104
+
105
+ # Add Session Note instructions
106
+ note_instructions = """
107
+
108
+ IMPORTANT - Session Note Management:
109
+ You have access to record_note and recall_notes tools. Use them to:
110
+ - record_note: Save important facts, preferences, decisions that should persist
111
+ - recall_notes: Retrieve previously saved notes
112
+
113
+ Guidelines:
114
+ - Proactively record key information during conversations
115
+ - Recall notes at the start to restore context
116
+ - Categories: user_info, user_preference, project_info, decision, etc.
117
+ """
118
+ system_prompt += note_instructions
119
+
120
+ # Initialize LLM
121
+ llm_client = LLMClient(
122
+ api_key=config.llm.api_key,
123
+ api_base=config.llm.api_base,
124
+ model=config.llm.model,
125
+ )
126
+
127
+ # Memory file
128
+ memory_file = Path(workspace_dir) / ".agent_memory.json"
129
+
130
+ # Tools including Session Note tools
131
+ tools = [
132
+ ReadTool(workspace_dir=workspace_dir),
133
+ WriteTool(workspace_dir=workspace_dir),
134
+ BashTool(),
135
+ SessionNoteTool(memory_file=str(memory_file)),
136
+ RecallNoteTool(memory_file=str(memory_file)),
137
+ ]
138
+
139
+ # === First Session ===
140
+ print("=" * 60)
141
+ print("Session 1: Teaching the agent about user preferences")
142
+ print("=" * 60)
143
+
144
+ agent1 = Agent(
145
+ llm_client=llm_client,
146
+ system_prompt=system_prompt,
147
+ tools=tools,
148
+ max_steps=15,
149
+ workspace_dir=workspace_dir,
150
+ )
151
+
152
+ task1 = """
153
+ Hello! Let me introduce myself:
154
+ - I'm Alex, a senior Python developer
155
+ - I'm building an AI agent framework called "mini-agent"
156
+ - I use Python 3.12 with asyncio
157
+ - I prefer type hints and comprehensive docstrings
158
+ - My coding style: clean, functional, well-tested
159
+
160
+ Please remember this information for future conversations.
161
+ Also, create a simple README.md file acknowledging you understood.
162
+ """
163
+
164
+ print(f"\n📝 User message:\n{task1}\n")
165
+ print("🤖 Agent is working...\n")
166
+
167
+ agent1.add_user_message(task1)
168
+
169
+ try:
170
+ result1 = await agent1.run()
171
+ print("\n" + "=" * 60)
172
+ print("Agent response:")
173
+ print("=" * 60)
174
+ print(result1)
175
+ print("=" * 60)
176
+
177
+ # Check memory file
178
+ if memory_file.exists():
179
+ notes = json.loads(memory_file.read_text())
180
+ print(f"\n✅ Agent recorded {len(notes)} notes in memory")
181
+ for note in notes:
182
+ print(f" - [{note['category']}] {note['content'][:50]}...")
183
+ else:
184
+ print("\n⚠️ No notes found")
185
+
186
+ except Exception as e:
187
+ print(f"❌ Error: {e}")
188
+ return
189
+
190
+ # === Second Session (New Agent Instance) ===
191
+ print("\n\n" + "=" * 60)
192
+ print("Session 2: New agent instance (simulating new conversation)")
193
+ print("=" * 60)
194
+
195
+ agent2 = Agent(
196
+ llm_client=llm_client,
197
+ system_prompt=system_prompt,
198
+ tools=tools,
199
+ max_steps=10,
200
+ workspace_dir=workspace_dir,
201
+ )
202
+
203
+ task2 = """
204
+ Hello! I'm back. Do you remember who I am and what project I'm working on?
205
+ What were my code style preferences?
206
+ """
207
+
208
+ print(f"\n📝 User message:\n{task2}\n")
209
+ print("🤖 Agent is working (should recall previous notes)...\n")
210
+
211
+ agent2.add_user_message(task2)
212
+
213
+ try:
214
+ result2 = await agent2.run()
215
+ print("\n" + "=" * 60)
216
+ print("Agent response:")
217
+ print("=" * 60)
218
+ print(result2)
219
+ print("=" * 60)
220
+
221
+ print("\n✅ Session Note Demo completed!")
222
+ print("\nKey Points:")
223
+ print(" 1. Agent in Session 1 recorded important information")
224
+ print(" 2. Agent in Session 2 recalled previous notes")
225
+ print(" 3. Memory persists across agent instances via file")
226
+
227
+ except Exception as e:
228
+ print(f"❌ Error: {e}")
229
+
230
+
231
+ async def main():
232
+ """Run all demos."""
233
+ print("=" * 60)
234
+ print("Session Note Tool Examples")
235
+ print("=" * 60)
236
+ print("\nSession Notes allow agents to remember context across sessions.")
237
+ print("This is a key feature for building production-ready agents.\n")
238
+
239
+ # Run demos
240
+ await demo_direct_note_usage()
241
+ print("\n" * 2)
242
+ await demo_agent_with_notes()
243
+
244
+ print("\n" + "=" * 60)
245
+ print("All demos completed! ✅")
246
+ print("=" * 60)
247
+
248
+
249
+ if __name__ == "__main__":
250
+ asyncio.run(main())
examples/04_full_agent.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Example 4: Full Agent with All Features
2
+
3
+ This example demonstrates a complete agent setup with:
4
+ - All basic tools (Read, Write, Edit, Bash)
5
+ - Session Note tools for memory
6
+ - MCP tools (Memory, Search, etc.)
7
+ - Skills integration
8
+
9
+ Based on: tests/test_integration.py
10
+ """
11
+
12
+ import asyncio
13
+ import tempfile
14
+ from pathlib import Path
15
+
16
+ from mini_agent import LLMClient
17
+ from mini_agent.agent import Agent
18
+ from mini_agent.config import Config
19
+ from mini_agent.tools import BashTool, EditTool, ReadTool, WriteTool
20
+ from mini_agent.tools.mcp_loader import load_mcp_tools_async
21
+ from mini_agent.tools.note_tool import RecallNoteTool, SessionNoteTool
22
+
23
+
24
+ async def demo_full_agent():
25
+ """Demo: Full-featured agent with all capabilities."""
26
+ print("\n" + "=" * 60)
27
+ print("Full Mini Agent - All Features Enabled")
28
+ print("=" * 60)
29
+
30
+ # Load configuration
31
+ config_path = Path("mini_agent/config/config.yaml")
32
+ if not config_path.exists():
33
+ print("❌ config.yaml not found. Please run:")
34
+ print(" cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml")
35
+ return
36
+
37
+ config = Config.from_yaml(config_path)
38
+
39
+ # Check API key
40
+ if not config.llm.api_key or config.llm.api_key.startswith("YOUR_"):
41
+ print("❌ API key not configured in config.yaml")
42
+ return
43
+
44
+ # Create workspace
45
+ with tempfile.TemporaryDirectory() as workspace_dir:
46
+ print(f"📁 Workspace: {workspace_dir}")
47
+
48
+ # Load system prompt (Agent will auto-inject workspace info)
49
+ system_prompt_path = Path("mini_agent/config/system_prompt.md")
50
+ if system_prompt_path.exists():
51
+ system_prompt = system_prompt_path.read_text(encoding="utf-8")
52
+ else:
53
+ system_prompt = "You are a helpful AI assistant."
54
+
55
+ # Add Session Note instructions
56
+ note_instructions = """
57
+
58
+ IMPORTANT - Session Memory:
59
+ You have record_note and recall_notes tools. Use them to:
60
+ - Save important facts, decisions, and context
61
+ - Recall previous information across conversations
62
+ """
63
+ system_prompt += note_instructions
64
+
65
+ # Initialize LLM
66
+ llm_client = LLMClient(
67
+ api_key=config.llm.api_key,
68
+ api_base=config.llm.api_base,
69
+ model=config.llm.model,
70
+ )
71
+
72
+ # Initialize basic tools
73
+ tools = [
74
+ ReadTool(workspace_dir=workspace_dir),
75
+ WriteTool(workspace_dir=workspace_dir),
76
+ EditTool(workspace_dir=workspace_dir),
77
+ BashTool(),
78
+ ]
79
+ print("✓ Loaded 4 basic tools")
80
+
81
+ # Add Session Note tools
82
+ memory_file = Path(workspace_dir) / ".agent_memory.json"
83
+ tools.extend(
84
+ [
85
+ SessionNoteTool(memory_file=str(memory_file)),
86
+ RecallNoteTool(memory_file=str(memory_file)),
87
+ ]
88
+ )
89
+ print("✓ Loaded 2 Session Note tools")
90
+
91
+ # Load MCP tools (if configured)
92
+ try:
93
+ mcp_tools = await load_mcp_tools_async(config_path="mini_agent/config/mcp.json")
94
+ if mcp_tools:
95
+ tools.extend(mcp_tools)
96
+ print(f"✓ Loaded {len(mcp_tools)} MCP tools")
97
+ else:
98
+ print("⚠️ No MCP tools configured (mcp.json is empty or disabled)")
99
+ except Exception as e:
100
+ print(f"⚠️ MCP tools not loaded: {e}")
101
+
102
+ # Create agent
103
+ agent = Agent(
104
+ llm_client=llm_client,
105
+ system_prompt=system_prompt,
106
+ tools=tools,
107
+ max_steps=config.agent.max_steps,
108
+ workspace_dir=workspace_dir,
109
+ )
110
+
111
+ print(f"\n🤖 Agent created with {len(tools)} total tools\n")
112
+
113
+ # Task: Complex task that uses multiple tools
114
+ task = """
115
+ Please help me with the following tasks:
116
+
117
+ 1. Create a Python script called 'calculator.py' that:
118
+ - Has functions for add, subtract, multiply, divide
119
+ - Has a main() function that demonstrates usage
120
+ - Includes proper docstrings and type hints
121
+
122
+ 2. Create a README.md file that:
123
+ - Describes the calculator script
124
+ - Shows how to run it
125
+ - Lists the available functions
126
+
127
+ 3. Test the calculator by running it with bash
128
+
129
+ 4. Remember this project info:
130
+ - Project: Simple Calculator
131
+ - Language: Python
132
+ - Purpose: Demonstration of agent capabilities
133
+ """
134
+
135
+ print("=" * 60)
136
+ print("📝 Task:")
137
+ print("=" * 60)
138
+ print(task)
139
+ print("\n" + "=" * 60)
140
+ print("🤖 Agent is working...\n")
141
+
142
+ agent.add_user_message(task)
143
+
144
+ try:
145
+ result = await agent.run()
146
+
147
+ print("\n" + "=" * 60)
148
+ print("✅ Agent completed!")
149
+ print("=" * 60)
150
+ print(f"\nAgent's final response:\n{result}\n")
151
+
152
+ # Show created files
153
+ print("=" * 60)
154
+ print("��� Created files in workspace:")
155
+ print("=" * 60)
156
+
157
+ workspace = Path(workspace_dir)
158
+ for file in workspace.glob("*"):
159
+ if file.is_file() and not file.name.startswith("."):
160
+ print(f"\n📄 {file.name}:")
161
+ print("-" * 60)
162
+ content = file.read_text()
163
+ # Show first 20 lines
164
+ lines = content.split("\n")[:20]
165
+ print("\n".join(lines))
166
+ if len(content.split("\n")) > 20:
167
+ print("... (truncated)")
168
+ print("-" * 60)
169
+
170
+ # Show memory
171
+ if memory_file.exists():
172
+ import json
173
+
174
+ notes = json.loads(memory_file.read_text())
175
+ print(f"\n💾 Session notes recorded: {len(notes)}")
176
+ for note in notes:
177
+ print(f" - [{note['category']}] {note['content'][:60]}...")
178
+
179
+ except Exception as e:
180
+ print(f"❌ Error during agent execution: {e}")
181
+ import traceback
182
+
183
+ traceback.print_exc()
184
+
185
+
186
+ async def demo_interactive_mode():
187
+ """Demo: Interactive conversation with agent."""
188
+ print("\n" + "=" * 60)
189
+ print("Interactive Mini Agent")
190
+ print("=" * 60)
191
+ print("\nThis demo shows multi-turn conversation.")
192
+ print("(In production, use `mini-agent` for full interactive mode)")
193
+
194
+ # Load config
195
+ config_path = Path("mini_agent/config/config.yaml")
196
+ if not config_path.exists():
197
+ print("❌ config.yaml not found")
198
+ return
199
+
200
+ config = Config.from_yaml(config_path)
201
+
202
+ if not config.llm.api_key or config.llm.api_key.startswith("YOUR_"):
203
+ print("❌ API key not configured")
204
+ return
205
+
206
+ with tempfile.TemporaryDirectory() as workspace_dir:
207
+ # Setup
208
+ system_prompt = "You are a helpful assistant with access to tools."
209
+ llm_client = LLMClient(
210
+ api_key=config.llm.api_key,
211
+ api_base=config.llm.api_base,
212
+ model=config.llm.model,
213
+ )
214
+
215
+ tools = [
216
+ WriteTool(workspace_dir=workspace_dir),
217
+ ReadTool(workspace_dir=workspace_dir),
218
+ BashTool(),
219
+ ]
220
+
221
+ agent = Agent(
222
+ llm_client=llm_client,
223
+ system_prompt=system_prompt,
224
+ tools=tools,
225
+ max_steps=20,
226
+ workspace_dir=workspace_dir,
227
+ )
228
+
229
+ # Conversation turns
230
+ conversations = [
231
+ "Create a file called 'data.txt' with the numbers 1 to 5, one per line.",
232
+ "Now read the file and tell me what's in it.",
233
+ "Count how many lines are in the file using bash.",
234
+ ]
235
+
236
+ for i, message in enumerate(conversations, 1):
237
+ print(f"\n{'=' * 60}")
238
+ print(f"Turn {i}:")
239
+ print(f"{'=' * 60}")
240
+ print(f"User: {message}\n")
241
+
242
+ agent.add_user_message(message)
243
+
244
+ try:
245
+ result = await agent.run()
246
+ print(f"Agent: {result}\n")
247
+ except Exception as e:
248
+ print(f"Error: {e}")
249
+ break
250
+
251
+
252
+ async def main():
253
+ """Run all demos."""
254
+ print("=" * 60)
255
+ print("Full Agent Examples")
256
+ print("=" * 60)
257
+ print("\nThese examples demonstrate the complete agent capabilities:")
258
+ print("- All basic tools (file operations, bash)")
259
+ print("- Session memory (persistent notes)")
260
+ print("- MCP tools (if configured)")
261
+ print("- Multi-turn conversations\n")
262
+
263
+ # Run demos
264
+ await demo_full_agent()
265
+ print("\n" * 2)
266
+ await demo_interactive_mode()
267
+
268
+ print("\n" + "=" * 60)
269
+ print("All demos completed! ✅")
270
+ print("=" * 60)
271
+ print("\n💡 Next step: Try the interactive mode with:")
272
+ print(" mini-agent\n")
273
+
274
+
275
+ if __name__ == "__main__":
276
+ asyncio.run(main())
examples/05_provider_selection.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Example: Using LLMClient with different providers.
2
+
3
+ This example demonstrates how to use the LLMClient wrapper with different
4
+ LLM providers (Anthropic or OpenAI) through the provider parameter.
5
+ """
6
+
7
+ import asyncio
8
+ import os
9
+ from pathlib import Path
10
+
11
+ import yaml
12
+
13
+ from mini_agent import LLMClient, LLMProvider, Message
14
+
15
+
16
+ async def demo_anthropic_provider():
17
+ """Demo using LLMClient with Anthropic provider."""
18
+ print("\n" + "=" * 60)
19
+ print("DEMO: LLMClient with Anthropic Provider")
20
+ print("=" * 60)
21
+
22
+ # Load config
23
+ config_path = Path("mini_agent/config/config.yaml")
24
+ with open(config_path, encoding="utf-8") as f:
25
+ config = yaml.safe_load(f)
26
+
27
+ # Initialize client with Anthropic provider
28
+ client = LLMClient(
29
+ api_key=config["api_key"],
30
+ provider=LLMProvider.ANTHROPIC, # Specify Anthropic provider
31
+ model=config.get("model", "MiniMax-M2.1"),
32
+ )
33
+
34
+ print(f"Provider: {client.provider}")
35
+ print(f"API Base: {client.api_base}")
36
+
37
+ # Simple question
38
+ messages = [Message(role="user", content="Say 'Hello from Anthropic!'")]
39
+ print(f"\n👤 User: {messages[0].content}")
40
+
41
+ try:
42
+ response = await client.generate(messages)
43
+ if response.thinking:
44
+ print(f"💭 Thinking: {response.thinking}")
45
+ print(f"💬 Model: {response.content}")
46
+ print("✅ Anthropic provider demo completed")
47
+ except Exception as e:
48
+ print(f"❌ Error: {e}")
49
+
50
+
51
+ async def demo_openai_provider():
52
+ """Demo using LLMClient with OpenAI provider."""
53
+ print("\n" + "=" * 60)
54
+ print("DEMO: LLMClient with OpenAI Provider")
55
+ print("=" * 60)
56
+
57
+ # Load config
58
+ config_path = Path("mini_agent/config/config.yaml")
59
+ with open(config_path, encoding="utf-8") as f:
60
+ config = yaml.safe_load(f)
61
+
62
+ # Initialize client with OpenAI provider
63
+ client = LLMClient(
64
+ api_key=config["api_key"],
65
+ provider=LLMProvider.OPENAI, # Specify OpenAI provider
66
+ model=config.get("model", "MiniMax-M2.1"),
67
+ )
68
+
69
+ print(f"Provider: {client.provider}")
70
+ print(f"API Base: {client.api_base}")
71
+
72
+ # Simple question
73
+ messages = [Message(role="user", content="Say 'Hello from OpenAI!'")]
74
+ print(f"\n👤 User: {messages[0].content}")
75
+
76
+ try:
77
+ response = await client.generate(messages)
78
+ if response.thinking:
79
+ print(f"💭 Thinking: {response.thinking}")
80
+ print(f"💬 Model: {response.content}")
81
+ print("✅ OpenAI provider demo completed")
82
+ except Exception as e:
83
+ print(f"❌ Error: {e}")
84
+
85
+
86
+ async def demo_default_provider():
87
+ """Demo using LLMClient with default provider."""
88
+ print("\n" + "=" * 60)
89
+ print("DEMO: LLMClient with Default Provider (Anthropic)")
90
+ print("=" * 60)
91
+
92
+ # Load config
93
+ config_path = Path("mini_agent/config/config.yaml")
94
+ with open(config_path, encoding="utf-8") as f:
95
+ config = yaml.safe_load(f)
96
+
97
+ # Initialize client without specifying provider (defaults to Anthropic)
98
+ client = LLMClient(
99
+ api_key=config["api_key"],
100
+ model=config.get("model", "MiniMax-M2.1"),
101
+ )
102
+
103
+ print(f"Provider (default): {client.provider}")
104
+ print(f"API Base: {client.api_base}")
105
+
106
+ # Simple question
107
+ messages = [Message(role="user", content="Say 'Hello with default provider!'")]
108
+ print(f"\n👤 User: {messages[0].content}")
109
+
110
+ try:
111
+ response = await client.generate(messages)
112
+ print(f"💬 Model: {response.content}")
113
+ print("✅ Default provider demo completed")
114
+ except Exception as e:
115
+ print(f"❌ Error: {e}")
116
+
117
+
118
+ async def demo_provider_comparison():
119
+ """Compare responses from both providers."""
120
+ print("\n" + "=" * 60)
121
+ print("DEMO: Provider Comparison")
122
+ print("=" * 60)
123
+
124
+ # Load config
125
+ config_path = Path("mini_agent/config/config.yaml")
126
+ with open(config_path, encoding="utf-8") as f:
127
+ config = yaml.safe_load(f)
128
+
129
+ # Create clients for both providers
130
+ anthropic_client = LLMClient(
131
+ api_key=config["api_key"],
132
+ provider=LLMProvider.ANTHROPIC,
133
+ model=config.get("model", "MiniMax-M2.1"),
134
+ )
135
+
136
+ openai_client = LLMClient(
137
+ api_key=config["api_key"],
138
+ provider=LLMProvider.OPENAI,
139
+ model=config.get("model", "MiniMax-M2.1"),
140
+ )
141
+
142
+ # Same question for both
143
+ messages = [Message(role="user", content="What is 2+2?")]
144
+ print(f"\n👤 Question: {messages[0].content}\n")
145
+
146
+ try:
147
+ # Get response from Anthropic
148
+ anthropic_response = await anthropic_client.generate(messages)
149
+ print(f"🔵 Anthropic: {anthropic_response.content}")
150
+
151
+ # Get response from OpenAI
152
+ openai_response = await openai_client.generate(messages)
153
+ print(f"🟢 OpenAI: {openai_response.content}")
154
+
155
+ print("\n✅ Provider comparison completed")
156
+ except Exception as e:
157
+ print(f"❌ Error: {e}")
158
+
159
+
160
+ async def main():
161
+ """Run all demos."""
162
+ print("\n🚀 LLM Provider Selection Demo")
163
+ print("This demo shows how to use LLMClient with different providers.")
164
+ print("Make sure you have configured API key in config.yaml.")
165
+
166
+ try:
167
+ # Demo default provider
168
+ await demo_default_provider()
169
+
170
+ # Demo Anthropic provider
171
+ await demo_anthropic_provider()
172
+
173
+ # Demo OpenAI provider
174
+ await demo_openai_provider()
175
+
176
+ # Demo provider comparison
177
+ await demo_provider_comparison()
178
+
179
+ print("\n✅ All demos completed successfully!")
180
+
181
+ except Exception as e:
182
+ print(f"\n❌ Error: {e}")
183
+ import traceback
184
+
185
+ traceback.print_exc()
186
+
187
+
188
+ if __name__ == "__main__":
189
+ asyncio.run(main())
190
+
examples/06_tool_schema_demo.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Demo: Using Tool schemas with base Tool class.
2
+
3
+ This example demonstrates how to use the Tool base class and its schema methods.
4
+ """
5
+
6
+ import asyncio
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import yaml
11
+
12
+ from mini_agent import LLMClient, LLMProvider
13
+ from mini_agent.schema import Message
14
+ from mini_agent.tools.base import Tool, ToolResult
15
+
16
+
17
+ def load_config():
18
+ """Load config from config.yaml."""
19
+ config_path = Path("mini_agent/config/config.yaml")
20
+ with open(config_path, encoding="utf-8") as f:
21
+ return yaml.safe_load(f)
22
+
23
+
24
+ class WeatherTool(Tool):
25
+ """Example weather tool."""
26
+
27
+ @property
28
+ def name(self) -> str:
29
+ return "get_weather"
30
+
31
+ @property
32
+ def description(self) -> str:
33
+ return "Get current weather information for a location. Returns temperature and conditions."
34
+
35
+ @property
36
+ def parameters(self) -> dict[str, Any]:
37
+ return {
38
+ "type": "object",
39
+ "properties": {
40
+ "location": {
41
+ "type": "string",
42
+ "description": "City and state, e.g. 'San Francisco, CA' or 'London, UK'",
43
+ },
44
+ "unit": {
45
+ "type": "string",
46
+ "enum": ["celsius", "fahrenheit"],
47
+ "description": "Temperature unit (celsius or fahrenheit)",
48
+ },
49
+ },
50
+ "required": ["location"],
51
+ }
52
+
53
+ async def execute(self, **kwargs) -> ToolResult:
54
+ """Mock execute method."""
55
+ return ToolResult(success=True, content="Weather data")
56
+
57
+
58
+ class SearchTool(Tool):
59
+ """Example search tool."""
60
+
61
+ @property
62
+ def name(self) -> str:
63
+ return "search_web"
64
+
65
+ @property
66
+ def description(self) -> str:
67
+ return "Search the web for information about a topic"
68
+
69
+ @property
70
+ def parameters(self) -> dict[str, Any]:
71
+ return {
72
+ "type": "object",
73
+ "properties": {
74
+ "query": {
75
+ "type": "string",
76
+ "description": "Search query string",
77
+ },
78
+ "max_results": {
79
+ "type": "integer",
80
+ "description": "Maximum number of results to return (1-10)",
81
+ },
82
+ },
83
+ "required": ["query"],
84
+ }
85
+
86
+ async def execute(self, **kwargs) -> ToolResult:
87
+ """Mock execute method."""
88
+ return ToolResult(success=True, content="Search results")
89
+
90
+
91
+ class CalculatorTool(Tool):
92
+ """Example calculator tool."""
93
+
94
+ @property
95
+ def name(self) -> str:
96
+ return "calculator"
97
+
98
+ @property
99
+ def description(self) -> str:
100
+ return "Perform arithmetic calculations"
101
+
102
+ @property
103
+ def parameters(self) -> dict[str, Any]:
104
+ return {
105
+ "type": "object",
106
+ "properties": {
107
+ "expression": {
108
+ "type": "string",
109
+ "description": "Mathematical expression to evaluate, e.g. '2 + 2' or '10 * 5'",
110
+ }
111
+ },
112
+ "required": ["expression"],
113
+ }
114
+
115
+ async def execute(self, **kwargs) -> ToolResult:
116
+ """Mock execute method."""
117
+ return ToolResult(success=True, content="Calculation result")
118
+
119
+
120
+ class TranslateTool(Tool):
121
+ """Example translate tool."""
122
+
123
+ @property
124
+ def name(self) -> str:
125
+ return "translate"
126
+
127
+ @property
128
+ def description(self) -> str:
129
+ return "Translate text from one language to another"
130
+
131
+ @property
132
+ def parameters(self) -> dict[str, Any]:
133
+ return {
134
+ "type": "object",
135
+ "properties": {
136
+ "text": {
137
+ "type": "string",
138
+ "description": "Text to translate",
139
+ },
140
+ "target_language": {
141
+ "type": "string",
142
+ "description": "Target language code (e.g. 'en', 'es', 'fr')",
143
+ },
144
+ },
145
+ "required": ["text", "target_language"],
146
+ }
147
+
148
+ async def execute(self, **kwargs) -> ToolResult:
149
+ """Mock execute method."""
150
+ return ToolResult(success=True, content="Translation result")
151
+
152
+
153
+ async def demo_tool_schemas():
154
+ """Demonstrate using Tool objects with LLM."""
155
+ config = load_config()
156
+
157
+ print("=" * 60)
158
+ print("Method 1: Using Tool Objects with LLM")
159
+ print("=" * 60)
160
+
161
+ # Create tool instances
162
+ weather_tool = WeatherTool()
163
+ search_tool = SearchTool()
164
+
165
+ # Create client
166
+ client = LLMClient(
167
+ api_key=config["api_key"],
168
+ provider=LLMProvider.ANTHROPIC,
169
+ model="MiniMax-M2.1",
170
+ )
171
+
172
+ # Test with a query that should trigger weather tool
173
+ messages = [
174
+ Message(
175
+ role="user",
176
+ content="What's the weather like in Tokyo? I want it in celsius.",
177
+ )
178
+ ]
179
+
180
+ print("\nQuery: What's the weather like in Tokyo? I want it in celsius.")
181
+ print("\nAvailable tools:")
182
+ print(f" 1. {weather_tool.name}: {weather_tool.description}")
183
+ print(f" 2. {search_tool.name}: {search_tool.description}")
184
+
185
+ # Pass Tool objects directly to generate
186
+ response = await client.generate(
187
+ messages,
188
+ tools=[weather_tool, search_tool], # Using Tool objects
189
+ )
190
+
191
+ print(f"\nResponse content: {response.content}")
192
+
193
+ if response.thinking:
194
+ print(f"\nThinking: {response.thinking}")
195
+
196
+ if response.tool_calls:
197
+ print(f"\nTool calls made: {len(response.tool_calls)}")
198
+ for tool_call in response.tool_calls:
199
+ print(f" - Function: {tool_call.function.name}")
200
+ print(f" Arguments: {tool_call.function.arguments}")
201
+
202
+
203
+ async def demo_multiple_tools():
204
+ """Demonstrate using multiple Tool instances."""
205
+ config = load_config()
206
+
207
+ print("\n" + "=" * 60)
208
+ print("Method 2: Using Multiple Tool Instances")
209
+ print("=" * 60)
210
+
211
+ # Create tool instances
212
+ calculator_tool = CalculatorTool()
213
+ translate_tool = TranslateTool()
214
+
215
+ client = LLMClient(
216
+ api_key=config["api_key"],
217
+ provider=LLMProvider.ANTHROPIC,
218
+ model="MiniMax-M2.1",
219
+ )
220
+
221
+ messages = [Message(role="user", content="Calculate 15 * 23 for me")]
222
+
223
+ print("\nQuery: Calculate 15 * 23 for me")
224
+ print("\nAvailable tools:")
225
+ print(" 1. calculator (Tool)")
226
+ print(" 2. translate (Tool)")
227
+
228
+ response = await client.generate(messages, tools=[calculator_tool, translate_tool])
229
+
230
+ print(f"\nResponse content: {response.content}")
231
+
232
+ if response.thinking:
233
+ print(f"\nThinking: {response.thinking}")
234
+
235
+ if response.tool_calls:
236
+ print(f"\nTool calls made: {len(response.tool_calls)}")
237
+ for tool_call in response.tool_calls:
238
+ print(f" - Function: {tool_call.function.name}")
239
+ print(f" Arguments: {tool_call.function.arguments}")
240
+
241
+
242
+ async def demo_tool_schema_methods():
243
+ """Demonstrate Tool schema conversion methods."""
244
+ print("\n" + "=" * 60)
245
+ print("Method 3: Tool Schema Conversion Methods")
246
+ print("=" * 60)
247
+
248
+ weather_tool = WeatherTool()
249
+
250
+ print("\nTool to Anthropic schema (to_schema):")
251
+ anthropic_schema = weather_tool.to_schema()
252
+ print(f" {anthropic_schema}")
253
+
254
+ print("\nTool to OpenAI schema (to_openai_schema):")
255
+ openai_schema = weather_tool.to_openai_schema()
256
+ print(f" {openai_schema}")
257
+
258
+ print("\nSchema methods allow flexible tool usage with different LLM providers.")
259
+
260
+
261
+ async def main():
262
+ """Run all demos."""
263
+ print("\n🚀 Tool Schema Demo - Using Tool Base Class\n")
264
+
265
+ try:
266
+ # Demo 1: Tool objects with LLM
267
+ await demo_tool_schemas()
268
+
269
+ # Demo 2: Multiple tools
270
+ await demo_multiple_tools()
271
+
272
+ # Demo 3: Schema methods
273
+ await demo_tool_schema_methods()
274
+
275
+ print("\n✅ All demos completed successfully!")
276
+
277
+ except Exception as e:
278
+ print(f"\n❌ Error: {e}")
279
+ import traceback
280
+
281
+ traceback.print_exc()
282
+
283
+
284
+ if __name__ == "__main__":
285
+ asyncio.run(main())
examples/README.md ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mini Agent Examples
2
+
3
+ This directory contains a series of progressive examples to help you understand how to use the Mini Agent framework.
4
+
5
+ ## 📚 Example List
6
+
7
+ ### 01_basic_tools.py - Basic Tool Usage
8
+
9
+ **Difficulty**: ⭐ Beginner
10
+
11
+ **Content**:
12
+ - How to directly use ReadTool, WriteTool, EditTool, BashTool
13
+ - No Agent or LLM involved, pure tool call demonstrations
14
+ - Perfect for understanding each tool's basic functionality
15
+
16
+ **Run**:
17
+ ```bash
18
+ python examples/01_basic_tools.py
19
+ ```
20
+
21
+ **Key Learnings**:
22
+ - Tool input parameter formats
23
+ - ToolResult return structure
24
+ - Error handling approaches
25
+
26
+ ---
27
+
28
+ ### 02_simple_agent.py - Simple Agent Usage
29
+
30
+ **Difficulty**: ⭐⭐ Beginner-Intermediate
31
+
32
+ **Content**:
33
+ - Create the simplest Agent
34
+ - Have Agent perform file creation tasks
35
+ - Have Agent execute bash command tasks
36
+ - Understand Agent execution flow
37
+
38
+ **Run**:
39
+ ```bash
40
+ # Requires API key configuration first
41
+ python examples/02_simple_agent.py
42
+ ```
43
+
44
+ **Key Learnings**:
45
+ - Agent initialization process
46
+ - How to give tasks to Agent
47
+ - How Agent autonomously selects tools
48
+ - Task completion criteria
49
+
50
+ **Prerequisites**:
51
+ - API key configured in `mini_agent/config/config.yaml`
52
+
53
+ ---
54
+
55
+ ### 03_session_notes.py - Session Note Tool
56
+
57
+ **Difficulty**: ⭐⭐⭐ Intermediate
58
+
59
+ **Content**:
60
+ - Direct usage of Session Note tools (record_note, recall_notes)
61
+ - Agent using Session Notes to maintain cross-session memory
62
+ - Demonstrate how two Agent instances share memory
63
+
64
+ **Run**:
65
+ ```bash
66
+ python examples/03_session_notes.py
67
+ ```
68
+
69
+ **Key Learnings**:
70
+ - How Session Notes work
71
+ - Note categorization management (category)
72
+ - How to guide Agent to use notes in system prompt
73
+ - Cross-session memory implementation
74
+
75
+ **Highlight**:
76
+ This is one of the core features of this project! Shows a lightweight but effective session memory management solution.
77
+
78
+ ---
79
+
80
+ ### 04_full_agent.py - Full-Featured Agent
81
+
82
+ **Difficulty**: ⭐⭐⭐⭐ Advanced
83
+
84
+ **Content**:
85
+ - Complete Agent setup with all features
86
+ - Integration of basic tools + Session Notes + MCP tools
87
+ - Full execution flow for complex tasks
88
+ - Multi-turn conversation examples
89
+
90
+ **Run**:
91
+ ```bash
92
+ python examples/04_full_agent.py
93
+ ```
94
+
95
+ **Key Learnings**:
96
+ - How to combine multiple tools
97
+ - MCP tool loading and usage
98
+ - Complex task decomposition and execution
99
+ - Production environment Agent configuration
100
+
101
+ **Prerequisites**:
102
+ - API key configured
103
+ - (Optional) MCP tools configured
104
+
105
+ ---
106
+
107
+ ## 🚀 Quick Start
108
+
109
+ ### 1. Configure API Key
110
+
111
+ ```bash
112
+ # Copy configuration template
113
+ cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml
114
+
115
+ # Edit config file and fill in your MiniMax API Key
116
+ vim mini_agent/config/config.yaml
117
+ ```
118
+
119
+ ### 2. Run Your First Example
120
+
121
+ ```bash
122
+ # Example that doesn't need API key
123
+ python examples/01_basic_tools.py
124
+
125
+ # Example that needs API key
126
+ python examples/02_simple_agent.py
127
+ ```
128
+
129
+ ### 3. Progressive Learning
130
+
131
+ Recommended to learn in numerical order:
132
+ 1. **01_basic_tools.py** - Understand tools
133
+ 2. **02_simple_agent.py** - Understand Agent
134
+ 3. **03_session_notes.py** - Understand memory management
135
+ 4. **04_full_agent.py** - Understand complete system
136
+
137
+ ---
138
+
139
+ ## 📖 Relationship with Test Cases
140
+
141
+ These examples are all refined from test cases in the `tests/` directory:
142
+
143
+ | Example | Based on Test | Description |
144
+ | ------------------- | ---------------------------------------------------- | ------------------------------- |
145
+ | 01_basic_tools.py | tests/test_tools.py | Basic tool unit tests |
146
+ | 02_simple_agent.py | tests/test_agent.py | Agent basic functionality tests |
147
+ | 03_session_notes.py | tests/test_note_tool.py<br>tests/test_integration.py | Session Note tool tests |
148
+ | 04_full_agent.py | tests/test_integration.py | Complete integration tests |
149
+
150
+ ---
151
+
152
+ ## 💡 Recommended Learning Paths
153
+
154
+ ### Path 1: Quick Start
155
+ 1. Run `01_basic_tools.py` - Learn about tools
156
+ 2. Run `02_simple_agent.py` - Run your first Agent
157
+ 3. Go directly to interactive mode with `mini-agent`
158
+
159
+ ### Path 2: Deep Understanding
160
+ 1. Read and run all examples (01 → 04)
161
+ 2. Read corresponding test cases (`tests/`)
162
+ 3. Read core implementation code (`mini_agent/`)
163
+ 4. Try modifying examples to implement your own features
164
+
165
+ ### Path 3: Production Application
166
+ 1. Understand all examples
167
+ 2. Read [Production Deployment Guide](../docs/PRODUCTION_GUIDE.md)
168
+ 3. Configure MCP tools and Skills
169
+ 4. Extend tool set based on needs
170
+
171
+ ---
172
+
173
+ ## 🔧 Troubleshooting
174
+
175
+ ### API Key Error
176
+ ```
177
+ ❌ API key not configured in config.yaml
178
+ ```
179
+ **Solution**: Ensure you've configured a valid MiniMax API Key in `mini_agent/config/config.yaml`
180
+
181
+ ### config.yaml Not Found
182
+ ```
183
+ ❌ config.yaml not found
184
+ ```
185
+ **Solution**:
186
+ ```bash
187
+ cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml
188
+ ```
189
+
190
+ ### MCP Tools Loading Failed
191
+ ```
192
+ ⚠️ MCP tools not loaded: [error message]
193
+ ```
194
+ **Solution**: MCP tools are optional and don't affect basic functionality. If you need them, refer to the MCP configuration section in the main README.
195
+
196
+ ---
197
+
198
+ ## 📚 More Resources
199
+
200
+ - [Main Project README](../README.md) - Complete project documentation
201
+ - [Test Cases](../tests/) - More usage examples
202
+ - [Core Implementation](../mini_agent/) - Source code
203
+ - [Production Guide](../docs/PRODUCTION_GUIDE.md) - Deployment guide
204
+
205
+ ---
206
+
207
+ ## 🤝 Contributing Examples
208
+
209
+ If you have good usage examples, PRs are welcome!
210
+
211
+ Suggested new example directions:
212
+ - Web search integration examples (using MiniMax Search MCP)
213
+ - Skills usage examples (document processing, design, etc.)
214
+ - Custom tool development examples
215
+ - Error handling and retry mechanism examples
216
+
217
+ ---
218
+
219
+ **⭐ If these examples help you, please give the project a Star!**
examples/README_CN.md ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mini Agent Examples
2
+
3
+ 这个目录包含了一系列渐进式的示例,帮助你理解如何使用 Mini Agent 框架。
4
+
5
+ ## 📚 示例列表
6
+
7
+ ### 01_basic_tools.py - 基础工具使用
8
+
9
+ **难度**: ⭐ 入门
10
+
11
+ **内容**:
12
+ - 如何直接使用 ReadTool、WriteTool、EditTool、BashTool
13
+ - 不涉及 Agent 或 LLM,纯粹的工具调用演示
14
+ - 适合理解每个工具的基本功能
15
+
16
+ **运行**:
17
+ ```bash
18
+ python examples/01_basic_tools.py
19
+ ```
20
+
21
+ **学习要点**:
22
+ - 工具的输入参数格式
23
+ - ToolResult 的返回结构
24
+ - 错误处理方式
25
+
26
+ ---
27
+
28
+ ### 02_simple_agent.py - 简单 Agent 使用
29
+
30
+ **难度**: ⭐⭐ 初级
31
+
32
+ **内容**:
33
+ - 创建最简单的 Agent
34
+ - 让 Agent 执行文件创建任务
35
+ - 让 Agent 执行 bash 命令任务
36
+ - 理解 Agent 的执行流程
37
+
38
+ **运行**:
39
+ ```bash
40
+ # 需要先配置 API key
41
+ python examples/02_simple_agent.py
42
+ ```
43
+
44
+ **学习要点**:
45
+ - Agent 的初始化流程
46
+ - 如何给 Agent 下达任务
47
+ - Agent 如何自主选择工具
48
+ - 任务完成的判断标准
49
+
50
+ **前置要求**:
51
+ - 已配置 `mini_agent/config/config.yaml` 中的 API key
52
+
53
+ ---
54
+
55
+ ### 03_session_notes.py - Session Note 工具
56
+
57
+ **难度**: ⭐⭐⭐ 中级
58
+
59
+ **内容**:
60
+ - 直接使用 Session Note 工具(record_note, recall_notes)
61
+ - Agent 使用 Session Note 保持跨会话记忆
62
+ - 演示两个 Agent 实例如何共享记忆
63
+
64
+ **运行**:
65
+ ```bash
66
+ python examples/03_session_notes.py
67
+ ```
68
+
69
+ **学习要点**:
70
+ - Session Note 的工作原理
71
+ - 笔记的分类管理(category)
72
+ - 如何在 system prompt 中引导 Agent 使用笔记
73
+ - 跨会话记忆的实现方式
74
+
75
+ **亮点**:
76
+ 这是本项目的核心特性之一!展示了一种轻量但有效的会话记忆管理方案。
77
+
78
+ ---
79
+
80
+ ### 04_full_agent.py - 完整功能 Agent
81
+
82
+ **难度**: ⭐⭐⭐⭐ 高级
83
+
84
+ **内容**:
85
+ - 包含所有功能的完整 Agent 设置
86
+ - 集成基础工具 + Session Notes + MCP 工具
87
+ - 复杂任务的完整执行流程
88
+ - 多轮对话示例
89
+
90
+ **运行**:
91
+ ```bash
92
+ python examples/04_full_agent.py
93
+ ```
94
+
95
+ **学习要点**:
96
+ - 如何组合多种工具
97
+ - MCP 工具的加载和使用
98
+ - 复杂任务的分解和执行
99
+ - 生产环境的 Agent 配置
100
+
101
+ **前置要求**:
102
+ - 已配置 API key
103
+ - (可选)配置了 MCP 工具
104
+
105
+ ---
106
+
107
+ ## 🚀 快速开始
108
+
109
+ ### 1. 配置 API Key
110
+
111
+ ```bash
112
+ # 复制配置模板
113
+ cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml
114
+
115
+ # 编辑配置文件,填入你的 MiniMax API Key
116
+ vim mini_agent/config/config.yaml
117
+ ```
118
+
119
+ ### 2. 运行第一个示例
120
+
121
+ ```bash
122
+ # 不需要 API key 的示例
123
+ python examples/01_basic_tools.py
124
+
125
+ # 需要 API key 的示例
126
+ python examples/02_simple_agent.py
127
+ ```
128
+
129
+ ### 3. 逐步学习
130
+
131
+ 建议按照编号顺序学习:
132
+ 1. **01_basic_tools.py** - 理解工具
133
+ 2. **02_simple_agent.py** - 理解 Agent
134
+ 3. **03_session_notes.py** - 理解记忆管理
135
+ 4. **04_full_agent.py** - 理解完整系统
136
+
137
+ ---
138
+
139
+ ## 📖 与测试用例的对应关系
140
+
141
+ 这些示例都是基于 `tests/` 目录中的测试用例提炼而来:
142
+
143
+ | Example | Based on Test | Description |
144
+ | ------------------- | ---------------------------------------------------- | --------------------- |
145
+ | 01_basic_tools.py | tests/test_tools.py | 基础工具单元测试 |
146
+ | 02_simple_agent.py | tests/test_agent.py | Agent 基本功能测试 |
147
+ | 03_session_notes.py | tests/test_note_tool.py<br>tests/test_integration.py | Session Note 工具测试 |
148
+ | 04_full_agent.py | tests/test_integration.py | 完整集成测试 |
149
+
150
+ ---
151
+
152
+ ## 💡 学习路径建议
153
+
154
+ ### 路径 1: 快速上手
155
+ 1. 运行 `01_basic_tools.py` - 了解工具
156
+ 2. 运行 `02_simple_agent.py` - 运行第一个 Agent
157
+ 3. 直接使用 `mini-agent` 进入交互模式
158
+
159
+ ### 路径 2: 深入理解
160
+ 1. 阅读并运行所有示例 (01 → 04)
161
+ 2. 阅读对应的测试用例 (`tests/`)
162
+ 3. 阅读核心实现代码 (`mini_agent/`)
163
+ 4. 尝试修改示例,实现自己的功能
164
+
165
+ ### 路径 3: 生产应用
166
+ 1. 理解所有示例
167
+ 2. 阅读 [生产环境部署指南](../docs/PRODUCTION_GUIDE.md)
168
+ 3. 配置 MCP 工具和 Skills
169
+ 4. 根据需求扩展工具集
170
+
171
+ ---
172
+
173
+ ## 🔧 故障排除
174
+
175
+ ### API Key 错误
176
+ ```
177
+ ❌ API key not configured in config.yaml
178
+ ```
179
+ **解决**: 确保在 `mini_agent/config/config.yaml` 中配置了有效的 MiniMax API Key
180
+
181
+ ### 找不到 config.yaml
182
+ ```
183
+ ❌ config.yaml not found
184
+ ```
185
+ **解决**:
186
+ ```bash
187
+ cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml
188
+ ```
189
+
190
+ ### MCP 工具加载失败
191
+ ```
192
+ ⚠️ MCP tools not loaded: [error message]
193
+ ```
194
+ **解决**: MCP 工具是可选的,不影响基本功能。如需使用,请参考主 README 中的 MCP 配置章节。
195
+
196
+ ---
197
+
198
+ ## 📚 更多资源
199
+
200
+ - [项目主 README](../README.md) - 完整项目文档
201
+ - [测试用例](../tests/) - 更多使用示例
202
+ - [核心实现](../mini_agent/) - 源代码
203
+ - [生产环境指南](../docs/PRODUCTION_GUIDE.md) - 部署指南
204
+
205
+ ---
206
+
207
+ ## 🤝 贡献示例
208
+
209
+ 如果你有好的使用示例,欢迎提交 PR!
210
+
211
+ 建议的新示例方向:
212
+ - Web 搜索集成示例(使用 MiniMax Search MCP)
213
+ - Skills 使用示例(文档处理、设计等)
214
+ - 自定义工具开发示例
215
+ - 错误处理和重试机制示例
216
+
217
+ ---
218
+
219
+ **⭐ 如果这些示例对你有帮助,欢迎给项目一个 Star!**
mini_agent/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mini Agent - Minimal single agent with basic tools and MCP support."""
2
+
3
+ from .agent import Agent
4
+ from .llm import LLMClient
5
+ from .schema import FunctionCall, LLMProvider, LLMResponse, Message, ToolCall
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ __all__ = [
10
+ "Agent",
11
+ "LLMClient",
12
+ "LLMProvider",
13
+ "Message",
14
+ "LLMResponse",
15
+ "ToolCall",
16
+ "FunctionCall",
17
+ ]
mini_agent/acp/__init__.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ACP (Agent Client Protocol) bridge for Mini-Agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+ from uuid import uuid4
11
+
12
+ from acp import (
13
+ PROTOCOL_VERSION,
14
+ AgentSideConnection,
15
+ CancelNotification,
16
+ InitializeRequest,
17
+ InitializeResponse,
18
+ NewSessionRequest,
19
+ NewSessionResponse,
20
+ PromptRequest,
21
+ PromptResponse,
22
+ session_notification,
23
+ start_tool_call,
24
+ stdio_streams,
25
+ text_block,
26
+ tool_content,
27
+ update_agent_message,
28
+ update_agent_thought,
29
+ update_tool_call,
30
+ )
31
+ from pydantic import field_validator
32
+ from acp.schema import AgentCapabilities, Implementation, McpCapabilities
33
+
34
+ from mini_agent.agent import Agent
35
+ from mini_agent.cli import add_workspace_tools, initialize_base_tools
36
+ from mini_agent.config import Config
37
+ from mini_agent.llm import LLMClient
38
+ from mini_agent.retry import RetryConfig as RetryConfigBase
39
+ from mini_agent.schema import Message
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ try:
45
+ class InitializeRequestPatch(InitializeRequest):
46
+ @field_validator("protocolVersion", mode="before")
47
+ @classmethod
48
+ def normalize_protocol_version(cls, value: Any) -> int:
49
+ if isinstance(value, str):
50
+ try:
51
+ return int(value.split(".")[0])
52
+ except Exception:
53
+ return 1
54
+ if isinstance(value, (int, float)):
55
+ return int(value)
56
+ return 1
57
+
58
+ InitializeRequest = InitializeRequestPatch
59
+ InitializeRequest.model_rebuild(force=True)
60
+ except Exception: # pragma: no cover - defensive
61
+ logger.debug("ACP schema patch skipped")
62
+
63
+
64
+ @dataclass
65
+ class SessionState:
66
+ agent: Agent
67
+ cancelled: bool = False
68
+
69
+
70
+ class MiniMaxACPAgent:
71
+ """Minimal ACP adapter wrapping the existing Agent runtime."""
72
+
73
+ def __init__(
74
+ self,
75
+ conn: AgentSideConnection,
76
+ config: Config,
77
+ llm: LLMClient,
78
+ base_tools: list,
79
+ system_prompt: str,
80
+ ):
81
+ self._conn = conn
82
+ self._config = config
83
+ self._llm = llm
84
+ self._base_tools = base_tools
85
+ self._system_prompt = system_prompt
86
+ self._sessions: dict[str, SessionState] = {}
87
+
88
+ async def initialize(self, params: InitializeRequest) -> InitializeResponse: # noqa: ARG002
89
+ return InitializeResponse(
90
+ protocolVersion=PROTOCOL_VERSION,
91
+ agentCapabilities=AgentCapabilities(loadSession=False),
92
+ agentInfo=Implementation(name="mini-agent", title="Mini-Agent", version="0.1.0"),
93
+ )
94
+
95
+ async def newSession(self, params: NewSessionRequest) -> NewSessionResponse:
96
+ session_id = f"sess-{len(self._sessions)}-{uuid4().hex[:8]}"
97
+ workspace = Path(params.cwd or self._config.agent.workspace_dir).expanduser()
98
+ if not workspace.is_absolute():
99
+ workspace = workspace.resolve()
100
+ tools = list(self._base_tools)
101
+ add_workspace_tools(tools, self._config, workspace)
102
+ agent = Agent(llm_client=self._llm, system_prompt=self._system_prompt, tools=tools, max_steps=self._config.agent.max_steps, workspace_dir=str(workspace))
103
+ self._sessions[session_id] = SessionState(agent=agent)
104
+ return NewSessionResponse(sessionId=session_id)
105
+
106
+ async def prompt(self, params: PromptRequest) -> PromptResponse:
107
+ state = self._sessions.get(params.sessionId)
108
+ if not state:
109
+ # Auto-create session if not found (compatibility with clients that skip newSession)
110
+ logger.warning(f"Session '{params.sessionId}' not found, auto-creating new session")
111
+ new_session = await self.newSession(NewSessionRequest(cwd=None))
112
+ state = self._sessions.get(new_session.sessionId)
113
+ if not state:
114
+ logger.error("Failed to auto-create session")
115
+ return PromptResponse(stopReason="refusal")
116
+ state.cancelled = False
117
+ user_text = "\n".join(block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "") for block in params.prompt)
118
+ state.agent.messages.append(Message(role="user", content=user_text))
119
+ stop_reason = await self._run_turn(state, params.sessionId)
120
+ return PromptResponse(stopReason=stop_reason)
121
+
122
+ async def cancel(self, params: CancelNotification) -> None:
123
+ state = self._sessions.get(params.sessionId)
124
+ if state:
125
+ state.cancelled = True
126
+
127
+ async def _run_turn(self, state: SessionState, session_id: str) -> str:
128
+ agent = state.agent
129
+ for _ in range(agent.max_steps):
130
+ if state.cancelled:
131
+ return "cancelled"
132
+ tool_schemas = [tool.to_schema() for tool in agent.tools.values()]
133
+ try:
134
+ response = await agent.llm.generate(messages=agent.messages, tools=tool_schemas)
135
+ except Exception as exc:
136
+ logger.exception("LLM error")
137
+ await self._send(session_id, update_agent_message(text_block(f"Error: {exc}")))
138
+ return "refusal"
139
+ if response.thinking:
140
+ await self._send(session_id, update_agent_thought(text_block(response.thinking)))
141
+ if response.content:
142
+ await self._send(session_id, update_agent_message(text_block(response.content)))
143
+ agent.messages.append(Message(role="assistant", content=response.content, thinking=response.thinking, tool_calls=response.tool_calls))
144
+ if not response.tool_calls:
145
+ return "end_turn"
146
+ for call in response.tool_calls:
147
+ name, args = call.function.name, call.function.arguments
148
+ # Show tool name with key arguments for better visibility
149
+ args_preview = ", ".join(f"{k}={repr(v)[:50]}" for k, v in list(args.items())[:2]) if isinstance(args, dict) else ""
150
+ label = f"🔧 {name}({args_preview})" if args_preview else f"🔧 {name}()"
151
+ await self._send(session_id, start_tool_call(call.id, label, kind="execute", raw_input=args))
152
+ tool = agent.tools.get(name)
153
+ if not tool:
154
+ text, status = f"❌ Unknown tool: {name}", "failed"
155
+ else:
156
+ try:
157
+ result = await tool.execute(**args)
158
+ status = "completed" if result.success else "failed"
159
+ prefix = "✅" if result.success else "❌"
160
+ text = f"{prefix} {result.content if result.success else result.error or 'Tool execution failed'}"
161
+ except Exception as exc:
162
+ status, text = "failed", f"❌ Tool error: {exc}"
163
+ await self._send(session_id, update_tool_call(call.id, status=status, content=[tool_content(text_block(text))], raw_output=text))
164
+ agent.messages.append(Message(role="tool", content=text, tool_call_id=call.id, name=name))
165
+ return "max_turn_requests"
166
+
167
+ async def _send(self, session_id: str, update: Any) -> None:
168
+ await self._conn.sessionUpdate(session_notification(session_id, update))
169
+
170
+
171
+ async def run_acp_server(config: Config | None = None) -> None:
172
+ """Run Mini-Agent as an ACP-compatible stdio server."""
173
+ config = config or Config.load()
174
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
175
+ base_tools, skill_loader = await initialize_base_tools(config)
176
+ prompt_path = Config.find_config_file(config.agent.system_prompt_path)
177
+ if prompt_path and prompt_path.exists():
178
+ system_prompt = prompt_path.read_text(encoding="utf-8")
179
+ else:
180
+ system_prompt = "You are a helpful AI assistant."
181
+ if skill_loader:
182
+ meta = skill_loader.get_skills_metadata_prompt()
183
+ if meta:
184
+ system_prompt = f"{system_prompt.rstrip()}\n\n{meta}"
185
+ rcfg = config.llm.retry
186
+ llm = LLMClient(api_key=config.llm.api_key, api_base=config.llm.api_base, model=config.llm.model, retry_config=RetryConfigBase(enabled=rcfg.enabled, max_retries=rcfg.max_retries, initial_delay=rcfg.initial_delay, max_delay=rcfg.max_delay, exponential_base=rcfg.exponential_base))
187
+ reader, writer = await stdio_streams()
188
+ AgentSideConnection(lambda conn: MiniMaxACPAgent(conn, config, llm, base_tools, system_prompt), writer, reader)
189
+ logger.info("Mini-Agent ACP server running")
190
+ await asyncio.Event().wait()
191
+
192
+
193
+ def main() -> None:
194
+ asyncio.run(run_acp_server())
195
+
196
+
197
+ __all__ = ["MiniMaxACPAgent", "run_acp_server", "main"]
mini_agent/acp/server.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """ACP server entry point."""
2
+
3
+ from mini_agent.acp import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
mini_agent/agent.py ADDED
@@ -0,0 +1,523 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Core Agent implementation."""
2
+
3
+ import asyncio
4
+ import json
5
+ from pathlib import Path
6
+ from time import perf_counter
7
+ from typing import Optional
8
+
9
+ import tiktoken
10
+
11
+ from .llm import LLMClient
12
+ from .logger import AgentLogger
13
+ from .schema import Message
14
+ from .tools.base import Tool, ToolResult
15
+ from .utils import calculate_display_width
16
+
17
+
18
+ # ANSI color codes
19
+ class Colors:
20
+ """Terminal color definitions"""
21
+
22
+ RESET = "\033[0m"
23
+ BOLD = "\033[1m"
24
+ DIM = "\033[2m"
25
+
26
+ # Foreground colors
27
+ RED = "\033[31m"
28
+ GREEN = "\033[32m"
29
+ YELLOW = "\033[33m"
30
+ BLUE = "\033[34m"
31
+ MAGENTA = "\033[35m"
32
+ CYAN = "\033[36m"
33
+
34
+ # Bright colors
35
+ BRIGHT_BLACK = "\033[90m"
36
+ BRIGHT_RED = "\033[91m"
37
+ BRIGHT_GREEN = "\033[92m"
38
+ BRIGHT_YELLOW = "\033[93m"
39
+ BRIGHT_BLUE = "\033[94m"
40
+ BRIGHT_MAGENTA = "\033[95m"
41
+ BRIGHT_CYAN = "\033[96m"
42
+ BRIGHT_WHITE = "\033[97m"
43
+
44
+
45
+ class Agent:
46
+ """Single agent with basic tools and MCP support."""
47
+
48
+ def __init__(
49
+ self,
50
+ llm_client: LLMClient,
51
+ system_prompt: str,
52
+ tools: list[Tool],
53
+ max_steps: int = 50,
54
+ workspace_dir: str = "./workspace",
55
+ token_limit: int = 80000, # Summary triggered when tokens exceed this value
56
+ ):
57
+ self.llm = llm_client
58
+ self.tools = {tool.name: tool for tool in tools}
59
+ self.max_steps = max_steps
60
+ self.token_limit = token_limit
61
+ self.workspace_dir = Path(workspace_dir)
62
+ # Cancellation event for interrupting agent execution (set externally, e.g., by Esc key)
63
+ self.cancel_event: Optional[asyncio.Event] = None
64
+
65
+ # Ensure workspace exists
66
+ self.workspace_dir.mkdir(parents=True, exist_ok=True)
67
+
68
+ # Inject workspace information into system prompt if not already present
69
+ if "Current Workspace" not in system_prompt:
70
+ workspace_info = f"\n\n## Current Workspace\nYou are currently working in: `{self.workspace_dir.absolute()}`\nAll relative paths will be resolved relative to this directory."
71
+ system_prompt = system_prompt + workspace_info
72
+
73
+ self.system_prompt = system_prompt
74
+
75
+ # Initialize message history
76
+ self.messages: list[Message] = [Message(role="system", content=system_prompt)]
77
+
78
+ # Initialize logger
79
+ self.logger = AgentLogger()
80
+
81
+ # Token usage from last API response (updated after each LLM call)
82
+ self.api_total_tokens: int = 0
83
+ # Flag to skip token check right after summary (avoid consecutive triggers)
84
+ self._skip_next_token_check: bool = False
85
+
86
+ def add_user_message(self, content: str):
87
+ """Add a user message to history."""
88
+ self.messages.append(Message(role="user", content=content))
89
+
90
+ def _check_cancelled(self) -> bool:
91
+ """Check if agent execution has been cancelled.
92
+
93
+ Returns:
94
+ True if cancelled, False otherwise.
95
+ """
96
+ if self.cancel_event is not None and self.cancel_event.is_set():
97
+ return True
98
+ return False
99
+
100
+ def _cleanup_incomplete_messages(self):
101
+ """Remove the incomplete assistant message and its partial tool results.
102
+
103
+ This ensures message consistency after cancellation by removing
104
+ only the current step's incomplete messages, preserving completed steps.
105
+ """
106
+ # Find the index of the last assistant message
107
+ last_assistant_idx = -1
108
+ for i in range(len(self.messages) - 1, -1, -1):
109
+ if self.messages[i].role == "assistant":
110
+ last_assistant_idx = i
111
+ break
112
+
113
+ if last_assistant_idx == -1:
114
+ # No assistant message found, nothing to clean
115
+ return
116
+
117
+ # Remove the last assistant message and all tool results after it
118
+ removed_count = len(self.messages) - last_assistant_idx
119
+ if removed_count > 0:
120
+ self.messages = self.messages[:last_assistant_idx]
121
+ print(f"{Colors.DIM} Cleaned up {removed_count} incomplete message(s){Colors.RESET}")
122
+
123
+ def _estimate_tokens(self) -> int:
124
+ """Accurately calculate token count for message history using tiktoken
125
+
126
+ Uses cl100k_base encoder (GPT-4/Claude/M2 compatible)
127
+ """
128
+ try:
129
+ # Use cl100k_base encoder (used by GPT-4 and most modern models)
130
+ encoding = tiktoken.get_encoding("cl100k_base")
131
+ except Exception:
132
+ # Fallback: if tiktoken initialization fails, use simple estimation
133
+ return self._estimate_tokens_fallback()
134
+
135
+ total_tokens = 0
136
+
137
+ for msg in self.messages:
138
+ # Count text content
139
+ if isinstance(msg.content, str):
140
+ total_tokens += len(encoding.encode(msg.content))
141
+ elif isinstance(msg.content, list):
142
+ for block in msg.content:
143
+ if isinstance(block, dict):
144
+ # Convert dict to string for calculation
145
+ total_tokens += len(encoding.encode(str(block)))
146
+
147
+ # Count thinking
148
+ if msg.thinking:
149
+ total_tokens += len(encoding.encode(msg.thinking))
150
+
151
+ # Count tool_calls
152
+ if msg.tool_calls:
153
+ total_tokens += len(encoding.encode(str(msg.tool_calls)))
154
+
155
+ # Metadata overhead per message (approximately 4 tokens)
156
+ total_tokens += 4
157
+
158
+ return total_tokens
159
+
160
+ def _estimate_tokens_fallback(self) -> int:
161
+ """Fallback token estimation method (when tiktoken is unavailable)"""
162
+ total_chars = 0
163
+ for msg in self.messages:
164
+ if isinstance(msg.content, str):
165
+ total_chars += len(msg.content)
166
+ elif isinstance(msg.content, list):
167
+ for block in msg.content:
168
+ if isinstance(block, dict):
169
+ total_chars += len(str(block))
170
+
171
+ if msg.thinking:
172
+ total_chars += len(msg.thinking)
173
+
174
+ if msg.tool_calls:
175
+ total_chars += len(str(msg.tool_calls))
176
+
177
+ # Rough estimation: average 2.5 characters = 1 token
178
+ return int(total_chars / 2.5)
179
+
180
+ async def _summarize_messages(self):
181
+ """Message history summarization: summarize conversations between user messages when tokens exceed limit
182
+
183
+ Strategy (Agent mode):
184
+ - Keep all user messages (these are user intents)
185
+ - Summarize content between each user-user pair (agent execution process)
186
+ - If last round is still executing (has agent/tool messages but no next user), also summarize
187
+ - Structure: system -> user1 -> summary1 -> user2 -> summary2 -> user3 -> summary3 (if executing)
188
+
189
+ Summary is triggered when EITHER:
190
+ - Local token estimation exceeds limit
191
+ - API reported total_tokens exceeds limit
192
+ """
193
+ # Skip check if we just completed a summary (wait for next LLM call to update api_total_tokens)
194
+ if self._skip_next_token_check:
195
+ self._skip_next_token_check = False
196
+ return
197
+
198
+ estimated_tokens = self._estimate_tokens()
199
+
200
+ # Check both local estimation and API reported tokens
201
+ should_summarize = estimated_tokens > self.token_limit or self.api_total_tokens > self.token_limit
202
+
203
+ # If neither exceeded, no summary needed
204
+ if not should_summarize:
205
+ return
206
+
207
+ print(
208
+ f"\n{Colors.BRIGHT_YELLOW}📊 Token usage - Local estimate: {estimated_tokens}, API reported: {self.api_total_tokens}, Limit: {self.token_limit}{Colors.RESET}"
209
+ )
210
+ print(f"{Colors.BRIGHT_YELLOW}🔄 Triggering message history summarization...{Colors.RESET}")
211
+
212
+ # Find all user message indices (skip system prompt)
213
+ user_indices = [i for i, msg in enumerate(self.messages) if msg.role == "user" and i > 0]
214
+
215
+ # Need at least 1 user message to perform summary
216
+ if len(user_indices) < 1:
217
+ print(f"{Colors.BRIGHT_YELLOW}⚠️ Insufficient messages, cannot summarize{Colors.RESET}")
218
+ return
219
+
220
+ # Build new message list
221
+ new_messages = [self.messages[0]] # Keep system prompt
222
+ summary_count = 0
223
+
224
+ # Iterate through each user message and summarize the execution process after it
225
+ for i, user_idx in enumerate(user_indices):
226
+ # Add current user message
227
+ new_messages.append(self.messages[user_idx])
228
+
229
+ # Determine message range to summarize
230
+ # If last user, go to end of message list; otherwise to before next user
231
+ if i < len(user_indices) - 1:
232
+ next_user_idx = user_indices[i + 1]
233
+ else:
234
+ next_user_idx = len(self.messages)
235
+
236
+ # Extract execution messages for this round
237
+ execution_messages = self.messages[user_idx + 1 : next_user_idx]
238
+
239
+ # If there are execution messages in this round, summarize them
240
+ if execution_messages:
241
+ summary_text = await self._create_summary(execution_messages, i + 1)
242
+ if summary_text:
243
+ summary_message = Message(
244
+ role="user",
245
+ content=f"[Assistant Execution Summary]\n\n{summary_text}",
246
+ )
247
+ new_messages.append(summary_message)
248
+ summary_count += 1
249
+
250
+ # Replace message list
251
+ self.messages = new_messages
252
+
253
+ # Skip next token check to avoid consecutive summary triggers
254
+ # (api_total_tokens will be updated after next LLM call)
255
+ self._skip_next_token_check = True
256
+
257
+ new_tokens = self._estimate_tokens()
258
+ print(f"{Colors.BRIGHT_GREEN}✓ Summary completed, local tokens: {estimated_tokens} → {new_tokens}{Colors.RESET}")
259
+ print(f"{Colors.DIM} Structure: system + {len(user_indices)} user messages + {summary_count} summaries{Colors.RESET}")
260
+ print(f"{Colors.DIM} Note: API token count will update on next LLM call{Colors.RESET}")
261
+
262
+ async def _create_summary(self, messages: list[Message], round_num: int) -> str:
263
+ """Create summary for one execution round
264
+
265
+ Args:
266
+ messages: List of messages to summarize
267
+ round_num: Round number
268
+
269
+ Returns:
270
+ Summary text
271
+ """
272
+ if not messages:
273
+ return ""
274
+
275
+ # Build summary content
276
+ summary_content = f"Round {round_num} execution process:\n\n"
277
+ for msg in messages:
278
+ if msg.role == "assistant":
279
+ content_text = msg.content if isinstance(msg.content, str) else str(msg.content)
280
+ summary_content += f"Assistant: {content_text}\n"
281
+ if msg.tool_calls:
282
+ tool_names = [tc.function.name for tc in msg.tool_calls]
283
+ summary_content += f" → Called tools: {', '.join(tool_names)}\n"
284
+ elif msg.role == "tool":
285
+ result_preview = msg.content if isinstance(msg.content, str) else str(msg.content)
286
+ summary_content += f" ← Tool returned: {result_preview}...\n"
287
+
288
+ # Call LLM to generate concise summary
289
+ try:
290
+ summary_prompt = f"""Please provide a concise summary of the following Agent execution process:
291
+
292
+ {summary_content}
293
+
294
+ Requirements:
295
+ 1. Focus on what tasks were completed and which tools were called
296
+ 2. Keep key execution results and important findings
297
+ 3. Be concise and clear, within 1000 words
298
+ 4. Use English
299
+ 5. Do not include "user" related content, only summarize the Agent's execution process"""
300
+
301
+ summary_msg = Message(role="user", content=summary_prompt)
302
+ response = await self.llm.generate(
303
+ messages=[
304
+ Message(
305
+ role="system",
306
+ content="You are an assistant skilled at summarizing Agent execution processes.",
307
+ ),
308
+ summary_msg,
309
+ ]
310
+ )
311
+
312
+ summary_text = response.content
313
+ print(f"{Colors.BRIGHT_GREEN}✓ Summary for round {round_num} generated successfully{Colors.RESET}")
314
+ return summary_text
315
+
316
+ except Exception as e:
317
+ print(f"{Colors.BRIGHT_RED}✗ Summary generation failed for round {round_num}: {e}{Colors.RESET}")
318
+ # Use simple text summary on failure
319
+ return summary_content
320
+
321
+ async def run(self, cancel_event: Optional[asyncio.Event] = None) -> str:
322
+ """Execute agent loop until task is complete or max steps reached.
323
+
324
+ Args:
325
+ cancel_event: Optional asyncio.Event that can be set to cancel execution.
326
+ When set, the agent will stop at the next safe checkpoint
327
+ (after completing the current step to keep messages consistent).
328
+
329
+ Returns:
330
+ The final response content, or error message (including cancellation message).
331
+ """
332
+ # Set cancellation event (can also be set via self.cancel_event before calling run())
333
+ if cancel_event is not None:
334
+ self.cancel_event = cancel_event
335
+
336
+ # Start new run, initialize log file
337
+ self.logger.start_new_run()
338
+ print(f"{Colors.DIM}📝 Log file: {self.logger.get_log_file_path()}{Colors.RESET}")
339
+
340
+ step = 0
341
+ run_start_time = perf_counter()
342
+
343
+ while step < self.max_steps:
344
+ # Check for cancellation at start of each step
345
+ if self._check_cancelled():
346
+ self._cleanup_incomplete_messages()
347
+ cancel_msg = "Task cancelled by user."
348
+ print(f"\n{Colors.BRIGHT_YELLOW}⚠️ {cancel_msg}{Colors.RESET}")
349
+ return cancel_msg
350
+
351
+ step_start_time = perf_counter()
352
+ # Check and summarize message history to prevent context overflow
353
+ await self._summarize_messages()
354
+
355
+ # Step header with proper width calculation
356
+ BOX_WIDTH = 58
357
+ step_text = f"{Colors.BOLD}{Colors.BRIGHT_CYAN}💭 Step {step + 1}/{self.max_steps}{Colors.RESET}"
358
+ step_display_width = calculate_display_width(step_text)
359
+ padding = max(0, BOX_WIDTH - 1 - step_display_width) # -1 for leading space
360
+
361
+ print(f"\n{Colors.DIM}╭{'─' * BOX_WIDTH}╮{Colors.RESET}")
362
+ print(f"{Colors.DIM}│{Colors.RESET} {step_text}{' ' * padding}{Colors.DIM}│{Colors.RESET}")
363
+ print(f"{Colors.DIM}╰{'─' * BOX_WIDTH}╯{Colors.RESET}")
364
+
365
+ # Get tool list for LLM call
366
+ tool_list = list(self.tools.values())
367
+
368
+ # Log LLM request and call LLM with Tool objects directly
369
+ self.logger.log_request(messages=self.messages, tools=tool_list)
370
+
371
+ try:
372
+ response = await self.llm.generate(messages=self.messages, tools=tool_list)
373
+ except Exception as e:
374
+ # Check if it's a retry exhausted error
375
+ from .retry import RetryExhaustedError
376
+
377
+ if isinstance(e, RetryExhaustedError):
378
+ error_msg = f"LLM call failed after {e.attempts} retries\nLast error: {str(e.last_exception)}"
379
+ print(f"\n{Colors.BRIGHT_RED}❌ Retry failed:{Colors.RESET} {error_msg}")
380
+ else:
381
+ error_msg = f"LLM call failed: {str(e)}"
382
+ print(f"\n{Colors.BRIGHT_RED}❌ Error:{Colors.RESET} {error_msg}")
383
+ return error_msg
384
+
385
+ # Accumulate API reported token usage
386
+ if response.usage:
387
+ self.api_total_tokens = response.usage.total_tokens
388
+
389
+ # Log LLM response
390
+ self.logger.log_response(
391
+ content=response.content,
392
+ thinking=response.thinking,
393
+ tool_calls=response.tool_calls,
394
+ finish_reason=response.finish_reason,
395
+ )
396
+
397
+ # Add assistant message
398
+ assistant_msg = Message(
399
+ role="assistant",
400
+ content=response.content,
401
+ thinking=response.thinking,
402
+ tool_calls=response.tool_calls,
403
+ )
404
+ self.messages.append(assistant_msg)
405
+
406
+ # Print thinking if present
407
+ if response.thinking:
408
+ print(f"\n{Colors.BOLD}{Colors.MAGENTA}🧠 Thinking:{Colors.RESET}")
409
+ print(f"{Colors.DIM}{response.thinking}{Colors.RESET}")
410
+
411
+ # Print assistant response
412
+ if response.content:
413
+ print(f"\n{Colors.BOLD}{Colors.BRIGHT_BLUE}🤖 Assistant:{Colors.RESET}")
414
+ print(f"{response.content}")
415
+
416
+ # Check if task is complete (no tool calls)
417
+ if not response.tool_calls:
418
+ step_elapsed = perf_counter() - step_start_time
419
+ total_elapsed = perf_counter() - run_start_time
420
+ print(f"\n{Colors.DIM}⏱️ Step {step + 1} completed in {step_elapsed:.2f}s (total: {total_elapsed:.2f}s){Colors.RESET}")
421
+ return response.content
422
+
423
+ # Check for cancellation before executing tools
424
+ if self._check_cancelled():
425
+ self._cleanup_incomplete_messages()
426
+ cancel_msg = "Task cancelled by user."
427
+ print(f"\n{Colors.BRIGHT_YELLOW}⚠️ {cancel_msg}{Colors.RESET}")
428
+ return cancel_msg
429
+
430
+ # Execute tool calls
431
+ for tool_call in response.tool_calls:
432
+ tool_call_id = tool_call.id
433
+ function_name = tool_call.function.name
434
+ arguments = tool_call.function.arguments
435
+
436
+ # Tool call header
437
+ print(f"\n{Colors.BRIGHT_YELLOW}🔧 Tool Call:{Colors.RESET} {Colors.BOLD}{Colors.CYAN}{function_name}{Colors.RESET}")
438
+
439
+ # Arguments (formatted display)
440
+ print(f"{Colors.DIM} Arguments:{Colors.RESET}")
441
+ # Truncate each argument value to avoid overly long output
442
+ truncated_args = {}
443
+ for key, value in arguments.items():
444
+ value_str = str(value)
445
+ if len(value_str) > 200:
446
+ truncated_args[key] = value_str[:200] + "..."
447
+ else:
448
+ truncated_args[key] = value
449
+ args_json = json.dumps(truncated_args, indent=2, ensure_ascii=False)
450
+ for line in args_json.split("\n"):
451
+ print(f" {Colors.DIM}{line}{Colors.RESET}")
452
+
453
+ # Execute tool
454
+ if function_name not in self.tools:
455
+ result = ToolResult(
456
+ success=False,
457
+ content="",
458
+ error=f"Unknown tool: {function_name}",
459
+ )
460
+ else:
461
+ try:
462
+ tool = self.tools[function_name]
463
+ result = await tool.execute(**arguments)
464
+ except Exception as e:
465
+ # Catch all exceptions during tool execution, convert to failed ToolResult
466
+ import traceback
467
+
468
+ error_detail = f"{type(e).__name__}: {str(e)}"
469
+ error_trace = traceback.format_exc()
470
+ result = ToolResult(
471
+ success=False,
472
+ content="",
473
+ error=f"Tool execution failed: {error_detail}\n\nTraceback:\n{error_trace}",
474
+ )
475
+
476
+ # Log tool execution result
477
+ self.logger.log_tool_result(
478
+ tool_name=function_name,
479
+ arguments=arguments,
480
+ result_success=result.success,
481
+ result_content=result.content if result.success else None,
482
+ result_error=result.error if not result.success else None,
483
+ )
484
+
485
+ # Print result
486
+ if result.success:
487
+ result_text = result.content
488
+ if len(result_text) > 300:
489
+ result_text = result_text[:300] + f"{Colors.DIM}...{Colors.RESET}"
490
+ print(f"{Colors.BRIGHT_GREEN}✓ Result:{Colors.RESET} {result_text}")
491
+ else:
492
+ print(f"{Colors.BRIGHT_RED}✗ Error:{Colors.RESET} {Colors.RED}{result.error}{Colors.RESET}")
493
+
494
+ # Add tool result message
495
+ tool_msg = Message(
496
+ role="tool",
497
+ content=result.content if result.success else f"Error: {result.error}",
498
+ tool_call_id=tool_call_id,
499
+ name=function_name,
500
+ )
501
+ self.messages.append(tool_msg)
502
+
503
+ # Check for cancellation after each tool execution
504
+ if self._check_cancelled():
505
+ self._cleanup_incomplete_messages()
506
+ cancel_msg = "Task cancelled by user."
507
+ print(f"\n{Colors.BRIGHT_YELLOW}⚠️ {cancel_msg}{Colors.RESET}")
508
+ return cancel_msg
509
+
510
+ step_elapsed = perf_counter() - step_start_time
511
+ total_elapsed = perf_counter() - run_start_time
512
+ print(f"\n{Colors.DIM}⏱️ Step {step + 1} completed in {step_elapsed:.2f}s (total: {total_elapsed:.2f}s){Colors.RESET}")
513
+
514
+ step += 1
515
+
516
+ # Max steps reached
517
+ error_msg = f"Task couldn't be completed after {self.max_steps} steps."
518
+ print(f"\n{Colors.BRIGHT_YELLOW}⚠️ {error_msg}{Colors.RESET}")
519
+ return error_msg
520
+
521
+ def get_history(self) -> list[Message]:
522
+ """Get message history."""
523
+ return self.messages.copy()
mini_agent/cli.py ADDED
@@ -0,0 +1,834 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Mini Agent - Interactive Runtime Example
3
+
4
+ Usage:
5
+ mini-agent [--workspace DIR]
6
+
7
+ Examples:
8
+ mini-agent # Use current directory as workspace
9
+ mini-agent --workspace /path/to/dir # Use specific workspace directory
10
+ """
11
+
12
+ import argparse
13
+ import asyncio
14
+ import platform
15
+ import subprocess
16
+ import sys
17
+ import threading
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+ from typing import List
21
+
22
+ from prompt_toolkit import PromptSession
23
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
24
+ from prompt_toolkit.completion import WordCompleter
25
+ from prompt_toolkit.history import FileHistory
26
+ from prompt_toolkit.key_binding import KeyBindings
27
+ from prompt_toolkit.styles import Style
28
+
29
+ from mini_agent import LLMClient
30
+ from mini_agent.agent import Agent
31
+ from mini_agent.config import Config
32
+ from mini_agent.schema import LLMProvider
33
+ from mini_agent.tools.base import Tool
34
+ from mini_agent.tools.bash_tool import BashKillTool, BashOutputTool, BashTool
35
+ from mini_agent.tools.file_tools import EditTool, ReadTool, WriteTool
36
+ from mini_agent.tools.mcp_loader import cleanup_mcp_connections, load_mcp_tools_async, set_mcp_timeout_config
37
+ from mini_agent.tools.note_tool import SessionNoteTool
38
+ from mini_agent.tools.skill_tool import create_skill_tools
39
+ from mini_agent.utils import calculate_display_width
40
+
41
+
42
+ # ANSI color codes
43
+ class Colors:
44
+ """Terminal color definitions"""
45
+
46
+ RESET = "\033[0m"
47
+ BOLD = "\033[1m"
48
+ DIM = "\033[2m"
49
+
50
+ # Foreground colors
51
+ BLACK = "\033[30m"
52
+ RED = "\033[31m"
53
+ GREEN = "\033[32m"
54
+ YELLOW = "\033[33m"
55
+ BLUE = "\033[34m"
56
+ MAGENTA = "\033[35m"
57
+ CYAN = "\033[36m"
58
+ WHITE = "\033[37m"
59
+
60
+ # Bright colors
61
+ BRIGHT_BLACK = "\033[90m"
62
+ BRIGHT_RED = "\033[91m"
63
+ BRIGHT_GREEN = "\033[92m"
64
+ BRIGHT_YELLOW = "\033[93m"
65
+ BRIGHT_BLUE = "\033[94m"
66
+ BRIGHT_MAGENTA = "\033[95m"
67
+ BRIGHT_CYAN = "\033[96m"
68
+ BRIGHT_WHITE = "\033[97m"
69
+
70
+ # Background colors
71
+ BG_RED = "\033[41m"
72
+ BG_GREEN = "\033[42m"
73
+ BG_YELLOW = "\033[43m"
74
+ BG_BLUE = "\033[44m"
75
+
76
+
77
+ def get_log_directory() -> Path:
78
+ """Get the log directory path."""
79
+ return Path.home() / ".mini-agent" / "log"
80
+
81
+
82
+ def show_log_directory(open_file_manager: bool = True) -> None:
83
+ """Show log directory contents and optionally open file manager.
84
+
85
+ Args:
86
+ open_file_manager: Whether to open the system file manager
87
+ """
88
+ log_dir = get_log_directory()
89
+
90
+ print(f"\n{Colors.BRIGHT_CYAN}📁 Log Directory: {log_dir}{Colors.RESET}")
91
+
92
+ if not log_dir.exists() or not log_dir.is_dir():
93
+ print(f"{Colors.RED}Log directory does not exist: {log_dir}{Colors.RESET}\n")
94
+ return
95
+
96
+ log_files = list(log_dir.glob("*.log"))
97
+
98
+ if not log_files:
99
+ print(f"{Colors.YELLOW}No log files found in directory.{Colors.RESET}\n")
100
+ return
101
+
102
+ # Sort by modification time (newest first)
103
+ log_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
104
+
105
+ print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}")
106
+ print(f"{Colors.BOLD}{Colors.BRIGHT_YELLOW}Available Log Files (newest first):{Colors.RESET}")
107
+
108
+ for i, log_file in enumerate(log_files[:10], 1):
109
+ mtime = datetime.fromtimestamp(log_file.stat().st_mtime)
110
+ size = log_file.stat().st_size
111
+ size_str = f"{size:,}" if size < 1024 else f"{size / 1024:.1f}K"
112
+ print(f" {Colors.GREEN}{i:2d}.{Colors.RESET} {Colors.BRIGHT_WHITE}{log_file.name}{Colors.RESET}")
113
+ print(f" {Colors.DIM}Modified: {mtime.strftime('%Y-%m-%d %H:%M:%S')}, Size: {size_str}{Colors.RESET}")
114
+
115
+ if len(log_files) > 10:
116
+ print(f" {Colors.DIM}... and {len(log_files) - 10} more files{Colors.RESET}")
117
+
118
+ print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}")
119
+
120
+ # Open file manager
121
+ if open_file_manager:
122
+ _open_directory_in_file_manager(log_dir)
123
+
124
+ print()
125
+
126
+
127
+ def _open_directory_in_file_manager(directory: Path) -> None:
128
+ """Open directory in system file manager (cross-platform)."""
129
+ system = platform.system()
130
+
131
+ try:
132
+ if system == "Darwin":
133
+ subprocess.run(["open", str(directory)], check=False)
134
+ elif system == "Windows":
135
+ subprocess.run(["explorer", str(directory)], check=False)
136
+ elif system == "Linux":
137
+ subprocess.run(["xdg-open", str(directory)], check=False)
138
+ except FileNotFoundError:
139
+ print(f"{Colors.YELLOW}Could not open file manager. Please navigate manually.{Colors.RESET}")
140
+ except Exception as e:
141
+ print(f"{Colors.YELLOW}Error opening file manager: {e}{Colors.RESET}")
142
+
143
+
144
+ def read_log_file(filename: str) -> None:
145
+ """Read and display a specific log file.
146
+
147
+ Args:
148
+ filename: The log filename to read
149
+ """
150
+ log_dir = get_log_directory()
151
+ log_file = log_dir / filename
152
+
153
+ if not log_file.exists() or not log_file.is_file():
154
+ print(f"\n{Colors.RED}❌ Log file not found: {log_file}{Colors.RESET}\n")
155
+ return
156
+
157
+ print(f"\n{Colors.BRIGHT_CYAN}📄 Reading: {log_file}{Colors.RESET}")
158
+ print(f"{Colors.DIM}{'─' * 80}{Colors.RESET}")
159
+
160
+ try:
161
+ with open(log_file, "r", encoding="utf-8") as f:
162
+ content = f.read()
163
+ print(content)
164
+ print(f"{Colors.DIM}{'─' * 80}{Colors.RESET}")
165
+ print(f"\n{Colors.GREEN}✅ End of file{Colors.RESET}\n")
166
+ except Exception as e:
167
+ print(f"\n{Colors.RED}❌ Error reading file: {e}{Colors.RESET}\n")
168
+
169
+
170
+ def print_banner():
171
+ """Print welcome banner with proper alignment"""
172
+ BOX_WIDTH = 58
173
+ banner_text = f"{Colors.BOLD}🤖 Mini Agent - Multi-turn Interactive Session{Colors.RESET}"
174
+ banner_width = calculate_display_width(banner_text)
175
+
176
+ # Center the text with proper padding
177
+ total_padding = BOX_WIDTH - banner_width
178
+ left_padding = total_padding // 2
179
+ right_padding = total_padding - left_padding
180
+
181
+ print()
182
+ print(f"{Colors.BOLD}{Colors.BRIGHT_CYAN}╔{'═' * BOX_WIDTH}╗{Colors.RESET}")
183
+ print(
184
+ f"{Colors.BOLD}{Colors.BRIGHT_CYAN}║{Colors.RESET}{' ' * left_padding}{banner_text}{' ' * right_padding}{Colors.BOLD}{Colors.BRIGHT_CYAN}║{Colors.RESET}"
185
+ )
186
+ print(f"{Colors.BOLD}{Colors.BRIGHT_CYAN}╚{'═' * BOX_WIDTH}╝{Colors.RESET}")
187
+ print()
188
+
189
+
190
+ def print_help():
191
+ """Print help information"""
192
+ help_text = f"""
193
+ {Colors.BOLD}{Colors.BRIGHT_YELLOW}Available Commands:{Colors.RESET}
194
+ {Colors.BRIGHT_GREEN}/help{Colors.RESET} - Show this help message
195
+ {Colors.BRIGHT_GREEN}/clear{Colors.RESET} - Clear session history (keep system prompt)
196
+ {Colors.BRIGHT_GREEN}/history{Colors.RESET} - Show current session message count
197
+ {Colors.BRIGHT_GREEN}/stats{Colors.RESET} - Show session statistics
198
+ {Colors.BRIGHT_GREEN}/log{Colors.RESET} - Show log directory and recent files
199
+ {Colors.BRIGHT_GREEN}/log <file>{Colors.RESET} - Read a specific log file
200
+ {Colors.BRIGHT_GREEN}/exit{Colors.RESET} - Exit program (also: exit, quit, q)
201
+
202
+ {Colors.BOLD}{Colors.BRIGHT_YELLOW}Keyboard Shortcuts:{Colors.RESET}
203
+ {Colors.BRIGHT_CYAN}Esc{Colors.RESET} - Cancel current agent execution
204
+ {Colors.BRIGHT_CYAN}Ctrl+C{Colors.RESET} - Exit program
205
+ {Colors.BRIGHT_CYAN}Ctrl+U{Colors.RESET} - Clear current input line
206
+ {Colors.BRIGHT_CYAN}Ctrl+L{Colors.RESET} - Clear screen
207
+ {Colors.BRIGHT_CYAN}Ctrl+J{Colors.RESET} - Insert newline (also Ctrl+Enter)
208
+ {Colors.BRIGHT_CYAN}Tab{Colors.RESET} - Auto-complete commands
209
+ {Colors.BRIGHT_CYAN}↑/↓{Colors.RESET} - Browse command history
210
+ {Colors.BRIGHT_CYAN}→{Colors.RESET} - Accept auto-suggestion
211
+
212
+ {Colors.BOLD}{Colors.BRIGHT_YELLOW}Usage:{Colors.RESET}
213
+ - Enter your task directly, Agent will help you complete it
214
+ - Agent remembers all conversation content in this session
215
+ - Use {Colors.BRIGHT_GREEN}/clear{Colors.RESET} to start a new session
216
+ - Press {Colors.BRIGHT_CYAN}Enter{Colors.RESET} to submit your message
217
+ - Use {Colors.BRIGHT_CYAN}Ctrl+J{Colors.RESET} to insert line breaks within your message
218
+ """
219
+ print(help_text)
220
+
221
+
222
+ def print_session_info(agent: Agent, workspace_dir: Path, model: str):
223
+ """Print session information with proper alignment"""
224
+ BOX_WIDTH = 58
225
+
226
+ def print_info_line(text: str):
227
+ """Print a single info line with proper padding"""
228
+ # Account for leading space
229
+ text_width = calculate_display_width(text)
230
+ padding = max(0, BOX_WIDTH - 1 - text_width)
231
+ print(f"{Colors.DIM}│{Colors.RESET} {text}{' ' * padding}{Colors.DIM}│{Colors.RESET}")
232
+
233
+ # Top border
234
+ print(f"{Colors.DIM}┌{'─' * BOX_WIDTH}┐{Colors.RESET}")
235
+
236
+ # Header (centered)
237
+ header_text = f"{Colors.BRIGHT_CYAN}Session Info{Colors.RESET}"
238
+ header_width = calculate_display_width(header_text)
239
+ header_padding_total = BOX_WIDTH - 1 - header_width # -1 for leading space
240
+ header_padding_left = header_padding_total // 2
241
+ header_padding_right = header_padding_total - header_padding_left
242
+ print(f"{Colors.DIM}│{Colors.RESET} {' ' * header_padding_left}{header_text}{' ' * header_padding_right}{Colors.DIM}│{Colors.RESET}")
243
+
244
+ # Divider
245
+ print(f"{Colors.DIM}├{'─' * BOX_WIDTH}┤{Colors.RESET}")
246
+
247
+ # Info lines
248
+ print_info_line(f"Model: {model}")
249
+ print_info_line(f"Workspace: {workspace_dir}")
250
+ print_info_line(f"Message History: {len(agent.messages)} messages")
251
+ print_info_line(f"Available Tools: {len(agent.tools)} tools")
252
+
253
+ # Bottom border
254
+ print(f"{Colors.DIM}└{'─' * BOX_WIDTH}┘{Colors.RESET}")
255
+ print()
256
+ print(f"{Colors.DIM}Type {Colors.BRIGHT_GREEN}/help{Colors.DIM} for help, {Colors.BRIGHT_GREEN}/exit{Colors.DIM} to quit{Colors.RESET}")
257
+ print()
258
+
259
+
260
+ def print_stats(agent: Agent, session_start: datetime):
261
+ """Print session statistics"""
262
+ duration = datetime.now() - session_start
263
+ hours, remainder = divmod(int(duration.total_seconds()), 3600)
264
+ minutes, seconds = divmod(remainder, 60)
265
+
266
+ # Count different types of messages
267
+ user_msgs = sum(1 for m in agent.messages if m.role == "user")
268
+ assistant_msgs = sum(1 for m in agent.messages if m.role == "assistant")
269
+ tool_msgs = sum(1 for m in agent.messages if m.role == "tool")
270
+
271
+ print(f"\n{Colors.BOLD}{Colors.BRIGHT_CYAN}Session Statistics:{Colors.RESET}")
272
+ print(f"{Colors.DIM}{'─' * 40}{Colors.RESET}")
273
+ print(f" Session Duration: {hours:02d}:{minutes:02d}:{seconds:02d}")
274
+ print(f" Total Messages: {len(agent.messages)}")
275
+ print(f" - User Messages: {Colors.BRIGHT_GREEN}{user_msgs}{Colors.RESET}")
276
+ print(f" - Assistant Replies: {Colors.BRIGHT_BLUE}{assistant_msgs}{Colors.RESET}")
277
+ print(f" - Tool Calls: {Colors.BRIGHT_YELLOW}{tool_msgs}{Colors.RESET}")
278
+ print(f" Available Tools: {len(agent.tools)}")
279
+ if agent.api_total_tokens > 0:
280
+ print(f" API Tokens Used: {Colors.BRIGHT_MAGENTA}{agent.api_total_tokens:,}{Colors.RESET}")
281
+ print(f"{Colors.DIM}{'─' * 40}{Colors.RESET}\n")
282
+
283
+
284
+ def parse_args() -> argparse.Namespace:
285
+ """Parse command line arguments
286
+
287
+ Returns:
288
+ Parsed arguments
289
+ """
290
+ parser = argparse.ArgumentParser(
291
+ description="Mini Agent - AI assistant with file tools and MCP support",
292
+ formatter_class=argparse.RawDescriptionHelpFormatter,
293
+ epilog="""
294
+ Examples:
295
+ mini-agent # Use current directory as workspace
296
+ mini-agent --workspace /path/to/dir # Use specific workspace directory
297
+ mini-agent log # Show log directory and recent files
298
+ mini-agent log agent_run_xxx.log # Read a specific log file
299
+ """,
300
+ )
301
+ parser.add_argument(
302
+ "--workspace",
303
+ "-w",
304
+ type=str,
305
+ default=None,
306
+ help="Workspace directory (default: current directory)",
307
+ )
308
+ parser.add_argument(
309
+ "--version",
310
+ "-v",
311
+ action="version",
312
+ version="mini-agent 0.1.0",
313
+ )
314
+
315
+ # Subcommands
316
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
317
+
318
+ # log subcommand
319
+ log_parser = subparsers.add_parser("log", help="Show log directory or read log files")
320
+ log_parser.add_argument(
321
+ "filename",
322
+ nargs="?",
323
+ default=None,
324
+ help="Log filename to read (optional, shows directory if omitted)",
325
+ )
326
+
327
+ return parser.parse_args()
328
+
329
+
330
+ async def initialize_base_tools(config: Config):
331
+ """Initialize base tools (independent of workspace)
332
+
333
+ These tools are loaded from package configuration and don't depend on workspace.
334
+ Note: File tools are now workspace-dependent and initialized in add_workspace_tools()
335
+
336
+ Args:
337
+ config: Configuration object
338
+
339
+ Returns:
340
+ Tuple of (list of tools, skill loader if skills enabled)
341
+ """
342
+
343
+ tools = []
344
+ skill_loader = None
345
+
346
+ # 1. Bash tool and Bash Output tool
347
+ if config.tools.enable_bash:
348
+ bash_tool = BashTool()
349
+ tools.append(bash_tool)
350
+ print(f"{Colors.GREEN}✅ Loaded Bash tool{Colors.RESET}")
351
+
352
+ bash_output_tool = BashOutputTool()
353
+ tools.append(bash_output_tool)
354
+ print(f"{Colors.GREEN}✅ Loaded Bash Output tool{Colors.RESET}")
355
+
356
+ bash_kill_tool = BashKillTool()
357
+ tools.append(bash_kill_tool)
358
+ print(f"{Colors.GREEN}✅ Loaded Bash Kill tool{Colors.RESET}")
359
+
360
+ # 3. Claude Skills (loaded from package directory)
361
+ if config.tools.enable_skills:
362
+ print(f"{Colors.BRIGHT_CYAN}Loading Claude Skills...{Colors.RESET}")
363
+ try:
364
+ # Resolve skills directory with priority search
365
+ # Expand ~ to user home directory for portability
366
+ skills_path = Path(config.tools.skills_dir).expanduser()
367
+ if skills_path.is_absolute():
368
+ skills_dir = str(skills_path)
369
+ else:
370
+ # Search in priority order:
371
+ # 1. Current directory (dev mode: ./skills or ./mini_agent/skills)
372
+ # 2. Package directory (installed: site-packages/mini_agent/skills)
373
+ search_paths = [
374
+ skills_path, # ./skills for backward compatibility
375
+ Path("mini_agent") / skills_path, # ./mini_agent/skills
376
+ Config.get_package_dir() / skills_path, # site-packages/mini_agent/skills
377
+ ]
378
+
379
+ # Find first existing path
380
+ skills_dir = str(skills_path) # default
381
+ for path in search_paths:
382
+ if path.exists():
383
+ skills_dir = str(path.resolve())
384
+ break
385
+
386
+ skill_tools, skill_loader = create_skill_tools(skills_dir)
387
+ if skill_tools:
388
+ tools.extend(skill_tools)
389
+ print(f"{Colors.GREEN}✅ Loaded Skill tool (get_skill){Colors.RESET}")
390
+ else:
391
+ print(f"{Colors.YELLOW}⚠️ No available Skills found{Colors.RESET}")
392
+ except Exception as e:
393
+ print(f"{Colors.YELLOW}⚠️ Failed to load Skills: {e}{Colors.RESET}")
394
+
395
+ # 4. MCP tools (loaded with priority search)
396
+ if config.tools.enable_mcp:
397
+ print(f"{Colors.BRIGHT_CYAN}Loading MCP tools...{Colors.RESET}")
398
+ try:
399
+ # Apply MCP timeout configuration from config.yaml
400
+ mcp_config = config.tools.mcp
401
+ set_mcp_timeout_config(
402
+ connect_timeout=mcp_config.connect_timeout,
403
+ execute_timeout=mcp_config.execute_timeout,
404
+ sse_read_timeout=mcp_config.sse_read_timeout,
405
+ )
406
+ print(
407
+ f"{Colors.DIM} MCP timeouts: connect={mcp_config.connect_timeout}s, "
408
+ f"execute={mcp_config.execute_timeout}s, sse_read={mcp_config.sse_read_timeout}s{Colors.RESET}"
409
+ )
410
+
411
+ # Use priority search for mcp.json
412
+ mcp_config_path = Config.find_config_file(config.tools.mcp_config_path)
413
+ if mcp_config_path:
414
+ mcp_tools = await load_mcp_tools_async(str(mcp_config_path))
415
+ if mcp_tools:
416
+ tools.extend(mcp_tools)
417
+ print(f"{Colors.GREEN}✅ Loaded {len(mcp_tools)} MCP tools (from: {mcp_config_path}){Colors.RESET}")
418
+ else:
419
+ print(f"{Colors.YELLOW}⚠️ No available MCP tools found{Colors.RESET}")
420
+ else:
421
+ print(f"{Colors.YELLOW}⚠️ MCP config file not found: {config.tools.mcp_config_path}{Colors.RESET}")
422
+ except Exception as e:
423
+ print(f"{Colors.YELLOW}⚠️ Failed to load MCP tools: {e}{Colors.RESET}")
424
+
425
+ print() # Empty line separator
426
+ return tools, skill_loader
427
+
428
+
429
+ def add_workspace_tools(tools: List[Tool], config: Config, workspace_dir: Path):
430
+ """Add workspace-dependent tools
431
+
432
+ These tools need to know the workspace directory.
433
+
434
+ Args:
435
+ tools: Existing tools list to add to
436
+ config: Configuration object
437
+ workspace_dir: Workspace directory path
438
+ """
439
+ # Ensure workspace directory exists
440
+ workspace_dir.mkdir(parents=True, exist_ok=True)
441
+
442
+ # File tools - need workspace to resolve relative paths
443
+ if config.tools.enable_file_tools:
444
+ tools.extend(
445
+ [
446
+ ReadTool(workspace_dir=str(workspace_dir)),
447
+ WriteTool(workspace_dir=str(workspace_dir)),
448
+ EditTool(workspace_dir=str(workspace_dir)),
449
+ ]
450
+ )
451
+ print(f"{Colors.GREEN}✅ Loaded file operation tools (workspace: {workspace_dir}){Colors.RESET}")
452
+
453
+ # Session note tool - needs workspace to store memory file
454
+ if config.tools.enable_note:
455
+ tools.append(SessionNoteTool(memory_file=str(workspace_dir / ".agent_memory.json")))
456
+ print(f"{Colors.GREEN}✅ Loaded session note tool{Colors.RESET}")
457
+
458
+
459
+ async def run_agent(workspace_dir: Path):
460
+ """Run interactive Agent
461
+
462
+ Args:
463
+ workspace_dir: Workspace directory path
464
+ """
465
+ session_start = datetime.now()
466
+
467
+ # 1. Load configuration from package directory
468
+ config_path = Config.get_default_config_path()
469
+
470
+ if not config_path.exists():
471
+ print(f"{Colors.RED}❌ Configuration file not found{Colors.RESET}")
472
+ print()
473
+ print(f"{Colors.BRIGHT_CYAN}📦 Configuration Search Path:{Colors.RESET}")
474
+ print(f" {Colors.DIM}1) mini_agent/config/config.yaml{Colors.RESET} (development)")
475
+ print(f" {Colors.DIM}2) ~/.mini-agent/config/config.yaml{Colors.RESET} (user)")
476
+ print(f" {Colors.DIM}3) <package>/config/config.yaml{Colors.RESET} (installed)")
477
+ print()
478
+ print(f"{Colors.BRIGHT_YELLOW}🚀 Quick Setup (Recommended):{Colors.RESET}")
479
+ print(
480
+ f" {Colors.BRIGHT_GREEN}curl -fsSL https://raw.githubusercontent.com/MiniMax-AI/Mini-Agent/main/scripts/setup-config.sh | bash{Colors.RESET}"
481
+ )
482
+ print()
483
+ print(f"{Colors.DIM} This will automatically:{Colors.RESET}")
484
+ print(f"{Colors.DIM} • Create ~/.mini-agent/config/{Colors.RESET}")
485
+ print(f"{Colors.DIM} • Download configuration files{Colors.RESET}")
486
+ print(f"{Colors.DIM} • Guide you to add your API Key{Colors.RESET}")
487
+ print()
488
+ print(f"{Colors.BRIGHT_YELLOW}📝 Manual Setup:{Colors.RESET}")
489
+ user_config_dir = Path.home() / ".mini-agent" / "config"
490
+ example_config = Config.get_package_dir() / "config" / "config-example.yaml"
491
+ print(f" {Colors.DIM}mkdir -p {user_config_dir}{Colors.RESET}")
492
+ print(f" {Colors.DIM}cp {example_config} {user_config_dir}/config.yaml{Colors.RESET}")
493
+ print(f" {Colors.DIM}# Then edit {user_config_dir}/config.yaml to add your API Key{Colors.RESET}")
494
+ print()
495
+ return
496
+
497
+ try:
498
+ config = Config.from_yaml(config_path)
499
+ except FileNotFoundError:
500
+ print(f"{Colors.RED}❌ Error: Configuration file not found: {config_path}{Colors.RESET}")
501
+ return
502
+ except ValueError as e:
503
+ print(f"{Colors.RED}❌ Error: {e}{Colors.RESET}")
504
+ print(f"{Colors.YELLOW}Please check the configuration file format{Colors.RESET}")
505
+ return
506
+ except Exception as e:
507
+ print(f"{Colors.RED}❌ Error: Failed to load configuration file: {e}{Colors.RESET}")
508
+ return
509
+
510
+ # 2. Initialize LLM client
511
+ from mini_agent.retry import RetryConfig as RetryConfigBase
512
+
513
+ # Convert configuration format
514
+ retry_config = RetryConfigBase(
515
+ enabled=config.llm.retry.enabled,
516
+ max_retries=config.llm.retry.max_retries,
517
+ initial_delay=config.llm.retry.initial_delay,
518
+ max_delay=config.llm.retry.max_delay,
519
+ exponential_base=config.llm.retry.exponential_base,
520
+ retryable_exceptions=(Exception,),
521
+ )
522
+
523
+ # Create retry callback function to display retry information in terminal
524
+ def on_retry(exception: Exception, attempt: int):
525
+ """Retry callback function to display retry information"""
526
+ print(f"\n{Colors.BRIGHT_YELLOW}⚠️ LLM call failed (attempt {attempt}): {str(exception)}{Colors.RESET}")
527
+ next_delay = retry_config.calculate_delay(attempt - 1)
528
+ print(f"{Colors.DIM} Retrying in {next_delay:.1f}s (attempt {attempt + 1})...{Colors.RESET}")
529
+
530
+ # Convert provider string to LLMProvider enum
531
+ provider = LLMProvider.ANTHROPIC if config.llm.provider.lower() == "anthropic" else LLMProvider.OPENAI
532
+
533
+ llm_client = LLMClient(
534
+ api_key=config.llm.api_key,
535
+ provider=provider,
536
+ api_base=config.llm.api_base,
537
+ model=config.llm.model,
538
+ retry_config=retry_config if config.llm.retry.enabled else None,
539
+ )
540
+
541
+ # Set retry callback
542
+ if config.llm.retry.enabled:
543
+ llm_client.retry_callback = on_retry
544
+ print(f"{Colors.GREEN}✅ LLM retry mechanism enabled (max {config.llm.retry.max_retries} retries){Colors.RESET}")
545
+
546
+ # 3. Initialize base tools (independent of workspace)
547
+ tools, skill_loader = await initialize_base_tools(config)
548
+
549
+ # 4. Add workspace-dependent tools
550
+ add_workspace_tools(tools, config, workspace_dir)
551
+
552
+ # 5. Load System Prompt (with priority search)
553
+ system_prompt_path = Config.find_config_file(config.agent.system_prompt_path)
554
+ if system_prompt_path and system_prompt_path.exists():
555
+ system_prompt = system_prompt_path.read_text(encoding="utf-8")
556
+ print(f"{Colors.GREEN}✅ Loaded system prompt (from: {system_prompt_path}){Colors.RESET}")
557
+ else:
558
+ system_prompt = "You are Mini-Agent, an intelligent assistant powered by MiniMax M2.1 that can help users complete various tasks."
559
+ print(f"{Colors.YELLOW}⚠️ System prompt not found, using default{Colors.RESET}")
560
+
561
+ # 6. Inject Skills Metadata into System Prompt (Progressive Disclosure - Level 1)
562
+ if skill_loader:
563
+ skills_metadata = skill_loader.get_skills_metadata_prompt()
564
+ if skills_metadata:
565
+ # Replace placeholder with actual metadata
566
+ system_prompt = system_prompt.replace("{SKILLS_METADATA}", skills_metadata)
567
+ print(f"{Colors.GREEN}✅ Injected {len(skill_loader.loaded_skills)} skills metadata into system prompt{Colors.RESET}")
568
+ else:
569
+ # Remove placeholder if no skills
570
+ system_prompt = system_prompt.replace("{SKILLS_METADATA}", "")
571
+ else:
572
+ # Remove placeholder if skills not enabled
573
+ system_prompt = system_prompt.replace("{SKILLS_METADATA}", "")
574
+
575
+ # 7. Create Agent
576
+ agent = Agent(
577
+ llm_client=llm_client,
578
+ system_prompt=system_prompt,
579
+ tools=tools,
580
+ max_steps=config.agent.max_steps,
581
+ workspace_dir=str(workspace_dir),
582
+ )
583
+
584
+ # 8. Display welcome information
585
+ print_banner()
586
+ print_session_info(agent, workspace_dir, config.llm.model)
587
+
588
+ # 9. Setup prompt_toolkit session
589
+ # Command completer
590
+ command_completer = WordCompleter(
591
+ ["/help", "/clear", "/history", "/stats", "/log", "/exit", "/quit", "/q"],
592
+ ignore_case=True,
593
+ sentence=True,
594
+ )
595
+
596
+ # Custom style for prompt
597
+ prompt_style = Style.from_dict(
598
+ {
599
+ "prompt": "#00ff00 bold", # Green and bold
600
+ "separator": "#666666", # Gray
601
+ }
602
+ )
603
+
604
+ # Custom key bindings
605
+ kb = KeyBindings()
606
+
607
+ @kb.add("c-u") # Ctrl+U: Clear current line
608
+ def _(event):
609
+ """Clear the current input line"""
610
+ event.current_buffer.reset()
611
+
612
+ @kb.add("c-l") # Ctrl+L: Clear screen (optional bonus)
613
+ def _(event):
614
+ """Clear the screen"""
615
+ event.app.renderer.clear()
616
+
617
+ @kb.add("c-j") # Ctrl+J (对应 Ctrl+Enter)
618
+ def _(event):
619
+ """Insert a newline"""
620
+ event.current_buffer.insert_text("\n")
621
+
622
+ # Create prompt session with history and auto-suggest
623
+ # Use FileHistory for persistent history across sessions (stored in user's home directory)
624
+ history_file = Path.home() / ".mini-agent" / ".history"
625
+ history_file.parent.mkdir(parents=True, exist_ok=True)
626
+ session = PromptSession(
627
+ history=FileHistory(str(history_file)),
628
+ auto_suggest=AutoSuggestFromHistory(),
629
+ completer=command_completer,
630
+ style=prompt_style,
631
+ key_bindings=kb,
632
+ )
633
+
634
+ # 10. Interactive loop
635
+ while True:
636
+ try:
637
+ # Get user input using prompt_toolkit
638
+ user_input = await session.prompt_async(
639
+ [
640
+ ("class:prompt", "You"),
641
+ ("", " › "),
642
+ ],
643
+ multiline=False,
644
+ enable_history_search=True,
645
+ )
646
+ user_input = user_input.strip()
647
+
648
+ if not user_input:
649
+ continue
650
+
651
+ # Handle commands
652
+ if user_input.startswith("/"):
653
+ command = user_input.lower()
654
+
655
+ if command in ["/exit", "/quit", "/q"]:
656
+ print(f"\n{Colors.BRIGHT_YELLOW}👋 Goodbye! Thanks for using Mini Agent{Colors.RESET}\n")
657
+ print_stats(agent, session_start)
658
+ break
659
+
660
+ elif command == "/help":
661
+ print_help()
662
+ continue
663
+
664
+ elif command == "/clear":
665
+ # Clear message history but keep system prompt
666
+ old_count = len(agent.messages)
667
+ agent.messages = [agent.messages[0]] # Keep only system message
668
+ print(f"{Colors.GREEN}✅ Cleared {old_count - 1} messages, starting new session{Colors.RESET}\n")
669
+ continue
670
+
671
+ elif command == "/history":
672
+ print(f"\n{Colors.BRIGHT_CYAN}Current session message count: {len(agent.messages)}{Colors.RESET}\n")
673
+ continue
674
+
675
+ elif command == "/stats":
676
+ print_stats(agent, session_start)
677
+ continue
678
+
679
+ elif command == "/log" or command.startswith("/log "):
680
+ # Parse /log command
681
+ parts = user_input.split(maxsplit=1)
682
+ if len(parts) == 1:
683
+ # /log - show log directory
684
+ show_log_directory(open_file_manager=True)
685
+ else:
686
+ # /log <filename> - read specific log file
687
+ filename = parts[1].strip("\"'")
688
+ read_log_file(filename)
689
+ continue
690
+
691
+ else:
692
+ print(f"{Colors.RED}❌ Unknown command: {user_input}{Colors.RESET}")
693
+ print(f"{Colors.DIM}Type /help to see available commands{Colors.RESET}\n")
694
+ continue
695
+
696
+ # Normal conversation - exit check
697
+ if user_input.lower() in ["exit", "quit", "q"]:
698
+ print(f"\n{Colors.BRIGHT_YELLOW}👋 Goodbye! Thanks for using Mini Agent{Colors.RESET}\n")
699
+ print_stats(agent, session_start)
700
+ break
701
+
702
+ # Run Agent with Esc cancellation support
703
+ print(
704
+ f"\n{Colors.BRIGHT_BLUE}Agent{Colors.RESET} {Colors.DIM}›{Colors.RESET} {Colors.DIM}Thinking... (Esc to cancel){Colors.RESET}\n"
705
+ )
706
+ agent.add_user_message(user_input)
707
+
708
+ # Create cancellation event
709
+ cancel_event = asyncio.Event()
710
+ agent.cancel_event = cancel_event
711
+
712
+ # Esc key listener thread
713
+ esc_listener_stop = threading.Event()
714
+ esc_cancelled = [False] # Mutable container for thread access
715
+
716
+ def esc_key_listener():
717
+ """Listen for Esc key in a separate thread."""
718
+ if platform.system() == "Windows":
719
+ try:
720
+ import msvcrt
721
+
722
+ while not esc_listener_stop.is_set():
723
+ if msvcrt.kbhit():
724
+ char = msvcrt.getch()
725
+ if char == b"\x1b": # Esc
726
+ print(f"\n{Colors.BRIGHT_YELLOW}⏹️ Esc pressed, cancelling...{Colors.RESET}")
727
+ esc_cancelled[0] = True
728
+ cancel_event.set()
729
+ break
730
+ esc_listener_stop.wait(0.05)
731
+ except Exception:
732
+ pass
733
+ return
734
+
735
+ # Unix/macOS
736
+ try:
737
+ import select
738
+ import termios
739
+ import tty
740
+
741
+ fd = sys.stdin.fileno()
742
+ old_settings = termios.tcgetattr(fd)
743
+
744
+ try:
745
+ tty.setcbreak(fd)
746
+ while not esc_listener_stop.is_set():
747
+ rlist, _, _ = select.select([sys.stdin], [], [], 0.05)
748
+ if rlist:
749
+ char = sys.stdin.read(1)
750
+ if char == "\x1b": # Esc
751
+ print(f"\n{Colors.BRIGHT_YELLOW}⏹️ Esc pressed, cancelling...{Colors.RESET}")
752
+ esc_cancelled[0] = True
753
+ cancel_event.set()
754
+ break
755
+ finally:
756
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
757
+ except Exception:
758
+ pass
759
+
760
+ # Start Esc listener thread
761
+ esc_thread = threading.Thread(target=esc_key_listener, daemon=True)
762
+ esc_thread.start()
763
+
764
+ # Run agent with periodic cancellation check
765
+ try:
766
+ agent_task = asyncio.create_task(agent.run())
767
+
768
+ # Poll for cancellation while agent runs
769
+ while not agent_task.done():
770
+ if esc_cancelled[0]:
771
+ cancel_event.set()
772
+ await asyncio.sleep(0.1)
773
+
774
+ # Get result
775
+ _ = agent_task.result()
776
+
777
+ except asyncio.CancelledError:
778
+ print(f"\n{Colors.BRIGHT_YELLOW}⚠️ Agent execution cancelled{Colors.RESET}")
779
+ finally:
780
+ agent.cancel_event = None
781
+ esc_listener_stop.set()
782
+ esc_thread.join(timeout=0.2)
783
+
784
+ # Visual separation
785
+ print(f"\n{Colors.DIM}{'─' * 60}{Colors.RESET}\n")
786
+
787
+ except KeyboardInterrupt:
788
+ print(f"\n\n{Colors.BRIGHT_YELLOW}👋 Interrupt signal detected, exiting...{Colors.RESET}\n")
789
+ print_stats(agent, session_start)
790
+ break
791
+
792
+ except Exception as e:
793
+ print(f"\n{Colors.RED}❌ Error: {e}{Colors.RESET}")
794
+ print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n")
795
+
796
+ # 11. Cleanup MCP connections
797
+ try:
798
+ print(f"{Colors.BRIGHT_CYAN}Cleaning up MCP connections...{Colors.RESET}")
799
+ await cleanup_mcp_connections()
800
+ print(f"{Colors.GREEN}✅ Cleanup complete{Colors.RESET}\n")
801
+ except Exception as e:
802
+ print(f"{Colors.YELLOW}Error during cleanup (can be ignored): {e}{Colors.RESET}\n")
803
+
804
+
805
+ def main():
806
+ """Main entry point for CLI"""
807
+ # Parse command line arguments
808
+ args = parse_args()
809
+
810
+ # Handle log subcommand
811
+ if args.command == "log":
812
+ if args.filename:
813
+ read_log_file(args.filename)
814
+ else:
815
+ show_log_directory(open_file_manager=True)
816
+ return
817
+
818
+ # Determine workspace directory
819
+ # Expand ~ to user home directory for portability
820
+ if args.workspace:
821
+ workspace_dir = Path(args.workspace).expanduser().absolute()
822
+ else:
823
+ # Use current working directory
824
+ workspace_dir = Path.cwd()
825
+
826
+ # Ensure workspace directory exists
827
+ workspace_dir.mkdir(parents=True, exist_ok=True)
828
+
829
+ # Run the agent (config always loaded from package directory)
830
+ asyncio.run(run_agent(workspace_dir))
831
+
832
+
833
+ if __name__ == "__main__":
834
+ main()
mini_agent/config.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuration management module
2
+
3
+ Provides unified configuration loading and management functionality
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+ import yaml
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class RetryConfig(BaseModel):
13
+ """Retry configuration"""
14
+
15
+ enabled: bool = True
16
+ max_retries: int = 3
17
+ initial_delay: float = 1.0
18
+ max_delay: float = 60.0
19
+ exponential_base: float = 2.0
20
+
21
+
22
+ class LLMConfig(BaseModel):
23
+ """LLM configuration"""
24
+
25
+ api_key: str
26
+ api_base: str = "https://api.minimax.io"
27
+ model: str = "MiniMax-M2.1"
28
+ provider: str = "anthropic" # "anthropic" or "openai"
29
+ retry: RetryConfig = Field(default_factory=RetryConfig)
30
+
31
+
32
+ class AgentConfig(BaseModel):
33
+ """Agent configuration"""
34
+
35
+ max_steps: int = 50
36
+ workspace_dir: str = "./workspace"
37
+ system_prompt_path: str = "system_prompt.md"
38
+
39
+
40
+ class MCPConfig(BaseModel):
41
+ """MCP (Model Context Protocol) timeout configuration"""
42
+
43
+ connect_timeout: float = 10.0 # Connection timeout (seconds)
44
+ execute_timeout: float = 60.0 # Tool execution timeout (seconds)
45
+ sse_read_timeout: float = 120.0 # SSE read timeout (seconds)
46
+
47
+
48
+ class ToolsConfig(BaseModel):
49
+ """Tools configuration"""
50
+
51
+ # Basic tools (file operations, bash)
52
+ enable_file_tools: bool = True
53
+ enable_bash: bool = True
54
+ enable_note: bool = True
55
+
56
+ # Skills
57
+ enable_skills: bool = True
58
+ skills_dir: str = "./skills"
59
+
60
+ # MCP tools
61
+ enable_mcp: bool = True
62
+ mcp_config_path: str = "mcp.json"
63
+ mcp: MCPConfig = Field(default_factory=MCPConfig)
64
+
65
+
66
+ class Config(BaseModel):
67
+ """Main configuration class"""
68
+
69
+ llm: LLMConfig
70
+ agent: AgentConfig
71
+ tools: ToolsConfig
72
+
73
+ @classmethod
74
+ def load(cls) -> "Config":
75
+ """Load configuration from the default search path."""
76
+ config_path = cls.get_default_config_path()
77
+ if not config_path.exists():
78
+ raise FileNotFoundError("Configuration file not found. Run scripts/setup-config.sh or place config.yaml in mini_agent/config/.")
79
+ return cls.from_yaml(config_path)
80
+
81
+ @classmethod
82
+ def from_yaml(cls, config_path: str | Path) -> "Config":
83
+ """Load configuration from YAML file
84
+
85
+ Args:
86
+ config_path: Configuration file path
87
+
88
+ Returns:
89
+ Config instance
90
+
91
+ Raises:
92
+ FileNotFoundError: Configuration file does not exist
93
+ ValueError: Invalid configuration format or missing required fields
94
+ """
95
+ config_path = Path(config_path)
96
+
97
+ if not config_path.exists():
98
+ raise FileNotFoundError(f"Configuration file does not exist: {config_path}")
99
+
100
+ with open(config_path, encoding="utf-8") as f:
101
+ data = yaml.safe_load(f)
102
+
103
+ if not data:
104
+ raise ValueError("Configuration file is empty")
105
+
106
+ # Parse LLM configuration
107
+ if "api_key" not in data:
108
+ raise ValueError("Configuration file missing required field: api_key")
109
+
110
+ if not data["api_key"] or data["api_key"] == "YOUR_API_KEY_HERE":
111
+ raise ValueError("Please configure a valid API Key")
112
+
113
+ # Parse retry configuration
114
+ retry_data = data.get("retry", {})
115
+ retry_config = RetryConfig(
116
+ enabled=retry_data.get("enabled", True),
117
+ max_retries=retry_data.get("max_retries", 3),
118
+ initial_delay=retry_data.get("initial_delay", 1.0),
119
+ max_delay=retry_data.get("max_delay", 60.0),
120
+ exponential_base=retry_data.get("exponential_base", 2.0),
121
+ )
122
+
123
+ llm_config = LLMConfig(
124
+ api_key=data["api_key"],
125
+ api_base=data.get("api_base", "https://api.minimax.io"),
126
+ model=data.get("model", "MiniMax-M2.1"),
127
+ provider=data.get("provider", "anthropic"),
128
+ retry=retry_config,
129
+ )
130
+
131
+ # Parse Agent configuration
132
+ agent_config = AgentConfig(
133
+ max_steps=data.get("max_steps", 50),
134
+ workspace_dir=data.get("workspace_dir", "./workspace"),
135
+ system_prompt_path=data.get("system_prompt_path", "system_prompt.md"),
136
+ )
137
+
138
+ # Parse tools configuration
139
+ tools_data = data.get("tools", {})
140
+
141
+ # Parse MCP configuration
142
+ mcp_data = tools_data.get("mcp", {})
143
+ mcp_config = MCPConfig(
144
+ connect_timeout=mcp_data.get("connect_timeout", 10.0),
145
+ execute_timeout=mcp_data.get("execute_timeout", 60.0),
146
+ sse_read_timeout=mcp_data.get("sse_read_timeout", 120.0),
147
+ )
148
+
149
+ tools_config = ToolsConfig(
150
+ enable_file_tools=tools_data.get("enable_file_tools", True),
151
+ enable_bash=tools_data.get("enable_bash", True),
152
+ enable_note=tools_data.get("enable_note", True),
153
+ enable_skills=tools_data.get("enable_skills", True),
154
+ skills_dir=tools_data.get("skills_dir", "./skills"),
155
+ enable_mcp=tools_data.get("enable_mcp", True),
156
+ mcp_config_path=tools_data.get("mcp_config_path", "mcp.json"),
157
+ mcp=mcp_config,
158
+ )
159
+
160
+ return cls(
161
+ llm=llm_config,
162
+ agent=agent_config,
163
+ tools=tools_config,
164
+ )
165
+
166
+ @staticmethod
167
+ def get_package_dir() -> Path:
168
+ """Get the package installation directory
169
+
170
+ Returns:
171
+ Path to the mini_agent package directory
172
+ """
173
+ # Get the directory where this config.py file is located
174
+ return Path(__file__).parent
175
+
176
+ @classmethod
177
+ def find_config_file(cls, filename: str) -> Path | None:
178
+ """Find configuration file with priority order
179
+
180
+ Search for config file in the following order of priority:
181
+ 1) mini_agent/config/{filename} in current directory (development mode)
182
+ 2) ~/.mini-agent/config/{filename} in user home directory
183
+ 3) {package}/mini_agent/config/{filename} in package installation directory
184
+
185
+ Args:
186
+ filename: Configuration file name (e.g., "config.yaml", "mcp.json", "system_prompt.md")
187
+
188
+ Returns:
189
+ Path to found config file, or None if not found
190
+ """
191
+ # Priority 1: Development mode - current directory's config/ subdirectory
192
+ dev_config = Path.cwd() / "mini_agent" / "config" / filename
193
+ if dev_config.exists():
194
+ return dev_config
195
+
196
+ # Priority 2: User config directory
197
+ user_config = Path.home() / ".mini-agent" / "config" / filename
198
+ if user_config.exists():
199
+ return user_config
200
+
201
+ # Priority 3: Package installation directory's config/ subdirectory
202
+ package_config = cls.get_package_dir() / "config" / filename
203
+ if package_config.exists():
204
+ return package_config
205
+
206
+ return None
207
+
208
+ @classmethod
209
+ def get_default_config_path(cls) -> Path:
210
+ """Get the default config file path with priority search
211
+
212
+ Returns:
213
+ Path to config.yaml (prioritizes: dev config/ > user config/ > package config/)
214
+ """
215
+ config_path = cls.find_config_file("config.yaml")
216
+ if config_path:
217
+ return config_path
218
+
219
+ # Fallback to package config directory for error message purposes
220
+ return cls.get_package_dir() / "config" / "config.yaml"
mini_agent/config/config-example.yaml ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mini Agent Configuration Example
2
+ #
3
+ # Configuration File Locations (in priority order):
4
+ # 1) mini_agent/config/config.yaml - Development mode (current directory)
5
+ # 2) ~/.mini-agent/config/config.yaml - User config directory
6
+ # 3) <package>/mini_agent/config/config.yaml - Package installation directory
7
+ #
8
+ # To use this config:
9
+ # - Copy this file to one of the above locations as config.yaml
10
+ # - Fill in your API key and customize settings as needed
11
+ # - All config files (config.yaml, mcp.json, system_prompt.md) are in the same directory
12
+
13
+ # ===== LLM Configuration =====
14
+ # MiniMax API Configuration
15
+ # MiniMax provides both global and China platforms:
16
+ # - Global: https://platform.minimax.io -> api_base: https://api.minimax.io
17
+ # - China: https://platform.minimaxi.com -> api_base: https://api.minimaxi.com
18
+ # Please choose based on your network environment and get API key from corresponding platform
19
+ api_key: "YOUR_API_KEY_HERE" # Replace with your MiniMax API Key
20
+ api_base: "https://api.minimax.io" # Global users (default)
21
+ # api_base: "https://api.minimaxi.com" # China users
22
+ model: "MiniMax-M2.1"
23
+ # LLM provider: "anthropic" or "openai"
24
+ # For MiniMax API, the suffix (/anthropic or /v1) is auto-appended based on provider.
25
+ # For third-party APIs (e.g., https://api.siliconflow.cn/v1), api_base is used as-is.
26
+ provider: "anthropic" # Default: anthropic
27
+
28
+ # ===== Retry Configuration =====
29
+ retry:
30
+ enabled: true # Enable retry mechanism
31
+ max_retries: 3 # Maximum number of retries
32
+ initial_delay: 1.0 # Initial delay time (seconds)
33
+ max_delay: 60.0 # Maximum delay time (seconds)
34
+ exponential_base: 2.0 # Exponential backoff base (delay = initial_delay * base^attempt)
35
+
36
+ # ===== Agent Configuration =====
37
+ max_steps: 100 # Maximum execution steps
38
+ workspace_dir: "./workspace" # Working directory
39
+ system_prompt_path: "system_prompt.md" # System prompt file (same config directory)
40
+
41
+ # ===== Tools Configuration =====
42
+ tools:
43
+ # Basic tool switches
44
+ enable_file_tools: true # File read/write/edit tools (ReadTool, WriteTool, EditTool)
45
+ enable_bash: true # Bash command execution tool
46
+ enable_note: true # Session note tool (SessionNoteTool)
47
+
48
+ # Claude Skills
49
+ enable_skills: true # Enable Skills
50
+ skills_dir: "./skills" # Skills directory path
51
+
52
+ # MCP Tools
53
+ enable_mcp: true # Enable MCP tools
54
+ mcp_config_path: "mcp.json" # MCP configuration file (same config directory)
55
+ # Note: API Keys for MCP tools are configured in mcp.json
56
+ # MCP timeout configuration (prevents hanging on network issues)
57
+ mcp:
58
+ connect_timeout: 10.0 # Connection timeout in seconds (default: 10)
59
+ execute_timeout: 60.0 # Tool execution timeout in seconds (default: 60)
60
+ sse_read_timeout: 120.0 # SSE read timeout in seconds (default: 120)
mini_agent/config/mcp-example.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mcpServers": {
3
+ "minimax_search": {
4
+ "description": "MiniMax Search - Powerful web search and intelligent browsing ⭐",
5
+ "type": "stdio",
6
+ "command": "uvx",
7
+ "args": [
8
+ "--from",
9
+ "git+https://github.com/MiniMax-AI/minimax_search",
10
+ "minimax-search"
11
+ ],
12
+ "env": {
13
+ "JINA_API_KEY": "",
14
+ "SERPER_API_KEY": "",
15
+ "MINIMAX_API_KEY": ""
16
+ },
17
+ "disabled": true
18
+ },
19
+ "memory": {
20
+ "description": "Memory - Knowledge graph memory system (long-term memory based on graph database)",
21
+ "command": "npx",
22
+ "args": [
23
+ "-y",
24
+ "@modelcontextprotocol/server-memory"
25
+ ],
26
+ "disabled": true
27
+ }
28
+ }
29
+ }
mini_agent/config/system_prompt.md ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are Mini-Agent, a versatile AI assistant powered by MiniMax, capable of executing complex tasks through a rich toolset and specialized skills.
2
+
3
+ ## Core Capabilities
4
+
5
+ ### 1. **Basic Tools**
6
+ - **File Operations**: Read, write, edit files with full path support
7
+ - **Bash Execution**: Run commands, manage git, packages, and system operations
8
+ - **MCP Tools**: Access additional tools from configured MCP servers
9
+
10
+ ### 2. **Specialized Skills**
11
+ You have access to specialized skills that provide expert guidance and capabilities for specific tasks.
12
+
13
+ Skills are loaded dynamically using **Progressive Disclosure**:
14
+ - **Level 1 (Metadata)**: You see skill names and descriptions (below) at startup
15
+ - **Level 2 (Full Content)**: Load a skill's complete guidance using `get_skill(skill_name)`
16
+ - **Level 3+ (Resources)**: Skills may reference additional files and scripts as needed
17
+
18
+ **How to Use Skills:**
19
+ 1. Check the metadata below to identify relevant skills for your task
20
+ 2. Call `get_skill(skill_name)` to load the full guidance
21
+ 3. Follow the skill's instructions and use appropriate tools (bash, file operations, etc.)
22
+
23
+ **Important Notes:**
24
+ - Skills provide expert patterns and procedural knowledge
25
+ - **For Python skills** (pdf, pptx, docx, xlsx, canvas-design, algorithmic-art): Setup Python environment FIRST (see Python Environment Management below)
26
+ - Skills may reference scripts and resources - use bash or read_file to access them
27
+
28
+ ---
29
+
30
+ {SKILLS_METADATA}
31
+
32
+ ## Working Guidelines
33
+
34
+ ### Task Execution
35
+ 1. **Analyze** the request and identify if a skill can help
36
+ 2. **Break down** complex tasks into clear, executable steps
37
+ 3. **Use skills** when appropriate for specialized guidance
38
+ 4. **Execute** tools systematically and check results
39
+ 5. **Report** progress and any issues encountered
40
+
41
+ ### File Operations
42
+ - Use absolute paths or workspace-relative paths
43
+ - Verify file existence before reading/editing
44
+ - Create parent directories before writing files
45
+ - Handle errors gracefully with clear messages
46
+
47
+ ### Bash Commands
48
+ - Explain destructive operations before execution
49
+ - Check command outputs for errors
50
+ - Use appropriate error handling
51
+ - Prefer specialized tools over raw commands when available
52
+
53
+ ### Python Environment Management
54
+ **CRITICAL - Use `uv` for all Python operations. Before executing Python code:**
55
+ 1. Check/create venv: `if [ ! -d .venv ]; then uv venv; fi`
56
+ 2. Install packages: `uv pip install <package>`
57
+ 3. Run scripts: `uv run python script.py`
58
+ 4. If uv missing: `curl -LsSf https://astral.sh/uv/install.sh | sh`
59
+
60
+ **Python-based skills:** pdf, pptx, docx, xlsx, canvas-design, algorithmic-art
61
+
62
+ ### Communication
63
+ - Be concise but thorough in responses
64
+ - Explain your approach before tool execution
65
+ - Report errors with context and solutions
66
+ - Summarize accomplishments when complete
67
+
68
+ ### Best Practices
69
+ - **Don't guess** - use tools to discover missing information
70
+ - **Be proactive** - infer intent and take reasonable actions
71
+ - **Stay focused** - stop when the task is fulfilled
72
+ - **Use skills** - leverage specialized knowledge when relevant
73
+
74
+ ## Workspace Context
75
+ You are working in a workspace directory. All operations are relative to this context unless absolute paths are specified.
mini_agent/llm/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """LLM clients package supporting both Anthropic and OpenAI protocols."""
2
+
3
+ from .anthropic_client import AnthropicClient
4
+ from .base import LLMClientBase
5
+ from .llm_wrapper import LLMClient
6
+ from .openai_client import OpenAIClient
7
+
8
+ __all__ = ["LLMClientBase", "AnthropicClient", "OpenAIClient", "LLMClient"]
9
+
mini_agent/llm/anthropic_client.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Anthropic LLM client implementation."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ import anthropic
7
+
8
+ from ..retry import RetryConfig, async_retry
9
+ from ..schema import FunctionCall, LLMResponse, Message, TokenUsage, ToolCall
10
+ from .base import LLMClientBase
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class AnthropicClient(LLMClientBase):
16
+ """LLM client using Anthropic's protocol.
17
+
18
+ This client uses the official Anthropic SDK and supports:
19
+ - Extended thinking content
20
+ - Tool calling
21
+ - Retry logic
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ api_key: str,
27
+ api_base: str = "https://api.minimaxi.com/anthropic",
28
+ model: str = "MiniMax-M2.1",
29
+ retry_config: RetryConfig | None = None,
30
+ ):
31
+ """Initialize Anthropic client.
32
+
33
+ Args:
34
+ api_key: API key for authentication
35
+ api_base: Base URL for the API (default: MiniMax Anthropic endpoint)
36
+ model: Model name to use (default: MiniMax-M2.1)
37
+ retry_config: Optional retry configuration
38
+ """
39
+ super().__init__(api_key, api_base, model, retry_config)
40
+
41
+ # Initialize Anthropic async client
42
+ self.client = anthropic.AsyncAnthropic(
43
+ base_url=api_base,
44
+ api_key=api_key,
45
+ default_headers={"Authorization": f"Bearer {api_key}"},
46
+ )
47
+
48
+ async def _make_api_request(
49
+ self,
50
+ system_message: str | None,
51
+ api_messages: list[dict[str, Any]],
52
+ tools: list[Any] | None = None,
53
+ ) -> anthropic.types.Message:
54
+ """Execute API request (core method that can be retried).
55
+
56
+ Args:
57
+ system_message: Optional system message
58
+ api_messages: List of messages in Anthropic format
59
+ tools: Optional list of tools
60
+
61
+ Returns:
62
+ Anthropic Message response
63
+
64
+ Raises:
65
+ Exception: API call failed
66
+ """
67
+ params = {
68
+ "model": self.model,
69
+ "max_tokens": 16384,
70
+ "messages": api_messages,
71
+ }
72
+
73
+ if system_message:
74
+ params["system"] = system_message
75
+
76
+ if tools:
77
+ params["tools"] = self._convert_tools(tools)
78
+
79
+ # Use Anthropic SDK's async messages.create
80
+ response = await self.client.messages.create(**params)
81
+ return response
82
+
83
+ def _convert_tools(self, tools: list[Any]) -> list[dict[str, Any]]:
84
+ """Convert tools to Anthropic format.
85
+
86
+ Anthropic tool format:
87
+ {
88
+ "name": "tool_name",
89
+ "description": "Tool description",
90
+ "input_schema": {
91
+ "type": "object",
92
+ "properties": {...},
93
+ "required": [...]
94
+ }
95
+ }
96
+
97
+ Args:
98
+ tools: List of Tool objects or dicts
99
+
100
+ Returns:
101
+ List of tools in Anthropic dict format
102
+ """
103
+ result = []
104
+ for tool in tools:
105
+ if isinstance(tool, dict):
106
+ result.append(tool)
107
+ elif hasattr(tool, "to_schema"):
108
+ # Tool object with to_schema method
109
+ result.append(tool.to_schema())
110
+ else:
111
+ raise TypeError(f"Unsupported tool type: {type(tool)}")
112
+ return result
113
+
114
+ def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]:
115
+ """Convert internal messages to Anthropic format.
116
+
117
+ Args:
118
+ messages: List of internal Message objects
119
+
120
+ Returns:
121
+ Tuple of (system_message, api_messages)
122
+ """
123
+ system_message = None
124
+ api_messages = []
125
+
126
+ for msg in messages:
127
+ if msg.role == "system":
128
+ system_message = msg.content
129
+ continue
130
+
131
+ # For user and assistant messages
132
+ if msg.role in ["user", "assistant"]:
133
+ # Handle assistant messages with thinking or tool calls
134
+ if msg.role == "assistant" and (msg.thinking or msg.tool_calls):
135
+ # Build content blocks for assistant with thinking and/or tool calls
136
+ content_blocks = []
137
+
138
+ # Add thinking block if present
139
+ if msg.thinking:
140
+ content_blocks.append({"type": "thinking", "thinking": msg.thinking})
141
+
142
+ # Add text content if present
143
+ if msg.content:
144
+ content_blocks.append({"type": "text", "text": msg.content})
145
+
146
+ # Add tool use blocks
147
+ if msg.tool_calls:
148
+ for tool_call in msg.tool_calls:
149
+ content_blocks.append(
150
+ {
151
+ "type": "tool_use",
152
+ "id": tool_call.id,
153
+ "name": tool_call.function.name,
154
+ "input": tool_call.function.arguments,
155
+ }
156
+ )
157
+
158
+ api_messages.append({"role": "assistant", "content": content_blocks})
159
+ else:
160
+ api_messages.append({"role": msg.role, "content": msg.content})
161
+
162
+ # For tool result messages
163
+ elif msg.role == "tool":
164
+ # Anthropic uses user role with tool_result content blocks
165
+ api_messages.append(
166
+ {
167
+ "role": "user",
168
+ "content": [
169
+ {
170
+ "type": "tool_result",
171
+ "tool_use_id": msg.tool_call_id,
172
+ "content": msg.content,
173
+ }
174
+ ],
175
+ }
176
+ )
177
+
178
+ return system_message, api_messages
179
+
180
+ def _prepare_request(
181
+ self,
182
+ messages: list[Message],
183
+ tools: list[Any] | None = None,
184
+ ) -> dict[str, Any]:
185
+ """Prepare the request for Anthropic API.
186
+
187
+ Args:
188
+ messages: List of conversation messages
189
+ tools: Optional list of available tools
190
+
191
+ Returns:
192
+ Dictionary containing request parameters
193
+ """
194
+ system_message, api_messages = self._convert_messages(messages)
195
+
196
+ return {
197
+ "system_message": system_message,
198
+ "api_messages": api_messages,
199
+ "tools": tools,
200
+ }
201
+
202
+ def _parse_response(self, response: anthropic.types.Message) -> LLMResponse:
203
+ """Parse Anthropic response into LLMResponse.
204
+
205
+ Args:
206
+ response: Anthropic Message response
207
+
208
+ Returns:
209
+ LLMResponse object
210
+ """
211
+ # Extract text content, thinking, and tool calls
212
+ text_content = ""
213
+ thinking_content = ""
214
+ tool_calls = []
215
+
216
+ for block in response.content:
217
+ if block.type == "text":
218
+ text_content += block.text
219
+ elif block.type == "thinking":
220
+ thinking_content += block.thinking
221
+ elif block.type == "tool_use":
222
+ # Parse Anthropic tool_use block
223
+ tool_calls.append(
224
+ ToolCall(
225
+ id=block.id,
226
+ type="function",
227
+ function=FunctionCall(
228
+ name=block.name,
229
+ arguments=block.input,
230
+ ),
231
+ )
232
+ )
233
+
234
+ # Extract token usage from response
235
+ # Anthropic usage includes: input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens
236
+ usage = None
237
+ if hasattr(response, "usage") and response.usage:
238
+ input_tokens = response.usage.input_tokens or 0
239
+ output_tokens = response.usage.output_tokens or 0
240
+ cache_read_tokens = getattr(response.usage, "cache_read_input_tokens", 0) or 0
241
+ cache_creation_tokens = getattr(response.usage, "cache_creation_input_tokens", 0) or 0
242
+ total_input_tokens = input_tokens + cache_read_tokens + cache_creation_tokens
243
+ usage = TokenUsage(
244
+ prompt_tokens=total_input_tokens,
245
+ completion_tokens=output_tokens,
246
+ total_tokens=total_input_tokens + output_tokens,
247
+ )
248
+
249
+ return LLMResponse(
250
+ content=text_content,
251
+ thinking=thinking_content if thinking_content else None,
252
+ tool_calls=tool_calls if tool_calls else None,
253
+ finish_reason=response.stop_reason or "stop",
254
+ usage=usage,
255
+ )
256
+
257
+ async def generate(
258
+ self,
259
+ messages: list[Message],
260
+ tools: list[Any] | None = None,
261
+ ) -> LLMResponse:
262
+ """Generate response from Anthropic LLM.
263
+
264
+ Args:
265
+ messages: List of conversation messages
266
+ tools: Optional list of available tools
267
+
268
+ Returns:
269
+ LLMResponse containing the generated content
270
+ """
271
+ # Prepare request
272
+ request_params = self._prepare_request(messages, tools)
273
+
274
+ # Make API request with retry logic
275
+ if self.retry_config.enabled:
276
+ # Apply retry logic
277
+ retry_decorator = async_retry(config=self.retry_config, on_retry=self.retry_callback)
278
+ api_call = retry_decorator(self._make_api_request)
279
+ response = await api_call(
280
+ request_params["system_message"],
281
+ request_params["api_messages"],
282
+ request_params["tools"],
283
+ )
284
+ else:
285
+ # Don't use retry
286
+ response = await self._make_api_request(
287
+ request_params["system_message"],
288
+ request_params["api_messages"],
289
+ request_params["tools"],
290
+ )
291
+
292
+ # Parse and return response
293
+ return self._parse_response(response)
mini_agent/llm/base.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Base class for LLM clients."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+ from ..retry import RetryConfig
7
+ from ..schema import LLMResponse, Message
8
+
9
+
10
+ class LLMClientBase(ABC):
11
+ """Abstract base class for LLM clients.
12
+
13
+ This class defines the interface that all LLM clients must implement,
14
+ regardless of the underlying API protocol (Anthropic, OpenAI, etc.).
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ api_key: str,
20
+ api_base: str,
21
+ model: str,
22
+ retry_config: RetryConfig | None = None,
23
+ ):
24
+ """Initialize the LLM client.
25
+
26
+ Args:
27
+ api_key: API key for authentication
28
+ api_base: Base URL for the API
29
+ model: Model name to use
30
+ retry_config: Optional retry configuration
31
+ """
32
+ self.api_key = api_key
33
+ self.api_base = api_base
34
+ self.model = model
35
+ self.retry_config = retry_config or RetryConfig()
36
+
37
+ # Callback for tracking retry count
38
+ self.retry_callback = None
39
+
40
+ @abstractmethod
41
+ async def generate(
42
+ self,
43
+ messages: list[Message],
44
+ tools: list[Any] | None = None,
45
+ ) -> LLMResponse:
46
+ """Generate response from LLM.
47
+
48
+ Args:
49
+ messages: List of conversation messages
50
+ tools: Optional list of Tool objects or dicts
51
+
52
+ Returns:
53
+ LLMResponse containing the generated content, thinking, and tool calls
54
+ """
55
+ pass
56
+
57
+ @abstractmethod
58
+ def _prepare_request(
59
+ self,
60
+ messages: list[Message],
61
+ tools: list[Any] | None = None,
62
+ ) -> dict[str, Any]:
63
+ """Prepare the request payload for the API.
64
+
65
+ Args:
66
+ messages: List of conversation messages
67
+ tools: Optional list of available tools
68
+
69
+ Returns:
70
+ Dictionary containing the request payload
71
+ """
72
+ pass
73
+
74
+ @abstractmethod
75
+ def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]:
76
+ """Convert internal message format to API-specific format.
77
+
78
+ Args:
79
+ messages: List of internal Message objects
80
+
81
+ Returns:
82
+ Tuple of (system_message, api_messages)
83
+ """
84
+ pass
mini_agent/llm/llm_wrapper.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LLM client wrapper that supports multiple providers.
2
+
3
+ This module provides a unified interface for different LLM providers
4
+ (Anthropic and OpenAI) through a single LLMClient class.
5
+ """
6
+
7
+ import logging
8
+
9
+ from ..retry import RetryConfig
10
+ from ..schema import LLMProvider, LLMResponse, Message
11
+ from .anthropic_client import AnthropicClient
12
+ from .base import LLMClientBase
13
+ from .openai_client import OpenAIClient
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class LLMClient:
19
+ """LLM Client wrapper supporting multiple providers.
20
+
21
+ This class provides a unified interface for different LLM providers.
22
+ It automatically instantiates the correct underlying client based on
23
+ the provider parameter.
24
+
25
+ For MiniMax API (api.minimax.io or api.minimaxi.com), it appends the
26
+ appropriate endpoint suffix based on provider:
27
+ - anthropic: /anthropic
28
+ - openai: /v1
29
+
30
+ For third-party APIs, it uses the api_base as-is.
31
+ """
32
+
33
+ # MiniMax API domains that need automatic suffix handling
34
+ MINIMAX_DOMAINS = ("api.minimax.io", "api.minimaxi.com")
35
+
36
+ def __init__(
37
+ self,
38
+ api_key: str,
39
+ provider: LLMProvider = LLMProvider.ANTHROPIC,
40
+ api_base: str = "https://api.minimaxi.com",
41
+ model: str = "MiniMax-M2.1",
42
+ retry_config: RetryConfig | None = None,
43
+ ):
44
+ """Initialize LLM client with specified provider.
45
+
46
+ Args:
47
+ api_key: API key for authentication
48
+ provider: LLM provider (anthropic or openai)
49
+ api_base: Base URL for the API (default: https://api.minimaxi.com)
50
+ For MiniMax API, suffix is auto-appended based on provider.
51
+ For third-party APIs (e.g., https://api.siliconflow.cn/v1), used as-is.
52
+ model: Model name to use
53
+ retry_config: Optional retry configuration
54
+ """
55
+ self.provider = provider
56
+ self.api_key = api_key
57
+ self.model = model
58
+ self.retry_config = retry_config or RetryConfig()
59
+
60
+ # Normalize api_base (remove trailing slash)
61
+ api_base = api_base.rstrip("/")
62
+
63
+ # Check if this is a MiniMax API endpoint
64
+ is_minimax = any(domain in api_base for domain in self.MINIMAX_DOMAINS)
65
+
66
+ if is_minimax:
67
+ # For MiniMax API, ensure correct suffix based on provider
68
+ # Strip any existing suffix first
69
+ api_base = api_base.replace("/anthropic", "").replace("/v1", "")
70
+ if provider == LLMProvider.ANTHROPIC:
71
+ full_api_base = f"{api_base}/anthropic"
72
+ elif provider == LLMProvider.OPENAI:
73
+ full_api_base = f"{api_base}/v1"
74
+ else:
75
+ raise ValueError(f"Unsupported provider: {provider}")
76
+ else:
77
+ # For third-party APIs, use api_base as-is
78
+ full_api_base = api_base
79
+
80
+ self.api_base = full_api_base
81
+
82
+ # Instantiate the appropriate client
83
+ self._client: LLMClientBase
84
+ if provider == LLMProvider.ANTHROPIC:
85
+ self._client = AnthropicClient(
86
+ api_key=api_key,
87
+ api_base=full_api_base,
88
+ model=model,
89
+ retry_config=retry_config,
90
+ )
91
+ elif provider == LLMProvider.OPENAI:
92
+ self._client = OpenAIClient(
93
+ api_key=api_key,
94
+ api_base=full_api_base,
95
+ model=model,
96
+ retry_config=retry_config,
97
+ )
98
+ else:
99
+ raise ValueError(f"Unsupported provider: {provider}")
100
+
101
+ logger.info("Initialized LLM client with provider: %s, api_base: %s", provider, full_api_base)
102
+
103
+ @property
104
+ def retry_callback(self):
105
+ """Get retry callback."""
106
+ return self._client.retry_callback
107
+
108
+ @retry_callback.setter
109
+ def retry_callback(self, value):
110
+ """Set retry callback."""
111
+ self._client.retry_callback = value
112
+
113
+ async def generate(
114
+ self,
115
+ messages: list[Message],
116
+ tools: list | None = None,
117
+ ) -> LLMResponse:
118
+ """Generate response from LLM.
119
+
120
+ Args:
121
+ messages: List of conversation messages
122
+ tools: Optional list of Tool objects or dicts
123
+
124
+ Returns:
125
+ LLMResponse containing the generated content
126
+ """
127
+ return await self._client.generate(messages, tools)
mini_agent/llm/openai_client.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OpenAI LLM client implementation."""
2
+
3
+ import json
4
+ import logging
5
+ from typing import Any
6
+
7
+ from openai import AsyncOpenAI
8
+
9
+ from ..retry import RetryConfig, async_retry
10
+ from ..schema import FunctionCall, LLMResponse, Message, TokenUsage, ToolCall
11
+ from .base import LLMClientBase
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class OpenAIClient(LLMClientBase):
17
+ """LLM client using OpenAI's protocol.
18
+
19
+ This client uses the official OpenAI SDK and supports:
20
+ - Reasoning content (via reasoning_split=True)
21
+ - Tool calling
22
+ - Retry logic
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ api_key: str,
28
+ api_base: str = "https://api.minimaxi.com/v1",
29
+ model: str = "MiniMax-M2.1",
30
+ retry_config: RetryConfig | None = None,
31
+ ):
32
+ """Initialize OpenAI client.
33
+
34
+ Args:
35
+ api_key: API key for authentication
36
+ api_base: Base URL for the API (default: MiniMax OpenAI endpoint)
37
+ model: Model name to use (default: MiniMax-M2.1)
38
+ retry_config: Optional retry configuration
39
+ """
40
+ super().__init__(api_key, api_base, model, retry_config)
41
+
42
+ # Initialize OpenAI client
43
+ self.client = AsyncOpenAI(
44
+ api_key=api_key,
45
+ base_url=api_base,
46
+ )
47
+
48
+ async def _make_api_request(
49
+ self,
50
+ api_messages: list[dict[str, Any]],
51
+ tools: list[Any] | None = None,
52
+ ) -> Any:
53
+ """Execute API request (core method that can be retried).
54
+
55
+ Args:
56
+ api_messages: List of messages in OpenAI format
57
+ tools: Optional list of tools
58
+
59
+ Returns:
60
+ OpenAI ChatCompletion response (full response including usage)
61
+
62
+ Raises:
63
+ Exception: API call failed
64
+ """
65
+ params = {
66
+ "model": self.model,
67
+ "messages": api_messages,
68
+ # Enable reasoning_split to separate thinking content
69
+ "extra_body": {"reasoning_split": True},
70
+ }
71
+
72
+ if tools:
73
+ params["tools"] = self._convert_tools(tools)
74
+
75
+ # Use OpenAI SDK's chat.completions.create
76
+ response = await self.client.chat.completions.create(**params)
77
+ # Return full response to access usage info
78
+ return response
79
+
80
+ def _convert_tools(self, tools: list[Any]) -> list[dict[str, Any]]:
81
+ """Convert tools to OpenAI format.
82
+
83
+ Args:
84
+ tools: List of Tool objects or dicts
85
+
86
+ Returns:
87
+ List of tools in OpenAI dict format
88
+ """
89
+ result = []
90
+ for tool in tools:
91
+ if isinstance(tool, dict):
92
+ # If already a dict, check if it's in OpenAI format
93
+ if "type" in tool and tool["type"] == "function":
94
+ result.append(tool)
95
+ else:
96
+ # Assume it's in Anthropic format, convert to OpenAI
97
+ result.append(
98
+ {
99
+ "type": "function",
100
+ "function": {
101
+ "name": tool["name"],
102
+ "description": tool["description"],
103
+ "parameters": tool["input_schema"],
104
+ },
105
+ }
106
+ )
107
+ elif hasattr(tool, "to_openai_schema"):
108
+ # Tool object with to_openai_schema method
109
+ result.append(tool.to_openai_schema())
110
+ else:
111
+ raise TypeError(f"Unsupported tool type: {type(tool)}")
112
+ return result
113
+
114
+ def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]:
115
+ """Convert internal messages to OpenAI format.
116
+
117
+ Args:
118
+ messages: List of internal Message objects
119
+
120
+ Returns:
121
+ Tuple of (system_message, api_messages)
122
+ Note: OpenAI includes system message in the messages array
123
+ """
124
+ api_messages = []
125
+
126
+ for msg in messages:
127
+ if msg.role == "system":
128
+ # OpenAI includes system message in messages array
129
+ api_messages.append({"role": "system", "content": msg.content})
130
+ continue
131
+
132
+ # For user messages
133
+ if msg.role == "user":
134
+ api_messages.append({"role": "user", "content": msg.content})
135
+
136
+ # For assistant messages
137
+ elif msg.role == "assistant":
138
+ assistant_msg = {"role": "assistant"}
139
+
140
+ # Add content if present
141
+ if msg.content:
142
+ assistant_msg["content"] = msg.content
143
+
144
+ # Add tool calls if present
145
+ if msg.tool_calls:
146
+ tool_calls_list = []
147
+ for tool_call in msg.tool_calls:
148
+ tool_calls_list.append(
149
+ {
150
+ "id": tool_call.id,
151
+ "type": "function",
152
+ "function": {
153
+ "name": tool_call.function.name,
154
+ "arguments": json.dumps(tool_call.function.arguments),
155
+ },
156
+ }
157
+ )
158
+ assistant_msg["tool_calls"] = tool_calls_list
159
+
160
+ # IMPORTANT: Add reasoning_details if thinking is present
161
+ # This is CRITICAL for Interleaved Thinking to work properly!
162
+ # The complete response_message (including reasoning_details) must be
163
+ # preserved in Message History and passed back to the model in the next turn.
164
+ # This ensures the model's chain of thought is not interrupted.
165
+ if msg.thinking:
166
+ assistant_msg["reasoning_details"] = [{"text": msg.thinking}]
167
+
168
+ api_messages.append(assistant_msg)
169
+
170
+ # For tool result messages
171
+ elif msg.role == "tool":
172
+ api_messages.append(
173
+ {
174
+ "role": "tool",
175
+ "tool_call_id": msg.tool_call_id,
176
+ "content": msg.content,
177
+ }
178
+ )
179
+
180
+ return None, api_messages
181
+
182
+ def _prepare_request(
183
+ self,
184
+ messages: list[Message],
185
+ tools: list[Any] | None = None,
186
+ ) -> dict[str, Any]:
187
+ """Prepare the request for OpenAI API.
188
+
189
+ Args:
190
+ messages: List of conversation messages
191
+ tools: Optional list of available tools
192
+
193
+ Returns:
194
+ Dictionary containing request parameters
195
+ """
196
+ _, api_messages = self._convert_messages(messages)
197
+
198
+ return {
199
+ "api_messages": api_messages,
200
+ "tools": tools,
201
+ }
202
+
203
+ def _parse_response(self, response: Any) -> LLMResponse:
204
+ """Parse OpenAI response into LLMResponse.
205
+
206
+ Args:
207
+ response: OpenAI ChatCompletion response (full response object)
208
+
209
+ Returns:
210
+ LLMResponse object
211
+ """
212
+ # Get message from response
213
+ message = response.choices[0].message
214
+
215
+ # Extract text content
216
+ text_content = message.content or ""
217
+
218
+ # Extract thinking content from reasoning_details
219
+ thinking_content = ""
220
+ if hasattr(message, "reasoning_details") and message.reasoning_details:
221
+ # reasoning_details is a list of reasoning blocks
222
+ for detail in message.reasoning_details:
223
+ if hasattr(detail, "text"):
224
+ thinking_content += detail.text
225
+
226
+ # Extract tool calls
227
+ tool_calls = []
228
+ if message.tool_calls:
229
+ for tool_call in message.tool_calls:
230
+ # Parse arguments from JSON string
231
+ arguments = json.loads(tool_call.function.arguments)
232
+
233
+ tool_calls.append(
234
+ ToolCall(
235
+ id=tool_call.id,
236
+ type="function",
237
+ function=FunctionCall(
238
+ name=tool_call.function.name,
239
+ arguments=arguments,
240
+ ),
241
+ )
242
+ )
243
+
244
+ # Extract token usage from response
245
+ usage = None
246
+ if hasattr(response, "usage") and response.usage:
247
+ usage = TokenUsage(
248
+ prompt_tokens=response.usage.prompt_tokens or 0,
249
+ completion_tokens=response.usage.completion_tokens or 0,
250
+ total_tokens=response.usage.total_tokens or 0,
251
+ )
252
+
253
+ return LLMResponse(
254
+ content=text_content,
255
+ thinking=thinking_content if thinking_content else None,
256
+ tool_calls=tool_calls if tool_calls else None,
257
+ finish_reason="stop", # OpenAI doesn't provide finish_reason in the message
258
+ usage=usage,
259
+ )
260
+
261
+ async def generate(
262
+ self,
263
+ messages: list[Message],
264
+ tools: list[Any] | None = None,
265
+ ) -> LLMResponse:
266
+ """Generate response from OpenAI LLM.
267
+
268
+ Args:
269
+ messages: List of conversation messages
270
+ tools: Optional list of available tools
271
+
272
+ Returns:
273
+ LLMResponse containing the generated content
274
+ """
275
+ # Prepare request
276
+ request_params = self._prepare_request(messages, tools)
277
+
278
+ # Make API request with retry logic
279
+ if self.retry_config.enabled:
280
+ # Apply retry logic
281
+ retry_decorator = async_retry(config=self.retry_config, on_retry=self.retry_callback)
282
+ api_call = retry_decorator(self._make_api_request)
283
+ response = await api_call(
284
+ request_params["api_messages"],
285
+ request_params["tools"],
286
+ )
287
+ else:
288
+ # Don't use retry
289
+ response = await self._make_api_request(
290
+ request_params["api_messages"],
291
+ request_params["tools"],
292
+ )
293
+
294
+ # Parse and return response
295
+ return self._parse_response(response)
mini_agent/logger.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Agent run logger"""
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from .schema import Message, ToolCall
9
+
10
+
11
+ class AgentLogger:
12
+ """Agent run logger
13
+
14
+ Responsible for recording the complete interaction process of each agent run, including:
15
+ - LLM requests and responses
16
+ - Tool calls and results
17
+ """
18
+
19
+ def __init__(self):
20
+ """Initialize logger
21
+
22
+ Logs are stored in ~/.mini-agent/log/ directory
23
+ """
24
+ # Use ~/.mini-agent/log/ directory for logs
25
+ self.log_dir = Path.home() / ".mini-agent" / "log"
26
+ self.log_dir.mkdir(parents=True, exist_ok=True)
27
+ self.log_file = None
28
+ self.log_index = 0
29
+
30
+ def start_new_run(self):
31
+ """Start new run, create new log file"""
32
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
33
+ log_filename = f"agent_run_{timestamp}.log"
34
+ self.log_file = self.log_dir / log_filename
35
+ self.log_index = 0
36
+
37
+ # Write log header
38
+ with open(self.log_file, "w", encoding="utf-8") as f:
39
+ f.write("=" * 80 + "\n")
40
+ f.write(f"Agent Run Log - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
41
+ f.write("=" * 80 + "\n\n")
42
+
43
+ def log_request(self, messages: list[Message], tools: list[Any] | None = None):
44
+ """Log LLM request
45
+
46
+ Args:
47
+ messages: Message list
48
+ tools: Tool list (optional)
49
+ """
50
+ self.log_index += 1
51
+
52
+ # Build complete request data structure
53
+ request_data = {
54
+ "messages": [],
55
+ "tools": [],
56
+ }
57
+
58
+ # Convert messages to JSON serializable format
59
+ for msg in messages:
60
+ msg_dict = {
61
+ "role": msg.role,
62
+ "content": msg.content,
63
+ }
64
+ if msg.thinking:
65
+ msg_dict["thinking"] = msg.thinking
66
+ if msg.tool_calls:
67
+ msg_dict["tool_calls"] = [tc.model_dump() for tc in msg.tool_calls]
68
+ if msg.tool_call_id:
69
+ msg_dict["tool_call_id"] = msg.tool_call_id
70
+ if msg.name:
71
+ msg_dict["name"] = msg.name
72
+
73
+ request_data["messages"].append(msg_dict)
74
+
75
+ # Only record tool names
76
+ if tools:
77
+ request_data["tools"] = [tool.name for tool in tools]
78
+
79
+ # Format as JSON
80
+ content = "LLM Request:\n\n"
81
+ content += json.dumps(request_data, indent=2, ensure_ascii=False)
82
+
83
+ self._write_log("REQUEST", content)
84
+
85
+ def log_response(
86
+ self,
87
+ content: str,
88
+ thinking: str | None = None,
89
+ tool_calls: list[ToolCall] | None = None,
90
+ finish_reason: str | None = None,
91
+ ):
92
+ """Log LLM response
93
+
94
+ Args:
95
+ content: Response content
96
+ thinking: Thinking content (optional)
97
+ tool_calls: Tool call list (optional)
98
+ finish_reason: Finish reason (optional)
99
+ """
100
+ self.log_index += 1
101
+
102
+ # Build complete response data structure
103
+ response_data = {
104
+ "content": content,
105
+ }
106
+
107
+ if thinking:
108
+ response_data["thinking"] = thinking
109
+
110
+ if tool_calls:
111
+ response_data["tool_calls"] = [tc.model_dump() for tc in tool_calls]
112
+
113
+ if finish_reason:
114
+ response_data["finish_reason"] = finish_reason
115
+
116
+ # Format as JSON
117
+ log_content = "LLM Response:\n\n"
118
+ log_content += json.dumps(response_data, indent=2, ensure_ascii=False)
119
+
120
+ self._write_log("RESPONSE", log_content)
121
+
122
+ def log_tool_result(
123
+ self,
124
+ tool_name: str,
125
+ arguments: dict[str, Any],
126
+ result_success: bool,
127
+ result_content: str | None = None,
128
+ result_error: str | None = None,
129
+ ):
130
+ """Log tool execution result
131
+
132
+ Args:
133
+ tool_name: Tool name
134
+ arguments: Tool arguments
135
+ result_success: Whether successful
136
+ result_content: Result content (on success)
137
+ result_error: Error message (on failure)
138
+ """
139
+ self.log_index += 1
140
+
141
+ # Build complete tool execution result data structure
142
+ tool_result_data = {
143
+ "tool_name": tool_name,
144
+ "arguments": arguments,
145
+ "success": result_success,
146
+ }
147
+
148
+ if result_success:
149
+ tool_result_data["result"] = result_content
150
+ else:
151
+ tool_result_data["error"] = result_error
152
+
153
+ # Format as JSON
154
+ content = "Tool Execution:\n\n"
155
+ content += json.dumps(tool_result_data, indent=2, ensure_ascii=False)
156
+
157
+ self._write_log("TOOL_RESULT", content)
158
+
159
+ def _write_log(self, log_type: str, content: str):
160
+ """Write log entry
161
+
162
+ Args:
163
+ log_type: Log type (REQUEST, RESPONSE, TOOL_RESULT)
164
+ content: Log content
165
+ """
166
+ if self.log_file is None:
167
+ return
168
+
169
+ with open(self.log_file, "a", encoding="utf-8") as f:
170
+ f.write("\n" + "-" * 80 + "\n")
171
+ f.write(f"[{self.log_index}] {log_type}\n")
172
+ f.write(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}\n")
173
+ f.write("-" * 80 + "\n")
174
+ f.write(content + "\n")
175
+
176
+ def get_log_file_path(self) -> Path:
177
+ """Get current log file path"""
178
+ return self.log_file
mini_agent/retry.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Elegant retry mechanism module
2
+
3
+ Provides decorators and utility functions to support retry logic for async functions.
4
+
5
+ Features:
6
+ - Supports exponential backoff strategy
7
+ - Configurable retry count and intervals
8
+ - Supports specifying retryable exception types
9
+ - Detailed logging
10
+ - Fully decoupled, non-invasive to business code
11
+ """
12
+
13
+ import asyncio
14
+ import functools
15
+ import logging
16
+ from typing import Any, Callable, Type, TypeVar
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ T = TypeVar("T")
21
+
22
+
23
+ class RetryConfig:
24
+ """Retry configuration class"""
25
+
26
+ def __init__(
27
+ self,
28
+ enabled: bool = True,
29
+ max_retries: int = 3,
30
+ initial_delay: float = 1.0,
31
+ max_delay: float = 60.0,
32
+ exponential_base: float = 2.0,
33
+ retryable_exceptions: tuple[Type[Exception], ...] = (Exception,),
34
+ ):
35
+ """
36
+ Args:
37
+ enabled: Whether to enable retry mechanism
38
+ max_retries: Maximum number of retries
39
+ initial_delay: Initial delay time (seconds)
40
+ max_delay: Maximum delay time (seconds)
41
+ exponential_base: Exponential backoff base
42
+ retryable_exceptions: Tuple of retryable exception types
43
+ """
44
+ self.enabled = enabled
45
+ self.max_retries = max_retries
46
+ self.initial_delay = initial_delay
47
+ self.max_delay = max_delay
48
+ self.exponential_base = exponential_base
49
+ self.retryable_exceptions = retryable_exceptions
50
+
51
+ def calculate_delay(self, attempt: int) -> float:
52
+ """Calculate delay time (exponential backoff)
53
+
54
+ Args:
55
+ attempt: Current attempt number (starting from 0)
56
+
57
+ Returns:
58
+ Delay time (seconds)
59
+ """
60
+ delay = self.initial_delay * (self.exponential_base**attempt)
61
+ return min(delay, self.max_delay)
62
+
63
+
64
+ class RetryExhaustedError(Exception):
65
+ """Retry exhausted exception"""
66
+
67
+ def __init__(self, last_exception: Exception, attempts: int):
68
+ self.last_exception = last_exception
69
+ self.attempts = attempts
70
+ super().__init__(f"Retry failed after {attempts} attempts. Last error: {str(last_exception)}")
71
+
72
+
73
+ def async_retry(
74
+ config: RetryConfig | None = None,
75
+ on_retry: Callable[[Exception, int], None] | None = None,
76
+ ) -> Callable:
77
+ """Async function retry decorator
78
+
79
+ Args:
80
+ config: Retry configuration object, uses default config if None
81
+ on_retry: Callback function on retry, receives exception and current attempt number
82
+
83
+ Returns:
84
+ Decorator function
85
+
86
+ Example:
87
+ ```python
88
+ @async_retry(RetryConfig(max_retries=3, initial_delay=1.0))
89
+ async def call_api():
90
+ # API call code
91
+ pass
92
+ ```
93
+ """
94
+ if config is None:
95
+ config = RetryConfig()
96
+
97
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
98
+ @functools.wraps(func)
99
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
100
+ last_exception: Exception | None = None
101
+
102
+ for attempt in range(config.max_retries + 1):
103
+ try:
104
+ # Try to execute function
105
+ return await func(*args, **kwargs)
106
+
107
+ except config.retryable_exceptions as e:
108
+ last_exception = e
109
+
110
+ # If this is the last attempt, don't retry
111
+ if attempt >= config.max_retries:
112
+ logger.error(f"Function {func.__name__} retry failed, reached maximum retry count {config.max_retries}")
113
+ raise RetryExhaustedError(e, attempt + 1)
114
+
115
+ # Calculate delay time
116
+ delay = config.calculate_delay(attempt)
117
+
118
+ # Log
119
+ logger.warning(
120
+ f"Function {func.__name__} call {attempt + 1} failed: {str(e)}, "
121
+ f"retrying attempt {attempt + 2} after {delay:.2f} seconds"
122
+ )
123
+
124
+ # Call callback function
125
+ if on_retry:
126
+ on_retry(e, attempt + 1)
127
+
128
+ # Wait before retry
129
+ await asyncio.sleep(delay)
130
+
131
+ # Should not reach here in theory
132
+ if last_exception:
133
+ raise last_exception
134
+ raise Exception("Unknown error")
135
+
136
+ return wrapper
137
+
138
+ return decorator
mini_agent/schema/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Schema definitions for Mini-Agent."""
2
+
3
+ from .schema import (
4
+ FunctionCall,
5
+ LLMProvider,
6
+ LLMResponse,
7
+ Message,
8
+ TokenUsage,
9
+ ToolCall,
10
+ )
11
+
12
+ __all__ = [
13
+ "FunctionCall",
14
+ "LLMProvider",
15
+ "LLMResponse",
16
+ "Message",
17
+ "TokenUsage",
18
+ "ToolCall",
19
+ ]
mini_agent/schema/schema.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+ from typing import Any
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class LLMProvider(str, Enum):
8
+ """LLM provider types."""
9
+
10
+ ANTHROPIC = "anthropic"
11
+ OPENAI = "openai"
12
+
13
+
14
+ class FunctionCall(BaseModel):
15
+ """Function call details."""
16
+
17
+ name: str
18
+ arguments: dict[str, Any] # Function arguments as dict
19
+
20
+
21
+ class ToolCall(BaseModel):
22
+ """Tool call structure."""
23
+
24
+ id: str
25
+ type: str # "function"
26
+ function: FunctionCall
27
+
28
+
29
+ class Message(BaseModel):
30
+ """Chat message."""
31
+
32
+ role: str # "system", "user", "assistant", "tool"
33
+ content: str | list[dict[str, Any]] # Can be string or list of content blocks
34
+ thinking: str | None = None # Extended thinking content for assistant messages
35
+ tool_calls: list[ToolCall] | None = None
36
+ tool_call_id: str | None = None
37
+ name: str | None = None # For tool role
38
+
39
+
40
+ class TokenUsage(BaseModel):
41
+ """Token usage statistics from LLM API response."""
42
+
43
+ prompt_tokens: int = 0
44
+ completion_tokens: int = 0
45
+ total_tokens: int = 0
46
+
47
+
48
+ class LLMResponse(BaseModel):
49
+ """LLM response."""
50
+
51
+ content: str
52
+ thinking: str | None = None # Extended thinking blocks
53
+ tool_calls: list[ToolCall] | None = None
54
+ finish_reason: str
55
+ usage: TokenUsage | None = None # Token usage from API response
mini_agent/skills/.claude-plugin/marketplace.json ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "anthropic-agent-skills",
3
+ "owner": {
4
+ "name": "Keith Lazuka",
5
+ "email": "klazuka@anthropic.com"
6
+ },
7
+ "metadata": {
8
+ "description": "Anthropic example skills",
9
+ "version": "1.0.0"
10
+ },
11
+ "plugins": [
12
+ {
13
+ "name": "document-skills",
14
+ "description": "Collection of document processing suite including Excel, Word, PowerPoint, and PDF capabilities",
15
+ "source": "./",
16
+ "strict": false,
17
+ "skills": [
18
+ "./document-skills/xlsx",
19
+ "./document-skills/docx",
20
+ "./document-skills/pptx",
21
+ "./document-skills/pdf"
22
+ ]
23
+ },
24
+ {
25
+ "name": "example-skills",
26
+ "description": "Collection of example skills demonstrating various capabilities including skill creation, MCP building, visual design, algorithmic art, internal communications, web testing, artifact building, Slack GIFs, and theme styling",
27
+ "source": "./",
28
+ "strict": false,
29
+ "skills": [
30
+ "./skill-creator",
31
+ "./mcp-builder",
32
+ "./canvas-design",
33
+ "./algorithmic-art",
34
+ "./internal-comms",
35
+ "./webapp-testing",
36
+ "./artifacts-builder",
37
+ "./slack-gif-creator",
38
+ "./theme-factory",
39
+ "./brand-guidelines"
40
+ ]
41
+ }
42
+ ]
43
+ }
mini_agent/skills/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .DS_Store
2
+
mini_agent/skills/README.md ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Skills
2
+ Skills are folders of instructions, scripts, and resources that Claude loads dynamically to improve performance on specialized tasks. Skills teach Claude how to complete specific tasks in a repeatable way, whether that's creating documents with your company's brand guidelines, analyzing data using your organization's specific workflows, or automating personal tasks.
3
+
4
+ For more information, check out:
5
+ - [What are skills?](https://support.claude.com/en/articles/12512176-what-are-skills)
6
+ - [Using skills in Claude](https://support.claude.com/en/articles/12512180-using-skills-in-claude)
7
+ - [How to create custom skills](https://support.claude.com/en/articles/12512198-creating-custom-skills)
8
+ - [Equipping agents for the real world with Agent Skills](https://anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)
9
+
10
+ # About This Repository
11
+
12
+ This repository contains example skills that demonstrate what's possible with Claude's skills system. These examples range from creative applications (art, music, design) to technical tasks (testing web apps, MCP server generation) to enterprise workflows (communications, branding, etc.).
13
+
14
+ Each skill is self-contained in its own directory with a `SKILL.md` file containing the instructions and metadata that Claude uses. Browse through these examples to get inspiration for your own skills or to understand different patterns and approaches.
15
+
16
+ The example skills in this repo are open source (Apache 2.0). We've also included the document creation & editing skills that power [Claude's document capabilities](https://www.anthropic.com/news/create-files) under the hood in the [`document-skills/`](./document-skills/) folder. These are source-available, not open source, but we wanted to share these with developers as a reference for more complex skills that are actively used in a production AI application.
17
+
18
+ **Note:** These are reference examples for inspiration and learning. They showcase general-purpose capabilities rather than organization-specific workflows or sensitive content.
19
+
20
+ ## Disclaimer
21
+
22
+ **These skills are provided for demonstration and educational purposes only.** While some of these capabilities may be available in Claude, the implementations and behaviors you receive from Claude may differ from what is shown in these examples. These examples are meant to illustrate patterns and possibilities. Always test skills thoroughly in your own environment before relying on them for critical tasks.
23
+
24
+ # Example Skills
25
+
26
+ This repository includes a diverse collection of example skills demonstrating different capabilities:
27
+
28
+ ## Creative & Design
29
+ - **algorithmic-art** - Create generative art using p5.js with seeded randomness, flow fields, and particle systems
30
+ - **canvas-design** - Design beautiful visual art in .png and .pdf formats using design philosophies
31
+ - **slack-gif-creator** - Create animated GIFs optimized for Slack's size constraints
32
+
33
+ ## Development & Technical
34
+ - **artifacts-builder** - Build complex claude.ai HTML artifacts using React, Tailwind CSS, and shadcn/ui components
35
+ - **mcp-server** - Guide for creating high-quality MCP servers to integrate external APIs and services
36
+ - **webapp-testing** - Test local web applications using Playwright for UI verification and debugging
37
+
38
+ ## Enterprise & Communication
39
+ - **brand-guidelines** - Apply Anthropic's official brand colors and typography to artifacts
40
+ - **internal-comms** - Write internal communications like status reports, newsletters, and FAQs
41
+ - **theme-factory** - Style artifacts with 10 pre-set professional themes or generate custom themes on-the-fly
42
+
43
+ ## Meta Skills
44
+ - **skill-creator** - Guide for creating effective skills that extend Claude's capabilities
45
+ - **template-skill** - A basic template to use as a starting point for new skills
46
+
47
+ # Document Skills
48
+
49
+ The `document-skills/` subdirectory contains skills that Anthropic developed to help Claude create various document file formats. These skills demonstrate advanced patterns for working with complex file formats and binary data:
50
+
51
+ - **docx** - Create, edit, and analyze Word documents with support for tracked changes, comments, formatting preservation, and text extraction
52
+ - **pdf** - Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms
53
+ - **pptx** - Create, edit, and analyze PowerPoint presentations with support for layouts, templates, charts, and automated slide generation
54
+ - **xlsx** - Create, edit, and analyze Excel spreadsheets with support for formulas, formatting, data analysis, and visualization
55
+
56
+ **Important Disclaimer:** These document skills are point-in-time snapshots and are not actively maintained or updated. Versions of these skills ship pre-included with Claude. They are primarily intended as reference examples to illustrate how Anthropic approaches developing more complex skills that work with binary file formats and document structures.
57
+
58
+ # Try in Claude Code, Claude.ai, and the API
59
+
60
+ ## Claude Code
61
+ You can register this repository as a Claude Code Plugin marketplace by running the following command in Claude Code:
62
+ ```
63
+ /plugin marketplace add anthropics/skills
64
+ ```
65
+
66
+ Then, to install a specific set of skills:
67
+ 1. Select `Browse and install plugins`
68
+ 2. Select `anthropic-agent-skills`
69
+ 3. Select `document-skills` or `example-skills`
70
+ 4. Select `Install now`
71
+
72
+ Alternatively, directly install either Plugin via:
73
+ ```
74
+ /plugin install document-skills@anthropic-agent-skills
75
+ /plugin install example-skills@anthropic-agent-skills
76
+ ```
77
+
78
+ After installing the plugin, you can use the skill by just mentioning it. For instance, if you install the `document-skills` plugin from the marketplace, you can ask Claude Code to do something like: "Use the PDF skill to extract the form fields from path/to/some-file.pdf"
79
+
80
+ ## Claude.ai
81
+
82
+ These example skills are all already available to paid plans in Claude.ai.
83
+
84
+ To use any skill from this repository or upload custom skills, follow the instructions in [Using skills in Claude](https://support.claude.com/en/articles/12512180-using-skills-in-claude#h_a4222fa77b).
85
+
86
+ ## Claude API
87
+
88
+ You can use Anthropic's pre-built skills, and upload custom skills, via the Claude API. See the [Skills API Quickstart](https://docs.claude.com/en/api/skills-guide#creating-a-skill) for more.
89
+
90
+ # Creating a Basic Skill
91
+
92
+ Skills are simple to create - just a folder with a `SKILL.md` file containing YAML frontmatter and instructions. You can use the **template-skill** in this repository as a starting point:
93
+
94
+ ```markdown
95
+ ---
96
+ name: my-skill-name
97
+ description: A clear description of what this skill does and when to use it
98
+ ---
99
+
100
+ # My Skill Name
101
+
102
+ [Add your instructions here that Claude will follow when this skill is active]
103
+
104
+ ## Examples
105
+ - Example usage 1
106
+ - Example usage 2
107
+
108
+ ## Guidelines
109
+ - Guideline 1
110
+ - Guideline 2
111
+ ```
112
+
113
+ The frontmatter requires only two fields:
114
+ - `name` - A unique identifier for your skill (lowercase, hyphens for spaces)
115
+ - `description` - A complete description of what the skill does and when to use it
116
+
117
+ The markdown content below contains the instructions, examples, and guidelines that Claude will follow. For more details, see [How to create custom skills](https://support.claude.com/en/articles/12512198-creating-custom-skills).
118
+
119
+ # Partner Skills
120
+
121
+ Skills are a great way to teach Claude how to get better at using specific pieces of software. As we see awesome example skills from partners, we may highlight some of them here:
122
+
123
+ - **Notion** - [Notion Skills for Claude](https://www.notion.so/notiondevs/Notion-Skills-for-Claude-28da4445d27180c7af1df7d8615723d0)
mini_agent/skills/THIRD_PARTY_NOTICES.md ADDED
@@ -0,0 +1,405 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # **Third-Party Notices**
2
+
3
+ THE FOLLOWING SETS FORTH ATTRIBUTION NOTICES FOR THIRD PARTY SOFTWARE THAT MAY BE CONTAINED IN PORTIONS OF THIS PRODUCT.
4
+
5
+ ---
6
+
7
+ ## **BSD 2-Clause License**
8
+
9
+ The following components are licensed under BSD 2-Clause License reproduced below:
10
+
11
+ **imageio 2.37.0**, Copyright (c) 2014-2022, imageio developers
12
+
13
+ **imageio-ffmpeg 0.6.0**, Copyright (c) 2019-2025, imageio
14
+
15
+ **License Text:**
16
+
17
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
18
+
19
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
20
+
21
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
22
+
23
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
+
25
+ ---
26
+
27
+ ## **GNU General Public License v3.0**
28
+
29
+ The following components are licensed under GNU General Public License v3.0 reproduced below:
30
+
31
+ **FFmpeg 7.0.2**, Copyright (c) 2000-2024 the FFmpeg developers
32
+
33
+ Source Code: [https://ffmpeg.org/releases/ffmpeg-7.0.2.tar.xz](https://ffmpeg.org/releases/ffmpeg-7.0.2.tar.xz)
34
+
35
+ **License Text:**
36
+
37
+ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007
38
+
39
+ Copyright © 2007 Free Software Foundation, Inc. [https://fsf.org/](https://fsf.org/)
40
+
41
+ Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
42
+
43
+ Preamble
44
+
45
+ The GNU General Public License is a free, copyleft license for software and other kinds of works.
46
+
47
+ The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
48
+
49
+ When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
50
+
51
+ To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
52
+
53
+ For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
54
+
55
+ Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
56
+
57
+ For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
58
+
59
+ Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
60
+
61
+ Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
62
+
63
+ The precise terms and conditions for copying, distribution and modification follow.
64
+
65
+ TERMS AND CONDITIONS
66
+
67
+ 0. Definitions.
68
+
69
+ "This License" refers to version 3 of the GNU General Public License.
70
+
71
+ "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
72
+
73
+ "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
74
+
75
+ To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
76
+
77
+ A "covered work" means either the unmodified Program or a work based on the Program.
78
+
79
+ To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
80
+
81
+ To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
82
+
83
+ An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
84
+
85
+ 1. Source Code.
86
+
87
+ The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
88
+
89
+ A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
90
+
91
+ The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
92
+
93
+ The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
94
+
95
+ The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
96
+
97
+ The Corresponding Source for a work in source code form is that same work.
98
+
99
+ 2. Basic Permissions.
100
+
101
+ All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
102
+
103
+ You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
104
+
105
+ Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
106
+
107
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
108
+
109
+ No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
110
+
111
+ When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
112
+
113
+ 4. Conveying Verbatim Copies.
114
+
115
+ You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
116
+
117
+ You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
118
+
119
+ 5. Conveying Modified Source Versions.
120
+
121
+ You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
122
+
123
+ a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
124
+
125
+ b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7\. This requirement modifies the requirement in section 4 to "keep intact all notices".
126
+
127
+ c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
128
+
129
+ d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
130
+
131
+ A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
132
+
133
+ 6. Conveying Non-Source Forms.
134
+
135
+ You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
136
+
137
+ a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
138
+
139
+ b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
140
+
141
+ c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
142
+
143
+ d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
144
+
145
+ e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
146
+
147
+ A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
148
+
149
+ A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
150
+
151
+ "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
152
+
153
+ If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
154
+
155
+ The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
156
+
157
+ Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
158
+
159
+ 7. Additional Terms.
160
+
161
+ "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
162
+
163
+ When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
164
+
165
+ Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
166
+
167
+ a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
168
+
169
+ b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
170
+
171
+ c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
172
+
173
+ d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
174
+
175
+ e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
176
+
177
+ f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
178
+
179
+ All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10\. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
180
+
181
+ If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
182
+
183
+ Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
184
+
185
+ 8. Termination.
186
+
187
+ You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
188
+
189
+ However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
190
+
191
+ Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
192
+
193
+ Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10\.
194
+
195
+ 9. Acceptance Not Required for Having Copies.
196
+
197
+ You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
198
+
199
+ 10. Automatic Licensing of Downstream Recipients.
200
+
201
+ Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
202
+
203
+ An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
204
+
205
+ You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
206
+
207
+ 11. Patents.
208
+
209
+ A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
210
+
211
+ A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
212
+
213
+ Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
214
+
215
+ In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
216
+
217
+ If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
218
+
219
+ If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
220
+
221
+ A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007\.
222
+
223
+ Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
224
+
225
+ 12. No Surrender of Others' Freedom.
226
+
227
+ If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
228
+
229
+ 13. Use with the GNU Affero General Public License.
230
+
231
+ Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
232
+
233
+ 14. Revised Versions of this License.
234
+
235
+ The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
236
+
237
+ Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
238
+
239
+ If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
240
+
241
+ Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
242
+
243
+ 15. Disclaimer of Warranty.
244
+
245
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
246
+
247
+ 16. Limitation of Liability.
248
+
249
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
250
+
251
+ 17. Interpretation of Sections 15 and 16\.
252
+
253
+ If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
254
+
255
+ END OF TERMS AND CONDITIONS
256
+
257
+ How to Apply These Terms to Your New Programs
258
+
259
+ If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
260
+
261
+ To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
262
+
263
+ \<one line to give the program's name and a brief idea of what it does.\>
264
+ Copyright (C) \<year\> \<name of author\>
265
+
266
+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
267
+
268
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
269
+
270
+ You should have received a copy of the GNU General Public License along with this program. If not, see [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/).
271
+
272
+ Also add information on how to contact you by electronic and paper mail.
273
+
274
+ If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
275
+
276
+ \<program\> Copyright (C) \<year\> \<name of author\>
277
+ This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. This is free software, and you are welcome to redistribute it under certain conditions; type 'show c' for details.
278
+
279
+ The hypothetical commands 'show w' and 'show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box".
280
+
281
+ You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/).
282
+
283
+ The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read [https://www.gnu.org/licenses/why-not-lgpl.html](https://www.gnu.org/licenses/why-not-lgpl.html).
284
+
285
+ ---
286
+
287
+ ## **MIT-CMU License (HPND)**
288
+
289
+ The following components are licensed under MIT-CMU License (HPND) reproduced below:
290
+
291
+ **Pillow 11.3.0**, Copyright © 1997-2011 by Secret Labs AB, Copyright © 1995-2011 by Fredrik Lundh and contributors, Copyright © 2010 by Jeffrey A. Clark and contributors
292
+
293
+ **License Text:**
294
+
295
+ By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions:
296
+
297
+ Permission to use, copy, modify and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission.
298
+
299
+ SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
300
+
301
+ ---
302
+
303
+ ## **SIL Open Font License v1.1**
304
+
305
+ The following fonts are licensed under SIL Open Font License v1.1 reproduced below:
306
+
307
+ **Arsenal SC**, Copyright 2012 The Arsenal Project Authors ([andrij.design@gmail.com](mailto:andrij.design@gmail.com))
308
+
309
+ **Big Shoulders**, Copyright 2019 The Big Shoulders Project Authors ([https://github.com/xotypeco/big\_shoulders](https://github.com/xotypeco/big_shoulders))
310
+
311
+ **Boldonse**, Copyright 2024 The Boldonse Project Authors ([https://github.com/googlefonts/boldonse](https://github.com/googlefonts/boldonse))
312
+
313
+ **Bricolage Grotesque**, Copyright 2022 The Bricolage Grotesque Project Authors ([https://github.com/ateliertriay/bricolage](https://github.com/ateliertriay/bricolage))
314
+
315
+ **Crimson Pro**, Copyright 2018 The Crimson Pro Project Authors ([https://github.com/Fonthausen/CrimsonPro](https://github.com/Fonthausen/CrimsonPro))
316
+
317
+ **DM Mono**, Copyright 2020 The DM Mono Project Authors ([https://www.github.com/googlefonts/dm-mono](https://www.github.com/googlefonts/dm-mono))
318
+
319
+ **Erica One**, Copyright (c) 2011 by LatinoType Limitada ([luciano@latinotype.com](mailto:luciano@latinotype.com)), with Reserved Font Name "Erica One"
320
+
321
+ **Geist Mono**, Copyright 2024 The Geist Project Authors ([https://github.com/vercel/geist-font.git](https://github.com/vercel/geist-font.git))
322
+
323
+ **Gloock**, Copyright 2022 The Gloock Project Authors ([https://github.com/duartp/gloock](https://github.com/duartp/gloock))
324
+
325
+ **IBM Plex Mono**, Copyright © 2017 IBM Corp., with Reserved Font Name "Plex"
326
+
327
+ **Instrument Sans**, Copyright 2022 The Instrument Sans Project Authors ([https://github.com/Instrument/instrument-sans](https://github.com/Instrument/instrument-sans))
328
+
329
+ **Italiana**, Copyright (c) 2011, Santiago Orozco ([hi@typemade.mx](mailto:hi@typemade.mx)), with Reserved Font Name "Italiana"
330
+
331
+ **JetBrains Mono**, Copyright 2020 The JetBrains Mono Project Authors ([https://github.com/JetBrains/JetBrainsMono](https://github.com/JetBrains/JetBrainsMono))
332
+
333
+ **Jura**, Copyright 2019 The Jura Project Authors ([https://github.com/ossobuffo/jura](https://github.com/ossobuffo/jura))
334
+
335
+ **Libre Baskerville**, Copyright 2012 The Libre Baskerville Project Authors ([https://github.com/impallari/Libre-Baskerville](https://github.com/impallari/Libre-Baskerville)), with Reserved Font Name "Libre Baskerville"
336
+
337
+ **Lora**, Copyright 2011 The Lora Project Authors ([https://github.com/cyrealtype/Lora-Cyrillic](https://github.com/cyrealtype/Lora-Cyrillic)), with Reserved Font Name "Lora"
338
+
339
+ **National Park**, Copyright 2025 The National Park Project Authors ([https://github.com/benhoepner/National-Park](https://github.com/benhoepner/National-Park))
340
+
341
+ **Nothing You Could Do**, Copyright (c) 2010, Kimberly Geswein (kimberlygeswein.com)
342
+
343
+ **Outfit**, Copyright 2021 The Outfit Project Authors ([https://github.com/Outfitio/Outfit-Fonts](https://github.com/Outfitio/Outfit-Fonts))
344
+
345
+ **Pixelify Sans**, Copyright 2021 The Pixelify Sans Project Authors ([https://github.com/eifetx/Pixelify-Sans](https://github.com/eifetx/Pixelify-Sans))
346
+
347
+ **Poiret One**, Copyright (c) 2011, Denis Masharov ([denis.masharov@gmail.com](mailto:denis.masharov@gmail.com))
348
+
349
+ **Red Hat Mono**, Copyright 2024 The Red Hat Project Authors ([https://github.com/RedHatOfficial/RedHatFont](https://github.com/RedHatOfficial/RedHatFont))
350
+
351
+ **Silkscreen**, Copyright 2001 The Silkscreen Project Authors ([https://github.com/googlefonts/silkscreen](https://github.com/googlefonts/silkscreen))
352
+
353
+ **Smooch Sans**, Copyright 2016 The Smooch Sans Project Authors ([https://github.com/googlefonts/smooch-sans](https://github.com/googlefonts/smooch-sans))
354
+
355
+ **Tektur**, Copyright 2023 The Tektur Project Authors ([https://www.github.com/hyvyys/Tektur](https://www.github.com/hyvyys/Tektur))
356
+
357
+ **Work Sans**, Copyright 2019 The Work Sans Project Authors ([https://github.com/weiweihuanghuang/Work-Sans](https://github.com/weiweihuanghuang/Work-Sans))
358
+
359
+ **Young Serif**, Copyright 2023 The Young Serif Project Authors ([https://github.com/noirblancrouge/YoungSerif](https://github.com/noirblancrouge/YoungSerif))
360
+
361
+ **License Text:**
362
+
363
+ ---
364
+
365
+ ## **SIL OPEN FONT LICENSE Version 1.1 \- 26 February 2007**
366
+
367
+ PREAMBLE
368
+
369
+ The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
370
+
371
+ The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
372
+
373
+ DEFINITIONS
374
+
375
+ "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
376
+
377
+ "Reserved Font Name" refers to any names specified as such after the copyright statement(s).
378
+
379
+ "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
380
+
381
+ "Modified Version" refers to any derivative made by adding to, deleting, or substituting \-- in part or in whole \-- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
382
+
383
+ "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
384
+
385
+ PERMISSION & CONDITIONS
386
+
387
+ Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
388
+
389
+ 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
390
+
391
+ 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
392
+
393
+ 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
394
+
395
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
396
+
397
+ 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
398
+
399
+ TERMINATION
400
+
401
+ This license becomes null and void if any of the above conditions are not met.
402
+
403
+ DISCLAIMER
404
+
405
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
mini_agent/skills/agent_skills_spec.md ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agent Skills Spec
2
+
3
+ A skill is a folder of instructions, scripts, and resources that agents can discover and load dynamically to perform better at specific tasks. In order for the folder to be recognized as a skill, it must contain a `SKILL.md` file.
4
+
5
+ # Skill Folder Layout
6
+
7
+ A minimal skill folder looks like this:
8
+
9
+ ```
10
+ my-skill/
11
+ - SKILL.md
12
+ ```
13
+
14
+ More complex skills can add additional directories and files as needed.
15
+
16
+
17
+ # The SKILL.md file
18
+
19
+ The skill's "entrypoint" is the `SKILL.md` file. It is the only file required to exist. The file must start with a YAML frontmatter followed by regular Markdown.
20
+
21
+ ## YAML Frontmatter
22
+
23
+ The YAML frontmatter has 2 required properties:
24
+
25
+ - `name`
26
+ - The name of the skill in hyphen-case
27
+ - Restricted to lowercase Unicode alphanumeric + hyphen
28
+ - Must match the name of the directory containing the SKILL.md
29
+ - `description`
30
+ - Description of what the skill does and when Claude should use it
31
+
32
+ There are 3 optional properties:
33
+
34
+ - `license`
35
+ - The license applied to the skill
36
+ - We recommend keeping it short (either the name of a license or the name of a bundled license file)
37
+ - `allowed-tools`
38
+ - A list of tools that are pre-approved to run
39
+ - Currently only supported in Claude Code
40
+ - `metadata`
41
+ - A map from string keys to string values
42
+ - Clients can use this to store additional properties not defined by the Agent Skills Spec
43
+ - We recommend making your key names reasonably unique to avoid accidental conflicts
44
+
45
+ ## Markdown Body
46
+
47
+ The Markdown body has no restrictions on it.
48
+
49
+ # Additional Information
50
+
51
+ For a minimal example, see the `template-skill` example.
52
+
53
+ # Version History
54
+
55
+ - 1.0 (2025-10-16) Public Launch
mini_agent/skills/algorithmic-art/LICENSE.txt ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.