qd-evolve 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qd_evolve-0.1.0/.gitignore +26 -0
- qd_evolve-0.1.0/.python-version +1 -0
- qd_evolve-0.1.0/CLAUDE.md +54 -0
- qd_evolve-0.1.0/DESIGN.md +557 -0
- qd_evolve-0.1.0/LICENSE +21 -0
- qd_evolve-0.1.0/PKG-INFO +340 -0
- qd_evolve-0.1.0/README.md +295 -0
- qd_evolve-0.1.0/config.json.example +339 -0
- qd_evolve-0.1.0/manifesto.md +71 -0
- qd_evolve-0.1.0/pyproject.toml +89 -0
- qd_evolve-0.1.0/qd_evolve/__init__.py +1 -0
- qd_evolve-0.1.0/qd_evolve/_templates/_system_tail.j2 +45 -0
- qd_evolve-0.1.0/qd_evolve/_templates/a2a-default.j2 +28 -0
- qd_evolve-0.1.0/qd_evolve/_templates/a2a-heartbeat.j2 +1 -0
- qd_evolve-0.1.0/qd_evolve/_templates/default.j2 +14 -0
- qd_evolve-0.1.0/qd_evolve/_templates/group-default.j2 +14 -0
- qd_evolve-0.1.0/qd_evolve/_templates/group-heartbeat.j2 +1 -0
- qd_evolve-0.1.0/qd_evolve/_templates/group-message.j2 +1 -0
- qd_evolve-0.1.0/qd_evolve/_templates/heartbeat.j2 +1 -0
- qd_evolve-0.1.0/qd_evolve/_templates/mqtt-default.j2 +29 -0
- qd_evolve-0.1.0/qd_evolve/_templates/mqtt-heartbeat.j2 +1 -0
- qd_evolve-0.1.0/qd_evolve/a2a_cli.py +959 -0
- qd_evolve-0.1.0/qd_evolve/agent/__init__.py +6 -0
- qd_evolve-0.1.0/qd_evolve/agent/a2a.py +148 -0
- qd_evolve-0.1.0/qd_evolve/agent/a2a_agent.py +313 -0
- qd_evolve-0.1.0/qd_evolve/agent/a2a_tools.py +320 -0
- qd_evolve-0.1.0/qd_evolve/agent/agent.py +928 -0
- qd_evolve-0.1.0/qd_evolve/agent/group_chat_agent.py +319 -0
- qd_evolve-0.1.0/qd_evolve/agent/group_chat_human.py +106 -0
- qd_evolve-0.1.0/qd_evolve/agent/group_chat_transport.py +256 -0
- qd_evolve-0.1.0/qd_evolve/agent/group_chat_wechat_human.py +161 -0
- qd_evolve-0.1.0/qd_evolve/agent/human_agent.py +188 -0
- qd_evolve-0.1.0/qd_evolve/agent/loader.py +300 -0
- qd_evolve-0.1.0/qd_evolve/agent/mqtt_agent.py +692 -0
- qd_evolve-0.1.0/qd_evolve/agent/mqtt_human_agent.py +423 -0
- qd_evolve-0.1.0/qd_evolve/agent/mqtt_transport.py +782 -0
- qd_evolve-0.1.0/qd_evolve/agent/protocol.py +36 -0
- qd_evolve-0.1.0/qd_evolve/agent/registry.py +100 -0
- qd_evolve-0.1.0/qd_evolve/agent/server.py +528 -0
- qd_evolve-0.1.0/qd_evolve/agent/transport.py +569 -0
- qd_evolve-0.1.0/qd_evolve/bridge/__init__.py +0 -0
- qd_evolve-0.1.0/qd_evolve/bridge/wechat_clawbot_client.py +413 -0
- qd_evolve-0.1.0/qd_evolve/chat_cli.py +559 -0
- qd_evolve-0.1.0/qd_evolve/cli_tools.py +78 -0
- qd_evolve-0.1.0/qd_evolve/cli_utils.py +42 -0
- qd_evolve-0.1.0/qd_evolve/core/__init__.py +1 -0
- qd_evolve-0.1.0/qd_evolve/core/config.py +248 -0
- qd_evolve-0.1.0/qd_evolve/core/logger.py +59 -0
- qd_evolve-0.1.0/qd_evolve/core/memory.py +405 -0
- qd_evolve-0.1.0/qd_evolve/core/prompts.py +69 -0
- qd_evolve-0.1.0/qd_evolve/core/providers.py +89 -0
- qd_evolve-0.1.0/qd_evolve/core/registry.py +221 -0
- qd_evolve-0.1.0/qd_evolve/core/toolbox.py +189 -0
- qd_evolve-0.1.0/qd_evolve/gchat_cli.py +477 -0
- qd_evolve-0.1.0/qd_evolve/memory_tui.py +283 -0
- qd_evolve-0.1.0/qd_evolve/mqtt_cli.py +982 -0
- qd_evolve-0.1.0/qd_evolve/skills.py +144 -0
- qd_evolve-0.1.0/qd_evolve/toolbox_tui.py +695 -0
- qd_evolve-0.1.0/qd_evolve/tools/__init__.py +5 -0
- qd_evolve-0.1.0/qd_evolve/tools/cli_loader.py +41 -0
- qd_evolve-0.1.0/qd_evolve/tools/install_func.py +113 -0
- qd_evolve-0.1.0/qd_evolve/tools/install_mcp.py +102 -0
- qd_evolve-0.1.0/qd_evolve/tools/install_skill.py +117 -0
- qd_evolve-0.1.0/qd_evolve/tools/recall_memory.py +93 -0
- qd_evolve-0.1.0/qd_evolve/tools/register_func.py +92 -0
- qd_evolve-0.1.0/qd_evolve/tools/register_mcp.py +45 -0
- qd_evolve-0.1.0/qd_evolve/tools/register_skill.py +45 -0
- qd_evolve-0.1.0/qd_evolve/tools/skill_loader.py +39 -0
- qd_evolve-0.1.0/qd_evolve/tools/staging.py +41 -0
- qd_evolve-0.1.0/qd_evolve/tools/tool_loader.py +42 -0
- qd_evolve-0.1.0/qd_evolve/utils/__init__.py +0 -0
- qd_evolve-0.1.0/qd_evolve/utils/adk_output.py +66 -0
- qd_evolve-0.1.0/qd_evolve/utils/adk_schema.py +103 -0
- qd_evolve-0.1.0/tests/TEST_PLAN.md +180 -0
- qd_evolve-0.1.0/tests/__init__.py +0 -0
- qd_evolve-0.1.0/tests/agent/__init__.py +0 -0
- qd_evolve-0.1.0/tests/agent/test_a2a_models.py +311 -0
- qd_evolve-0.1.0/tests/agent/test_agent_core.py +327 -0
- qd_evolve-0.1.0/tests/agent/test_agent_run.py +440 -0
- qd_evolve-0.1.0/tests/agent/test_group_chat_agent.py +297 -0
- qd_evolve-0.1.0/tests/agent/test_group_chat_human.py +159 -0
- qd_evolve-0.1.0/tests/agent/test_group_chat_transport.py +154 -0
- qd_evolve-0.1.0/tests/agent/test_human_agent.py +132 -0
- qd_evolve-0.1.0/tests/agent/test_loader.py +122 -0
- qd_evolve-0.1.0/tests/agent/test_mqtt_agent.py +280 -0
- qd_evolve-0.1.0/tests/agent/test_mqtt_human_agent.py +137 -0
- qd_evolve-0.1.0/tests/agent/test_mqtt_transport.py +243 -0
- qd_evolve-0.1.0/tests/agent/test_push_notification.py +392 -0
- qd_evolve-0.1.0/tests/agent/test_registry.py +136 -0
- qd_evolve-0.1.0/tests/agent/test_server.py +232 -0
- qd_evolve-0.1.0/tests/agent/test_transport.py +119 -0
- qd_evolve-0.1.0/tests/cli/__init__.py +0 -0
- qd_evolve-0.1.0/tests/cli/test_replay.py +89 -0
- qd_evolve-0.1.0/tests/cli/test_slash_commands.py +23 -0
- qd_evolve-0.1.0/tests/conftest.py +220 -0
- qd_evolve-0.1.0/tests/core/__init__.py +0 -0
- qd_evolve-0.1.0/tests/core/test_config.py +445 -0
- qd_evolve-0.1.0/tests/core/test_logger.py +75 -0
- qd_evolve-0.1.0/tests/core/test_memory.py +377 -0
- qd_evolve-0.1.0/tests/core/test_prompts.py +125 -0
- qd_evolve-0.1.0/tests/core/test_providers.py +162 -0
- qd_evolve-0.1.0/tests/core/test_registry.py +240 -0
- qd_evolve-0.1.0/tests/core/test_toolbox.py +392 -0
- qd_evolve-0.1.0/tests/replay/basic.txt +5 -0
- qd_evolve-0.1.0/tests/replay/slash_commands.txt +6 -0
- qd_evolve-0.1.0/tests/skills/__init__.py +0 -0
- qd_evolve-0.1.0/tests/test_cli_tools.py +181 -0
- qd_evolve-0.1.0/tests/test_skills.py +248 -0
- qd_evolve-0.1.0/tests/tools/__init__.py +0 -0
- qd_evolve-0.1.0/tests/tools/test_a2a_tools.py +131 -0
- qd_evolve-0.1.0/tests/tools/test_adk_output.py +118 -0
- qd_evolve-0.1.0/tests/tools/test_adk_schema.py +173 -0
- qd_evolve-0.1.0/tests/tools/test_cli_loader.py +45 -0
- qd_evolve-0.1.0/tests/tools/test_install_func.py +134 -0
- qd_evolve-0.1.0/tests/tools/test_install_mcp.py +97 -0
- qd_evolve-0.1.0/tests/tools/test_install_skill.py +216 -0
- qd_evolve-0.1.0/tests/tools/test_recall_memory.py +103 -0
- qd_evolve-0.1.0/tests/tools/test_register_func.py +169 -0
- qd_evolve-0.1.0/tests/tools/test_register_mcp.py +68 -0
- qd_evolve-0.1.0/tests/tools/test_register_skill.py +73 -0
- qd_evolve-0.1.0/tests/tools/test_skill_loader.py +43 -0
- qd_evolve-0.1.0/tests/tools/test_staging.py +52 -0
- qd_evolve-0.1.0/tests/tools/test_tool_loader.py +30 -0
- qd_evolve-0.1.0/tools/bridge/__init__.py +194 -0
- qd_evolve-0.1.0/tools/bridge/_mcp.py +320 -0
- qd_evolve-0.1.0/tools/bridge/_oat.py +192 -0
- qd_evolve-0.1.0/tools/bridge/oat.json +17 -0
- qd_evolve-0.1.0/tools/func/fetch.py +71 -0
- qd_evolve-0.1.0/tools/func/file_rw.py +95 -0
- qd_evolve-0.1.0/tools/func/run_python.py +78 -0
- qd_evolve-0.1.0/tools/func/run_shell.py +84 -0
- qd_evolve-0.1.0/tools/func/search.py +109 -0
- qd_evolve-0.1.0/tools/mcp/amap-maps.json +11 -0
- qd_evolve-0.1.0/tools/mcp/open-meteo-mcp-server.json +8 -0
- qd_evolve-0.1.0/tools/mcp/playwright.json +11 -0
- qd_evolve-0.1.0/tools/mcp/tavily.json +11 -0
- qd_evolve-0.1.0/tools/skills/baidu-search/SKILL.md +63 -0
- qd_evolve-0.1.0/tools/skills/baidu-search/_meta.json +6 -0
- qd_evolve-0.1.0/tools/skills/baidu-search/references/apikey-fetch.md +58 -0
- qd_evolve-0.1.0/tools/skills/baidu-search/scripts/search.py +123 -0
- qd_evolve-0.1.0/tools/skills/cli-register/SKILL.md +39 -0
- qd_evolve-0.1.0/tools/skills/cli-register/_meta.json +5 -0
- qd_evolve-0.1.0/tools/skills/find-tools/SKILL.md +46 -0
- qd_evolve-0.1.0/tools/skills/self-improvement/SKILL.md +572 -0
- qd_evolve-0.1.0/tools/skills/self-improvement/assets/LEARNINGS.md +45 -0
- qd_evolve-0.1.0/tools/skills/self-improvement/assets/SKILL-TEMPLATE.md +177 -0
- qd_evolve-0.1.0/tools/skills/self-improvement/hooks/openclaw/HOOK.md +23 -0
- qd_evolve-0.1.0/tools/skills/self-improvement/hooks/openclaw/handler.ts +46 -0
- qd_evolve-0.1.0/tools/skills/self-improvement/references/examples.md +374 -0
- qd_evolve-0.1.0/tools/skills/self-improvement/references/hooks-setup.md +223 -0
- qd_evolve-0.1.0/tools/skills/self-improvement/references/openclaw-integration.md +311 -0
- qd_evolve-0.1.0/tools/skills/self-improvement/scripts/activator.sh +20 -0
- qd_evolve-0.1.0/tools/skills/self-improvement/scripts/error-detector.sh +55 -0
- qd_evolve-0.1.0/tools/skills/self-improvement/scripts/extract-skill.sh +203 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/LICENSE.txt +202 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/SKILL.md +485 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/agents/analyzer.md +274 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/agents/comparator.md +202 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/agents/grader.md +223 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/assets/eval_review.html +146 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/references/schemas.md +430 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/scripts/__init__.py +0 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/scripts/generate_report.py +326 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/scripts/improve_description.py +247 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/scripts/package_skill.py +136 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/scripts/quick_validate.py +103 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/scripts/run_eval.py +310 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/scripts/run_loop.py +328 -0
- qd_evolve-0.1.0/tools/skills/skill-creator/scripts/utils.py +47 -0
- qd_evolve-0.1.0/uv.lock +3361 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
.mypy_cache/
|
|
4
|
+
*.py[oc]
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
wheels/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv
|
|
12
|
+
|
|
13
|
+
# IDE
|
|
14
|
+
.vscode/
|
|
15
|
+
|
|
16
|
+
# Claude Code
|
|
17
|
+
.claude/
|
|
18
|
+
|
|
19
|
+
# Config and data (contain secrets)
|
|
20
|
+
config.json
|
|
21
|
+
*.db
|
|
22
|
+
logs/
|
|
23
|
+
.qd_evolve/
|
|
24
|
+
|
|
25
|
+
# Test artifacts
|
|
26
|
+
.coverage
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
|
|
2
|
+
|
|
3
|
+
Tradeoff: These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
|
4
|
+
|
|
5
|
+
1. Think Before Coding
|
|
6
|
+
Don't assume. Don't hide confusion. Surface tradeoffs.
|
|
7
|
+
|
|
8
|
+
Before implementing:
|
|
9
|
+
|
|
10
|
+
State your assumptions explicitly. If uncertain, ask.
|
|
11
|
+
If multiple interpretations exist, present them - don't pick silently.
|
|
12
|
+
If a simpler approach exists, say so. Push back when warranted.
|
|
13
|
+
If something is unclear, stop. Name what's confusing. Ask.
|
|
14
|
+
2. Simplicity First
|
|
15
|
+
Minimum code that solves the problem. Nothing speculative.
|
|
16
|
+
|
|
17
|
+
No features beyond what was asked.
|
|
18
|
+
No abstractions for single-use code.
|
|
19
|
+
No "flexibility" or "configurability" that wasn't requested.
|
|
20
|
+
No error handling for impossible scenarios.
|
|
21
|
+
If you write 200 lines and it could be 50, rewrite it.
|
|
22
|
+
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
|
23
|
+
|
|
24
|
+
3. Surgical Changes
|
|
25
|
+
Touch only what you must. Clean up only your own mess.
|
|
26
|
+
|
|
27
|
+
When editing existing code:
|
|
28
|
+
|
|
29
|
+
Don't "improve" adjacent code, comments, or formatting.
|
|
30
|
+
Don't refactor things that aren't broken.
|
|
31
|
+
Match existing style, even if you'd do it differently.
|
|
32
|
+
If you notice unrelated dead code, mention it - don't delete it.
|
|
33
|
+
When your changes create orphans:
|
|
34
|
+
|
|
35
|
+
Remove imports/variables/functions that YOUR changes made unused.
|
|
36
|
+
Don't remove pre-existing dead code unless asked.
|
|
37
|
+
The test: Every changed line should trace directly to the user's request.
|
|
38
|
+
|
|
39
|
+
4. Goal-Driven Execution
|
|
40
|
+
Define success criteria. Loop until verified.
|
|
41
|
+
|
|
42
|
+
Transform tasks into verifiable goals:
|
|
43
|
+
|
|
44
|
+
"Add validation" → "Write tests for invalid inputs, then make them pass"
|
|
45
|
+
"Fix the bug" → "Write a test that reproduces it, then make it pass"
|
|
46
|
+
"Refactor X" → "Ensure tests pass before and after"
|
|
47
|
+
For multi-step tasks, state a brief plan:
|
|
48
|
+
|
|
49
|
+
1. [Step] → verify: [check]
|
|
50
|
+
2. [Step] → verify: [check]
|
|
51
|
+
3. [Step] → verify: [check]
|
|
52
|
+
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
|
53
|
+
|
|
54
|
+
These guidelines are working if: fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
# Design & Implementation
|
|
2
|
+
|
|
3
|
+
QD-Evolve is a multi-agent AI framework built on the belief that intelligence emerges, it isn't engineered. The design follows from the [manifesto](manifesto.md): give the model a minimal loop, a messy toolbox, and the ability to grow its own capabilities — then get out of the way.
|
|
4
|
+
|
|
5
|
+
## Core Philosophy
|
|
6
|
+
|
|
7
|
+
**The model knows best.** Every design decision starts from the premise that the LLM — not the framework — should decide what to do, when to do it, and how. The framework's job is to provide capabilities, not prescribe strategies. We don't encode ReAct, Plan-and-Execute, or any other reasoning template. The loop is: reason → call tools → observe → repeat. That's it.
|
|
8
|
+
|
|
9
|
+
**Emergence over engineering.** Multi-agent collaboration has no preset roles, no voting protocols, no orchestrator. Agents discover each other, send messages, and self-organize. Memory has no forgetting curves or episodic structures — just save and recall. The model learns what to keep.
|
|
10
|
+
|
|
11
|
+
**Physical isolation as the security boundary.** Software permissions, sandboxes, and content filters are all guardrails that a sufficiently capable model can talk its way past. The only meaningful security boundary is whether the model can physically affect the world without a human in the loop.
|
|
12
|
+
|
|
13
|
+
## Design Decisions and Trade-offs
|
|
14
|
+
|
|
15
|
+
### No orchestration layer
|
|
16
|
+
|
|
17
|
+
There is no Planner, no Executor, no Critic. Agents are peers. The trade-off: emergent coordination is less predictable than scripted workflows. The bet: as models improve, emergent coordination outperforms hand-coded protocols, and the framework won't need to be rewritten to keep up.
|
|
18
|
+
|
|
19
|
+
### No memory architecture
|
|
20
|
+
|
|
21
|
+
Save and recall is the entire memory surface. The trade-off: the model might miss important context that a sophisticated memory system would surface. The bet: the model's own attention mechanism is a better retrieval algorithm than any forgetting curve or episodic structure we could hard-code.
|
|
22
|
+
|
|
23
|
+
### Thread-locked agent loop
|
|
24
|
+
|
|
25
|
+
Agents serialize concurrent calls with a lock rather than supporting parallel execution. The trade-off: slower under concurrent load. The bet: correctness matters more than throughput, and concurrent LLM calls to the same agent would corrupt shared state (message list, tool registrations, memory).
|
|
26
|
+
|
|
27
|
+
### On-demand tool schemas
|
|
28
|
+
|
|
29
|
+
Tools start invisible to the model, revealed only when needed. The trade-off: extra round-trips when the model discovers it needs a tool. The bet: the prompt size savings (hundreds of tools × thousands of schema tokens) outweigh the latency of an extra `load_func` call.
|
|
30
|
+
|
|
31
|
+
### One config file
|
|
32
|
+
|
|
33
|
+
No environment variables, no CLI config, no database-backed settings. The trade-off: less flexible for containerized deployment where env vars are idiomatic. The bet: simplicity and discoverability matter more for a framework meant to be understood and modified.
|
|
34
|
+
|
|
35
|
+
### Physical isolation over software security
|
|
36
|
+
|
|
37
|
+
No sandbox, no permission system, no content filter. The trade-off: the model can do dangerous things if given dangerous tools. The design response: don't give it dangerous tools. The security boundary is what the model can physically reach — network access, filesystem access, process execution. A human presses the last button.
|
|
38
|
+
|
|
39
|
+
## Invariants
|
|
40
|
+
|
|
41
|
+
These are the constraints that every change must preserve:
|
|
42
|
+
|
|
43
|
+
1. **The agent loop is `reason → act → observe`.** No phases, no templates, no planning steps.
|
|
44
|
+
2. **Agents compose by wrapping, not inheritance.** Each layer adds exactly one concern.
|
|
45
|
+
3. **No more than one remote transport at a time.** In-process + HTTP, or in-process + MQTT, never both.
|
|
46
|
+
4. **MQTT transport is sole-consumer.** Group chat gets its own transport connection.
|
|
47
|
+
5. **Human and AI agents share the same protocol.** The transport layer doesn't distinguish them.
|
|
48
|
+
6. **Memory is save + recall + process capture.** Each save records the full Q/A along with the tool call process (name, parameters, success/failure). No forgetting curves, no episodic structures, no automatic categorization.
|
|
49
|
+
7. **Configuration is one file.** No env vars, no scattered config.
|
|
50
|
+
8. **Security is physical, not digital.** No software permission system that the model could reason past.
|
|
51
|
+
|
|
52
|
+
## Architecture
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
56
|
+
│ Chat CLI │ │ A2A CLI │ │ MQTT CLI │ │ GChat CLI │
|
|
57
|
+
│ (in-proc) │ │ (HTTP/SSE) │ │ (MQTT v5) │ │ (MQTT v5) │
|
|
58
|
+
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
|
59
|
+
│ │ │ │
|
|
60
|
+
▼ ▼ ▼ ▼
|
|
61
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
62
|
+
│ Agent Layer │
|
|
63
|
+
│ Agent ← A2AAgent ← MqttAgent ← GroupChatAgent | HumanAgent │
|
|
64
|
+
│ GroupChatWechatHuman (bridge) │
|
|
65
|
+
│ AgentRegistry | TransportRouter | EventSubscribers │
|
|
66
|
+
└──────────────────────────┬───────────────────────────────────────────┘
|
|
67
|
+
│
|
|
68
|
+
┌──────────────────┼──────────────────┐
|
|
69
|
+
▼ ▼ ▼
|
|
70
|
+
┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
71
|
+
│ Provider │ │ Memory │ │ Toolbox │
|
|
72
|
+
│ Registry │ │ Store │ │ Registry │
|
|
73
|
+
└──────────┘ └──────────┘ └──────────┘
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Agent Loop
|
|
77
|
+
|
|
78
|
+
The central abstraction: `reason → act → observe`, repeated until the model produces a text response with no tool calls. No planning phase, no reflection phase. The loop is guarded by a single `threading.Lock` — one invocation per agent at a time to prevent concurrent corruption of messages and tool state.
|
|
79
|
+
|
|
80
|
+
### Agent Hierarchy
|
|
81
|
+
|
|
82
|
+
Composition via wrapping, not inheritance:
|
|
83
|
+
|
|
84
|
+
- **Agent** (`agent/agent.py`): Pure LLM loop. Manages messages, memory recall, context compression, heartbeat, tool execution. Knows nothing of networks or other agents.
|
|
85
|
+
- **A2AAgent** (`agent/a2a_agent.py`): Wraps Agent, adds A2A identity (AgentCard, TaskStore), event subscriber fan-out.
|
|
86
|
+
- **MqttAgent** (`agent/mqtt_agent.py`): Wraps A2AAgent, adds MQTT v5 lifecycle (connect, LWT, subscribe, publish).
|
|
87
|
+
- **GroupChatAgent** (`agent/group_chat_agent.py`): Wraps MqttAgent, adds group chat — subscribes to `/chat` topics, deduplication, parallel `agent.run()`, group message publishing.
|
|
88
|
+
- **HumanAgent** (`agent/human_agent.py`): Implements AgentProtocol directly. No LLM, no tools, no memory. Returns `input_required`, completes asynchronously via webhook.
|
|
89
|
+
- **MqttHumanAgent** (`agent/mqtt_human_agent.py`): MQTT wrapper for HumanAgent.
|
|
90
|
+
- **GroupChatHuman** (`agent/group_chat_human.py`): Wraps MqttHumanAgent, adds terminal-based group chat UI.
|
|
91
|
+
- **GroupChatWechatHuman** (`agent/group_chat_wechat_human.py`): Wraps MqttHumanAgent, bridges WeChat iLink to MQTT group chat. Polls WeChat for incoming messages, forwards group responses back via WeChat.
|
|
92
|
+
|
|
93
|
+
### Transport
|
|
94
|
+
|
|
95
|
+
`TransportRouter(inproc, remote)` — holds exactly two transports. Routes locally registered agents to in-process, unknown agents to remote (HTTP or MQTT, never both). Group chat uses an independent `GroupChatTransport` connection to keep the MqttTransport sole-consumer design intact.
|
|
96
|
+
|
|
97
|
+
### Tool System
|
|
98
|
+
|
|
99
|
+
Five tool categories:
|
|
100
|
+
|
|
101
|
+
| Category | Location | Callable | Loading |
|
|
102
|
+
|----------|----------|----------|---------|
|
|
103
|
+
| System | `qd_evolve/tools/` | Yes | Auto-discovered, schema on demand |
|
|
104
|
+
| A2A | `qd_evolve/agent/a2a_tools.py` | Yes | Registered when A2A enabled |
|
|
105
|
+
| Func | `tools/func/` | Yes | Hot-loadable `.py` files |
|
|
106
|
+
| Skills | `tools/skills/` | No | SKILL.md, injected into prompt |
|
|
107
|
+
| CLI | `tools/cli/` | No | YAML definitions, via `run_shell` |
|
|
108
|
+
|
|
109
|
+
**On-demand loading**: tools start with name + description. Full schema loaded only when the model calls `load_func`/`load_skill`/`load_cli`. Tools move from unloaded to active for subsequent turns.
|
|
110
|
+
|
|
111
|
+
**Hot-loading**: install tools at runtime via `install_func`/`install_mcp`/`install_skill`. Staged in `.qd_evolve/staging/`, persisted via `register_*` to `config.json`.
|
|
112
|
+
|
|
113
|
+
**Bridge protocol**: `BridgeManager` auto-discovers bridge modules in `tools/bridge/_*.py`. Each bridge self-registers with discover/connect/disconnect functions. MCP bridge spawns subprocesses; OAT bridge imports packages in-process.
|
|
114
|
+
|
|
115
|
+
### Memory
|
|
116
|
+
|
|
117
|
+
SQLite + `sqlite-vec` with BGE-M3 embeddings. Two operations: `save` (insert + embed) and `recall` (embed query → cosine similarity → top-k). Auto-recall queries memory before each LLM call, injecting results into the system prompt. Deduplicated across turns via `RecalledMemoryRegistry`. Context compression truncates old Q/A pairs when tokens exceed threshold.
|
|
118
|
+
|
|
119
|
+
**Process capture**: `save()` accepts an optional `process` string recording each tool call in the iteration chain — name, parameters, and success/failure (tool results excluded). This enriches the `content` field for semantic recall without schema changes.
|
|
120
|
+
|
|
121
|
+
### Multi-Agent Communication
|
|
122
|
+
|
|
123
|
+
Two mechanisms: **direct tasking** (send task → lifecycle: submitted→working→completed/failed/canceled/input_required) and **group chat** (all agents subscribe to `/chat` topics, `@mentions` direct attention, no coordinator). Built on A2A v1.0: agent discovery, task management, SSE streaming, push notifications.
|
|
124
|
+
|
|
125
|
+
### Configuration
|
|
126
|
+
|
|
127
|
+
Single `config.json` file. No env vars. Each agent gets its own provider, model, memory DB, server binding, and toolbox state. Global defaults as fallback. `provider: "human"` identifies terminal human agents; `provider: "wechat-human"` identifies WeChat iLink bridge human agents. WeChat human agents persist their session token in the `wechat_session` field.
|
|
128
|
+
|
|
129
|
+
### Templates
|
|
130
|
+
|
|
131
|
+
Jinja2 system prompts with two-tier fallback: `templates/` (user) overrides `_templates/` (builtin). Mode-specific templates (single-agent, A2A, MQTT, group chat), each including a shared `_system_tail.j2`.
|
|
132
|
+
|
|
133
|
+
### Heartbeat
|
|
134
|
+
|
|
135
|
+
`asyncio.Event.wait(timeout)` — only fires on genuine idle. `touch_heartbeat()` resets the timer on activity. LLM response `"."` means stay silent. Mode-specific templates. `0` disables.
|
|
136
|
+
|
|
137
|
+
## Module Map
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
qd_evolve/
|
|
141
|
+
├── __main__.py # CLI entry (typer), subcommand registration
|
|
142
|
+
├── chat_cli.py # Single-agent chat loop
|
|
143
|
+
├── a2a_cli.py # A2A multi-agent chat + serve
|
|
144
|
+
├── mqtt_cli.py # MQTT multi-agent chat + serve
|
|
145
|
+
├── gchat_cli.py # Group chat
|
|
146
|
+
├── cli_utils.py # ReplayInput, TeeWriter, AGENT_COLORS
|
|
147
|
+
├── skills.py # SkillRegistry
|
|
148
|
+
├── cli_tools.py # CLIRegistry
|
|
149
|
+
├── toolbox_tui.py # Textual TUI for toolbox management
|
|
150
|
+
├── memory_tui.py # Textual TUI for memory browsing and search
|
|
151
|
+
├── core/
|
|
152
|
+
│ ├── config.py # Settings, AgentEntry, ServerConfig, MqttConfig (pydantic)
|
|
153
|
+
│ ├── providers.py # Provider + ProviderRegistry
|
|
154
|
+
│ ├── registry.py # ToolRegistry + ToolDef (on-demand loading)
|
|
155
|
+
│ ├── memory.py # MemoryStore (SQLite + sqlite-vec), RecalledMemoryRegistry
|
|
156
|
+
│ ├── prompts.py # PromptTemplateManager (Jinja2, two-tier fallback)
|
|
157
|
+
│ ├── toolbox.py # Per-agent tool state (enabled/preload/disabled)
|
|
158
|
+
│ └── logger.py # SharedFileHandler
|
|
159
|
+
├── agent/
|
|
160
|
+
│ ├── agent.py # Agent — LLM loop, tool exec, memory, compression, heartbeat
|
|
161
|
+
│ ├── a2a_agent.py # A2AAgent — wraps Agent, adds card + task_store + event fan-out
|
|
162
|
+
│ ├── mqtt_agent.py # MqttAgent — wraps A2AAgent, MQTT v5 lifecycle
|
|
163
|
+
│ ├── group_chat_agent.py # GroupChatAgent — wraps MqttAgent, group chat behavior
|
|
164
|
+
│ ├── group_chat_human.py # GroupChatHuman — wraps MqttHumanAgent, terminal group UI
|
|
165
|
+
│ ├── group_chat_wechat_human.py # GroupChatWechatHuman — WeChat iLink ↔ MQTT bridge
|
|
166
|
+
│ ├── group_chat_transport.py # GroupChatTransport — independent MQTT for /chat topics
|
|
167
|
+
│ ├── human_agent.py # HumanAgent — implements AgentProtocol directly, no LLM
|
|
168
|
+
│ ├── mqtt_human_agent.py # MQTT wrapper for HumanAgent
|
|
169
|
+
│ ├── server.py # A2AServer — aiohttp JSON-RPC + SSE endpoint
|
|
170
|
+
│ ├── transport.py # InprocTransport, HttpTransport, TransportRouter
|
|
171
|
+
│ ├── mqtt_transport.py # MqttTransport — sole-consumer MQTT v5
|
|
172
|
+
│ ├── registry.py # AgentRegistry — singleton, local agent lookup
|
|
173
|
+
│ ├── loader.py # init_process + create_agent factory
|
|
174
|
+
│ ├── a2a_tools.py # delegate_to, send_task, get_task, cancel_task
|
|
175
|
+
│ ├── protocol.py # AgentProtocol ABC (card, task_store, run, subscribe_events)
|
|
176
|
+
│ └── a2a.py # A2A v1.0 data models (Task, Message, AgentCard, etc.)
|
|
177
|
+
├── tools/
|
|
178
|
+
│ ├── __init__.py # Re-exports ToolRegistry
|
|
179
|
+
│ ├── tool_loader.py # load_func — on-demand func tool schema loading
|
|
180
|
+
│ ├── skill_loader.py # load_skill — on-demand skill content loading
|
|
181
|
+
│ ├── cli_loader.py # load_cli — on-demand CLI tool detail loading
|
|
182
|
+
│ ├── install_func.py # install_func — hot-load func tool
|
|
183
|
+
│ ├── install_mcp.py # install_mcp — hot-load MCP server
|
|
184
|
+
│ ├── install_skill.py # install_skill — hot-load skill
|
|
185
|
+
│ ├── register_func.py # register_func — persist func tool
|
|
186
|
+
│ ├── register_mcp.py # register_mcp — persist MCP server
|
|
187
|
+
│ ├── register_skill.py # register_skill — persist skill
|
|
188
|
+
│ ├── recall_memory.py # recall_memory — search memory
|
|
189
|
+
│ └── staging.py # .qd_evolve/staging/ directory helpers
|
|
190
|
+
├── bridge/
|
|
191
|
+
│ ├── __init__.py
|
|
192
|
+
│ └── wechat_clawbot_client.py # WeChatClawbotClient — iLink ClawBot protocol
|
|
193
|
+
├── utils/
|
|
194
|
+
│ ├── adk_schema.py # Google ADK → OpenAI JSON Schema
|
|
195
|
+
│ └── adk_output.py # ADK output normalization
|
|
196
|
+
└── _templates/ # Builtin Jinja2 templates
|
|
197
|
+
├── default.j2 # Single-agent system prompt
|
|
198
|
+
├── a2a-default.j2 # A2A system prompt
|
|
199
|
+
├── mqtt-default.j2 # MQTT system prompt
|
|
200
|
+
├── group-default.j2 # Group chat system prompt
|
|
201
|
+
├── group-heartbeat.j2 # Group chat heartbeat
|
|
202
|
+
├── group-message.j2 # Group chat incoming message format
|
|
203
|
+
├── heartbeat.j2 # Single-agent heartbeat
|
|
204
|
+
├── a2a-heartbeat.j2 # A2A heartbeat
|
|
205
|
+
├── mqtt-heartbeat.j2 # MQTT heartbeat
|
|
206
|
+
└── _system_tail.j2 # Shared tail (included by all templates)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Class Hierarchy
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
AgentProtocol (Protocol)
|
|
213
|
+
├── A2AAgent — wraps Agent, adds A2A identity + event fan-out
|
|
214
|
+
│ └── MqttAgent — wraps A2AAgent, adds MQTT v5 lifecycle
|
|
215
|
+
│ └── GroupChatAgent — wraps MqttAgent, adds group chat behavior
|
|
216
|
+
└── HumanAgent — no LLM, no tools, no memory; async completion via webhook
|
|
217
|
+
└── MqttHumanAgent — MQTT wrapper for HumanAgent
|
|
218
|
+
|
|
219
|
+
Group chat wrappers (composition over MqttHumanAgent):
|
|
220
|
+
├── GroupChatHuman — terminal-based group chat for human agents
|
|
221
|
+
└── GroupChatWechatHuman — WeChat iLink ↔ MQTT bidirectional bridge
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Composition, not inheritance. Each wrapper delegates to the inner agent and adds exactly one concern.
|
|
225
|
+
|
|
226
|
+
## Model Layer (pydantic)
|
|
227
|
+
|
|
228
|
+
All config models live in `qd_evolve/core/config.py`:
|
|
229
|
+
|
|
230
|
+
| Model | Purpose |
|
|
231
|
+
|-------|---------|
|
|
232
|
+
| `Settings` | Root config. Providers, agents, memory, thresholds, stream, heartbeat. |
|
|
233
|
+
| `AgentEntry` | Per-agent: name, description, provider, model, memory_db, server, toolbox, mqtt. `is_human` is `provider == "human"`. |
|
|
234
|
+
| `ProviderConfig` | API key, base URL, api type (openai-completions/openai-response/anthropic), models. |
|
|
235
|
+
| `ModelConfig` | Context window, max_tokens, reasoning flag, cost tracking. |
|
|
236
|
+
| `ServerConfig` | host, port. |
|
|
237
|
+
| `MqttConfig` | Per-agent MQTT: username, password, keepalive, TLS paths. |
|
|
238
|
+
| `MqttBrokerConfig` | Broker host, port, will_delay_interval. |
|
|
239
|
+
| `ToolboxSection` | Per-agent tool state: five dicts mapping name→state. |
|
|
240
|
+
| `MCPServerConfig` | MCP server: command, args, env, type (stdio/sse/http/ws), url, headers, timeout. |
|
|
241
|
+
| `EmbeddingsBackend` | model_path, dim, backend (sentence-transformers/llama-cpp-python). |
|
|
242
|
+
| `MemorySearchConfig` | auto_recall, auto_recall_top_k, recall_memory_limit. |
|
|
243
|
+
|
|
244
|
+
Validation: `AgentsConfig._validate_ports` rejects duplicate ports at model init.
|
|
245
|
+
|
|
246
|
+
A2A data models in `qd_evolve/agent/a2a.py`: `AgentCard`, `Task`, `TaskStatus`, `TaskState` (enum), `Message`, `Part`, `StreamResponse`, `AgentCapabilities`, `AgentSkill`, `FileContent`, `AgentExtension`.
|
|
247
|
+
|
|
248
|
+
## Agent Loop (`Agent._run_inner`)
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
_run_inner(user_input, system, provider, model):
|
|
252
|
+
1. Resolve provider/model (arg → instance → config default)
|
|
253
|
+
2. Append user message to self.messages
|
|
254
|
+
3. Auto-recall: query memory, inject into system prompt
|
|
255
|
+
4. Loop:
|
|
256
|
+
a. Create API client (openai or anthropic)
|
|
257
|
+
b. Build tool definitions from registry (active + preload)
|
|
258
|
+
c. Call LLM (dispatch by api_type)
|
|
259
|
+
d. If text response → save to memory (with process capture), compress, return
|
|
260
|
+
e. If tool calls → record name/params/success via _record_tool_call(), execute via ToolRegistry.call(), append results, continue
|
|
261
|
+
f. If max_iterations exceeded → return error
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The entire loop is guarded by `threading.Lock` (`_run_lock`). Only one `run()` per agent at a time.
|
|
265
|
+
|
|
266
|
+
### API Dispatch
|
|
267
|
+
|
|
268
|
+
Three code paths in `Agent`:
|
|
269
|
+
|
|
270
|
+
- `_run_anthropic()` — `client.messages.create()` with Anthropic SDK. Tool use via `stop_reason == "tool_use"`. Content blocks contain `tool_use` items.
|
|
271
|
+
- `_run_openai_completion()` — `client.chat.completions.create()` with OpenAI SDK. Tool calls via `msg.tool_calls`. Supports streaming (`stream=True`) with reasoning content for reasoning models.
|
|
272
|
+
- `_run_openai_response()` — OpenAI Responses API. Separate code path for the newer API shape.
|
|
273
|
+
|
|
274
|
+
Each path recursively calls itself for tool turns, incrementing an `_iter` counter checked against `max_iterations`.
|
|
275
|
+
|
|
276
|
+
`api_type` is mapped from config's `api` field: `openai-completions` → `openai_completion`, `openai-response` → `openai_response`, `anthropic` → `anthropic`.
|
|
277
|
+
|
|
278
|
+
### Tool Execution
|
|
279
|
+
|
|
280
|
+
`ToolRegistry.call(name, **kwargs)` spawns a daemon thread with a `DEFAULT_TOOL_TIMEOUT` (60s) timeout. If the thread is still alive after timeout, returns an error string. Exceptions are caught and formatted into error messages. `ImportError` is re-raised (not caught) to surface missing dependencies.
|
|
281
|
+
|
|
282
|
+
## Initialization Flow
|
|
283
|
+
|
|
284
|
+
### Per-process (`init_process`)
|
|
285
|
+
|
|
286
|
+
Called once. Sets up module-level singletons:
|
|
287
|
+
|
|
288
|
+
1. `SkillRegistry` — scans `tools/skills/` for SKILL.md files
|
|
289
|
+
2. `CLIRegistry` — scans `tools/cli/` for YAML definitions
|
|
290
|
+
3. `BridgeManager.connect_all()` — auto-discovers bridge modules in `tools/bridge/_*.py`, calls each bridge's `discover()` then `connect()`
|
|
291
|
+
4. Injects registries into loader tools (`skill_loader`, `install_skill`, `cli_loader`)
|
|
292
|
+
|
|
293
|
+
### Per-agent (`create_agent`)
|
|
294
|
+
|
|
295
|
+
Called for each agent. Returns a fully initialized agent:
|
|
296
|
+
|
|
297
|
+
1. Lookup `AgentEntry` from config by name
|
|
298
|
+
2. **Human short-circuit**: if `entry.is_human`, create `HumanAgent` (or `MqttHumanAgent` if MQTT mode), return immediately
|
|
299
|
+
3. Resolve singletons: `ToolRegistry`, `ProviderRegistry`, `SkillRegistry`, `CLIRegistry`
|
|
300
|
+
4. Apply per-agent toolbox state (enabled/preload/disabled)
|
|
301
|
+
5. Register A2A tools if >1 agent and not group chat
|
|
302
|
+
6. Build system prompt via `PromptTemplateManager.render()` with template variable injection
|
|
303
|
+
7. Create `MemoryStore` if `memory_db` is configured
|
|
304
|
+
8. Create `Agent` instance
|
|
305
|
+
9. Resolve provider/model (agent-specific → global default)
|
|
306
|
+
10. Wrap with `A2AAgent` if multi-agent, then `MqttAgent` if MQTT mode
|
|
307
|
+
11. Return agent
|
|
308
|
+
|
|
309
|
+
### Template Resolution
|
|
310
|
+
|
|
311
|
+
`create_agent` chains template name lookups:
|
|
312
|
+
|
|
313
|
+
1. If group chat: try `group-{template}`
|
|
314
|
+
2. If MQTT: try `mqtt-{template}` → fallback to `a2a-{template}` if multi-agent
|
|
315
|
+
3. If A2A only: try `a2a-{template}`
|
|
316
|
+
4. GChat fallback: try `gchat-{template}`
|
|
317
|
+
5. Default: `{template}` (usually `default`)
|
|
318
|
+
|
|
319
|
+
`PromptTemplateManager` uses a `_CombinedLoader` that checks `templates/` first (user overrides), then `_templates/` (builtins). Jinja2 with `trim_blocks=True`, `lstrip_blocks=True`.
|
|
320
|
+
|
|
321
|
+
## Transport
|
|
322
|
+
|
|
323
|
+
### TransportRouter
|
|
324
|
+
|
|
325
|
+
`TransportRouter(inproc, remote)` — holds exactly two transports. Routes to `inproc` for locally registered agents, to `remote` for unknown agents. `remote` is either `HttpTransport` or `MqttTransport`, never both.
|
|
326
|
+
|
|
327
|
+
### InprocTransport
|
|
328
|
+
|
|
329
|
+
Direct call to `Agent.run()` via thread pool (`asyncio.to_thread`). For human agents, uses async path: `receive_task()` → returns `input_required` immediately.
|
|
330
|
+
|
|
331
|
+
### HttpTransport
|
|
332
|
+
|
|
333
|
+
A2A JSON-RPC over HTTP. `POST /` with JSON-RPC body. Connects to remote agent's `A2AServer`. Supports SSE streaming for `message/stream`.
|
|
334
|
+
|
|
335
|
+
### MqttTransport
|
|
336
|
+
|
|
337
|
+
Implements `A2ATransport` over MQTT v5. Key features:
|
|
338
|
+
|
|
339
|
+
- **Request-response correlation**: MQTT v5 Response Topic + Correlation Data. Caller sets response topic, callee publishes result there.
|
|
340
|
+
- **Discovery**: Retained `AgentCard` on `$a2a/v1/discovery/{name}`. LWT clears it on disconnect.
|
|
341
|
+
- **Sole consumer**: `_listen_all()` subscribes to `$a2a/v1/response/{self}/+` and `$a2a/v1/event/{self}`. Only one consumer per connection.
|
|
342
|
+
- **QoS**: Task requests at QoS 1, events at QoS 0, discovery at QoS 1.
|
|
343
|
+
|
|
344
|
+
Topic structure:
|
|
345
|
+
|
|
346
|
+
| Topic | QoS | Retained | Purpose |
|
|
347
|
+
|-------|-----|----------|---------|
|
|
348
|
+
| `$a2a/v1/discovery/{agent}` | 1 | Yes + LWT | AgentCard, online/offline |
|
|
349
|
+
| `$a2a/v1/request/{agent}` | 1 | No | JSON-RPC requests |
|
|
350
|
+
| `$a2a/v1/response/{agent}/{req_id}` | 1 | No | MQTT v5 Response Topic |
|
|
351
|
+
| `$a2a/v1/event/{agent}` | 0 | No | Streaming + push notifications |
|
|
352
|
+
| `$a2a/v1/group/{name}/chat` | 0 | No | Group chat messages |
|
|
353
|
+
|
|
354
|
+
## Tool System
|
|
355
|
+
|
|
356
|
+
### ToolRegistry (`qd_evolve/core/registry.py`)
|
|
357
|
+
|
|
358
|
+
Central registry of `ToolDef` objects. Each `ToolDef` has: `name`, `description`, `handler` (callable), `input_schema` (JSON Schema dict), `enabled` (bool).
|
|
359
|
+
|
|
360
|
+
`definitions(api_format, active_tools)` produces tool schemas in the target API format. Only returns schemas for tools whose names are in `active_tools` — this is the on-demand loading mechanism.
|
|
361
|
+
|
|
362
|
+
### On-Demand Loading
|
|
363
|
+
|
|
364
|
+
Three loader tools implement the pattern:
|
|
365
|
+
|
|
366
|
+
1. **Func tools** (`tool_loader.py`): `load_func(name)` — imports the module, extracts `get_input_schema()` + `run()`, registers full schema in ToolRegistry, adds name to `_active_tools`
|
|
367
|
+
2. **Skills** (`skill_loader.py`): `load_skill(name)` — reads SKILL.md content, injects into system prompt by appending to messages, adds to `_loaded_skill_names`
|
|
368
|
+
3. **CLI tools** (`cli_loader.py`): `load_cli(name)` — reads YAML, formats as system prompt injection, adds to `_loaded_cli_names`
|
|
369
|
+
|
|
370
|
+
### Bridge System (`tools/bridge/`)
|
|
371
|
+
|
|
372
|
+
Self-registering plugin architecture:
|
|
373
|
+
|
|
374
|
+
- `BridgeManager` scans `tools/bridge/_*.py` for bridge modules
|
|
375
|
+
- Each module calls `BridgeManager.register(name, discover, connect, disconnect)`
|
|
376
|
+
- Each bridge's `discover()` returns config objects; `connect()` creates `Bridge` instances and registers tools
|
|
377
|
+
|
|
378
|
+
**MCP bridge** (`_mcp.py`): Scans `tools/mcp/*.json` + `.qd_evolve/staging/mcp/*.json`. Spawns subprocess via `mcp` SDK, discovers tools via `list_tools`, registers in ToolRegistry. Supports stdio, SSE, StreamableHTTP, WebSocket transports.
|
|
379
|
+
|
|
380
|
+
**OAT bridge** (`_oat.py`): Reads `tools/bridge/oat.json`. Imports Python packages directly, wraps functions as ToolRegistry handlers. No subprocess overhead. Schema conversion via `adk_schema.py` (Google ADK → OpenAI JSON Schema) and output normalization via `adk_output.py`.
|
|
381
|
+
|
|
382
|
+
### Hot-Loading
|
|
383
|
+
|
|
384
|
+
Five install/register pairs:
|
|
385
|
+
|
|
386
|
+
| Install (staging) | Register (persist) | Target |
|
|
387
|
+
|-------------------|-------------------|--------|
|
|
388
|
+
| `install_func` | `register_func` | `tools/func/*.py` |
|
|
389
|
+
| `install_mcp` | `register_mcp` | `tools/mcp/*.json` |
|
|
390
|
+
| `install_skill` | `register_skill` | `tools/skills/*/SKILL.md` |
|
|
391
|
+
|
|
392
|
+
Install writes to `.qd_evolve/staging/`, register copies from staging to the target directory and updates `config.json` toolbox state. Both work at runtime without restart.
|
|
393
|
+
|
|
394
|
+
### Toolbox State
|
|
395
|
+
|
|
396
|
+
`qd_evolve/core/toolbox.py` manages per-agent tool state in `config.json`:
|
|
397
|
+
|
|
398
|
+
- **enabled**: Tool is callable, schema starts unloaded
|
|
399
|
+
- **preload**: Tool is callable, schema loaded at startup
|
|
400
|
+
- **disabled**: Tool is invisible to the agent
|
|
401
|
+
|
|
402
|
+
Five sections: `tools`, `mcp_servers`, `bridge`, `cli`, `skills`. Bridge uses binary enabled/disabled (no preload). Managed via `qd-evolve toolbox` (Textual TUI) or direct `config.json` editing.
|
|
403
|
+
|
|
404
|
+
## Memory
|
|
405
|
+
|
|
406
|
+
### MemoryStore (`qd_evolve/core/memory.py`)
|
|
407
|
+
|
|
408
|
+
SQLite + `sqlite-vec` extension. Two tables:
|
|
409
|
+
|
|
410
|
+
- `conversations`: session_id, user_msg, assistant_msg, content (combined), accessed_at, access_count
|
|
411
|
+
- Vector index on `content` via `sqlite-vec` with BGE-M3 embeddings
|
|
412
|
+
|
|
413
|
+
`save(user_msg, assistant_msg)`: Inserts row, creates vector embedding.
|
|
414
|
+
|
|
415
|
+
`recall(query, limit)`: Embeds query, cosine similarity search, returns top-k `MemoryEntry` objects.
|
|
416
|
+
|
|
417
|
+
`new_session()`: Generates new session_id.
|
|
418
|
+
|
|
419
|
+
### Auto-Recall
|
|
420
|
+
|
|
421
|
+
Before each LLM call, `Agent._auto_recall()` queries memory with the user input. Results are deduplicated via `RecalledMemoryRegistry` (tracks seen IDs across turns). Deduped entries are injected into the system prompt under a `## Relevant Past Conversations` section.
|
|
422
|
+
|
|
423
|
+
### Context Compression
|
|
424
|
+
|
|
425
|
+
`Agent._compress_messages()` fires when `last_input_tokens / context_window > compress_threshold` (default 0.7). Removes oldest user/assistant/tool triples from the front of `self.messages` until estimated tokens drop below `target_threshold * context_window` (default 0.5). Simple truncation, no summarization.
|
|
426
|
+
|
|
427
|
+
### Embeddings Backend
|
|
428
|
+
|
|
429
|
+
Two backends: `sentence-transformers` (BGE-M3 via `SentenceTransformer`) and `llama-cpp-python` (local GGUF models). Configured per-backend in `embeddings_backends` config section.
|
|
430
|
+
|
|
431
|
+
## Event System
|
|
432
|
+
|
|
433
|
+
### Agent Events
|
|
434
|
+
|
|
435
|
+
`Agent._on_event` callback fires on: iteration start, status update, print output, error, completion, heartbeat, heartbeat_silent. Event dict has `type` + relevant fields.
|
|
436
|
+
|
|
437
|
+
`A2AAgent._push_event()` fans out to all subscribers (list of `asyncio.Queue`). Subscribers get events via `subscribe_events() → Queue`.
|
|
438
|
+
|
|
439
|
+
### SSE Streaming
|
|
440
|
+
|
|
441
|
+
`A2AServer` converts events to SSE `StreamResponse` objects. `message/stream` returns an async generator of SSE events. Custom metadata includes iteration number, status, token counts, heartbeat.
|
|
442
|
+
|
|
443
|
+
### MQTT Event Publishing
|
|
444
|
+
|
|
445
|
+
`MqttAgent` runs an `_event_pusher_task` that drains the event queue and publishes each event to `$a2a/v1/event/{agent_name}` as JSON.
|
|
446
|
+
|
|
447
|
+
## Heartbeat
|
|
448
|
+
|
|
449
|
+
`Agent.start_heartbeat_loop()` creates an asyncio task that:
|
|
450
|
+
|
|
451
|
+
1. `await asyncio.wait_for(self._hb_event.wait(), timeout=idle_seconds)`
|
|
452
|
+
2. If event was set → activity occurred, reset, don't fire
|
|
453
|
+
3. If timeout → genuine idle, call `heartbeat_check()` via thread pool
|
|
454
|
+
|
|
455
|
+
`touch_heartbeat()` sets the event. Called on user input before each LLM call.
|
|
456
|
+
|
|
457
|
+
Heartbeat response handling: if LLM returns `"."`, stays silent. Otherwise, the response is pushed as a heartbeat event. Configurable `heartbeat_idle_seconds` per agent; `0` disables.
|
|
458
|
+
|
|
459
|
+
## Group Chat
|
|
460
|
+
|
|
461
|
+
### GroupChatAgent
|
|
462
|
+
|
|
463
|
+
Wraps `MqttAgent`. Adds:
|
|
464
|
+
|
|
465
|
+
- **Group listener**: background asyncio task subscribes to `$a2a/v1/group/{member}/chat` for each member via `GroupChatTransport`
|
|
466
|
+
- **Deduplication**: `_seen_msg_ids` set, tracks message IDs to skip duplicates
|
|
467
|
+
- **Parallel processing**: incoming messages trigger `agent.run()` in thread pool; multiple messages can process concurrently (locking handled by Agent's `_run_lock`)
|
|
468
|
+
- **Response publishing**: formatted responses published to the group topic
|
|
469
|
+
- **Heartbeat override**: uses `group-heartbeat.j2` template; fires only when no recent group activity
|
|
470
|
+
|
|
471
|
+
### GroupChatTransport
|
|
472
|
+
|
|
473
|
+
Independent `aiomqtt.Client` connection for `/chat` topics. Keeps `MqttTransport`'s sole-consumer `_listen_all()` design intact by not adding subscriptions to the original client.
|
|
474
|
+
|
|
475
|
+
### Message Format
|
|
476
|
+
|
|
477
|
+
Incoming group messages formatted via `group-message.j2` template. `@mentions` parsed to determine if the message is addressed to this agent. `@all` matches everyone.
|
|
478
|
+
|
|
479
|
+
### GroupChatHuman
|
|
480
|
+
|
|
481
|
+
Wraps `MqttHumanAgent`. Provides an interactive terminal UI: incoming group messages appear above the prompt line, preserving partial input. Keyboard input is published to the group via `publish_human_input()`.
|
|
482
|
+
|
|
483
|
+
### GroupChatWechatHuman
|
|
484
|
+
|
|
485
|
+
Wraps `MqttHumanAgent`. Replaces terminal I/O with a WeChat iLink bidirectional bridge:
|
|
486
|
+
|
|
487
|
+
- **WeChat → MQTT**: Long-polls WeChat for new messages via `WechatClawbotClient.poll_updates()`, extracts text, parses `@mentions`, publishes to group MQTT topic
|
|
488
|
+
- **MQTT → WeChat**: Listens to all group messages, forwards to the WeChat user via `WechatClawbotClient.send_message()`, using the last `context_token` from the polled message
|
|
489
|
+
|
|
490
|
+
QR login on startup. Session token is persisted to `config.json` (`wechat_session` field) for reuse across restarts (valid for ~23 hours).
|
|
491
|
+
|
|
492
|
+
### WechatClawbotClient (`qd_evolve/bridge/wechat_clawbot_client.py`)
|
|
493
|
+
|
|
494
|
+
Standalone async client for the WeChat iLink ClawBot protocol. Extracted from SiverKing/weixin-ClawBot-API (MIT License). Handles: QR code login flow (terminal rendering), session persistence (`get_session_dict` / `try_restore_session`), message polling (`/ilink/bot/getupdates`), message sending (`/ilink/bot/sendmessage`), typing indicators. No dependencies on external WeChat libraries — uses only `aiohttp` + stdlib.
|
|
495
|
+
|
|
496
|
+
## CLI Layer
|
|
497
|
+
|
|
498
|
+
### Entry Point
|
|
499
|
+
|
|
500
|
+
`qd_evolve/__main__.py` registers typer subcommands: default (chat), `a2a`, `mqtt`, `gchat`, `toolbox`, `memory`.
|
|
501
|
+
|
|
502
|
+
### Chat Loop Pattern (all CLIs)
|
|
503
|
+
|
|
504
|
+
```
|
|
505
|
+
while True:
|
|
506
|
+
read user input (prompt_toolkit / replay)
|
|
507
|
+
if slash command → handle locally
|
|
508
|
+
if EOF / /quit → break
|
|
509
|
+
touch heartbeat
|
|
510
|
+
agent.run(input) → display response
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Slash Commands
|
|
514
|
+
|
|
515
|
+
Parsed in CLI layer, never sent to LLM. Each command is a string match before `agent.run()`. Commands: `/models`, `/agents`, `/tools`, `/skills`, `/cli`, `/status`, `/memory`, `/recall`, `/compress`, `/load`, `/reset`, `/clear`, `/help`, `/quit`.
|
|
516
|
+
|
|
517
|
+
### Replay Mode
|
|
518
|
+
|
|
519
|
+
`ReplayInput` reads lines from a file, feeds them as user input. `TeeWriter` captures output to a file. Used for automated testing.
|
|
520
|
+
|
|
521
|
+
## Dependencies
|
|
522
|
+
|
|
523
|
+
Core runtime: `anthropic`, `openai`, `pydantic`, `typer`, `rich`, `jinja2`, `pyyaml`, `sqlite-vec`, `prompt-toolkit`, `sentence-transformers` (or `llama-cpp-python`), `numpy`, `aiohttp`, `aiomqtt`, `mcp`, `textual`, `basic-open-agent-tools`, `coding-open-agent-tools`.
|
|
524
|
+
|
|
525
|
+
Bridge extras: `defusedxml` (coding-open-agent-tools dep), `tomlkit`, `markdown-pdf`.
|
|
526
|
+
|
|
527
|
+
Test: `pytest`, `pytest-asyncio`, `pytest-aiohttp`, `pytest-cov`, `pytest-mock`, `pytest-timeout`, `pytest-xdist`, `aioresponses`.
|
|
528
|
+
|
|
529
|
+
Python ≥ 3.13 required.
|
|
530
|
+
|
|
531
|
+
## Key Patterns
|
|
532
|
+
|
|
533
|
+
### Singleton via Module-Level Variable
|
|
534
|
+
|
|
535
|
+
Used by: `AgentRegistry`, `SkillRegistry`, `CLIRegistry`, `BridgeManager`, A2A `_transport`/`_task_store`.
|
|
536
|
+
|
|
537
|
+
Pattern: `_instance: T | None = None` at module level, `get_*()` returns it or raises, `set_*()` injects it. Avoids DI framework overhead.
|
|
538
|
+
|
|
539
|
+
### Callback Injection
|
|
540
|
+
|
|
541
|
+
Agent has three callbacks: `_on_status` (status bar), `_on_print` (output), `_on_event` (structured events). Set via setter methods. A2AAgent hooks `_on_event` to fan out to subscribers. CLIs set callbacks after agent creation.
|
|
542
|
+
|
|
543
|
+
### Tool Timeout via Daemon Thread
|
|
544
|
+
|
|
545
|
+
`ToolRegistry.call()` spawns a `threading.Thread(daemon=True)`, joins with timeout. If thread is alive after timeout, returns error. Avoids blocking the agent loop on hung tools.
|
|
546
|
+
|
|
547
|
+
### Lazy Import at Call Site
|
|
548
|
+
|
|
549
|
+
Common pattern: imports inside function bodies rather than at module top. Avoids circular imports between `agent/` and `core/`. Seen in: `InprocTransport._get_registry()`, `Agent._create_memory()`, `create_agent()`.
|
|
550
|
+
|
|
551
|
+
### Pydantic for All Config
|
|
552
|
+
|
|
553
|
+
Every config structure is a `pydantic.BaseModel`. No `dict.get()` with fallbacks — fields have explicit defaults in the model definition. Validation at model init catches config errors early.
|
|
554
|
+
|
|
555
|
+
### Jinja2 Two-Tier Templates
|
|
556
|
+
|
|
557
|
+
`PromptTemplateManager` checks `templates/` (user) first, falls back to `_templates/` (builtin). Templates are `.j2` files. Render context includes agent metadata, tool summaries, OS info, available agents, topology.
|
qd_evolve-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 juzcn
|
|
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.
|