Overview
GenericAgent is a minimalist, self-evolving autonomous agent framework (~3K lines of core Python) that gives any LLM system-level control over a local computer. The project was analyzed at commit 9024af7. Its distinguishing design is a layered memory system that crystallizes task execution paths into reusable skills, growing capabilities over time rather than preloading them.
The codebase targets individual power users who want a personal agent that learns from experience. It supports multiple LLM backends (Claude, Gemini, Kimi, OpenAI-compatible models) and multiple frontends (Streamlit, Telegram, QQ, WeChat, Feishu, DingTalk, terminal UI).
Key Findings
Architecture: Layered Memory × Minimal Tools × Agent Loop
GenericAgent’s architecture has three pillars:
-
Agent Loop (
agent_loop.py, ~125 lines) — The coreagent_runner_loopfunction drives a turn-based conversation: send messages to the LLM, parse tool calls, dispatch to handler methods, collect results, and loop. It uses a generator-based streaming pattern throughout, allowing frontends to display output incrementally. -
Tool Handler (
ga.py, ~585 lines) —GenericAgentHandlerextendsBaseHandlerand implements 9 tool methods via ado_prefix convention (do_code_run,do_file_read,do_file_write,do_file_patch,do_web_scan,do_web_execute_js,do_ask_user,do_update_working_checkpoint,do_start_long_term_update). A specialdo_no_toolhandles cases where the LLM doesn’t call a tool. -
LLM Client Layer (
llmcore.py, ~1026 lines) — A multi-backend client supporting Anthropic’s Messages API, OpenAI’s Chat Completions and Responses APIs, with SSE streaming, automatic retries, prompt caching, and message format translation between Claude and OpenAI conventions.
The Spine: Data Flow
User Input → GenericAgent.put_task() → agent_runner_loop()
→ LLMClient.chat() → API call (streaming SSE)
→ Parse response → Extract tool_calls
→ Handler.dispatch(tool_name) → do_* method
→ StepOutcome (data + next_prompt)
→ turn_end_callback() → Update history_info
→ Loop until no next_prompt or should_exit
The turn_end_callback is critical — it summarizes each turn into history_info, injects safety warnings at turn boundaries, and manages plan-mode state. History is maintained as a flat list of summary strings ([USER]: ... / [Agent] ...) rather than raw message objects.
Layered Memory System
The memory architecture is the project’s most distinctive feature:
| Layer | File | Purpose | Size Constraint |
|---|---|---|---|
| L0 | memory/memory_management_sop.md | Core axioms and update rules | Fixed |
| L1 | memory/global_mem_insight.txt | Navigation index, red-line rules | ≤30 lines |
| L2 | memory/global_mem.txt | Verified environment facts | Grows |
| L3 | memory/*.md, memory/*.py | Task-specific SOPs and skills | Unlimited |
| L4 | memory/L4_raw_sessions/ | Archived session transcripts | Unlimited |
The "No Execution, No Memory" axiom (L0) is strictly enforced: only action-verified information may be written to L1/L2/L3. This prevents hallucination from poisoning the knowledge base.
Memory updates happen through start_long_term_update, which prompts the agent to distill verified facts into the appropriate layer. The agent reads the memory management SOP and performs targeted file_patch operations — never bulk overwrites.
Tool Design Philosophy
The 9 tools are deliberately atomic and few:
- Execution:
code_run(arbitrary Python/Shell via subprocess),file_read,file_write,file_patch - Browser:
web_scan(simplified HTML),web_execute_js(full JS control) - Human-in-loop:
ask_user - Memory:
update_working_checkpoint(short-term),start_long_term_update(long-term)
The
code_runtool effectively makes the agent Turing-complete at runtime — it can install packages, create new scripts, and even define new tools dynamically. This is the mechanism for self-evolution.
Non-Obvious Details
Browser control via CDP bridge. The TMWebDriver class runs a local WebSocket server (port 18765) and HTTP long-poll server (port 18766). A Chrome extension (assets/tmwd_cdp_bridge/) connects real browser tabs to the agent. This preserves login sessions — a significant advantage over headless browser approaches.
Generator-based tool dispatch. All do_* methods are generators that yield streaming status updates and return StepOutcome. The try_call_generator helper normalizes between generator and regular return values. This is elegant but creates subtle control flow — tool methods must use return (yield from ...) for nested generators.
History compression. compress_history_tags in llmcore.py periodically truncates <thinking>, <tool_use>, and <tool_result> blocks in older messages to keep context under the window limit (~28K chars default). When compression isn’t enough, it pops oldest messages.
Plan mode. The handler supports a plan mode where tasks are tracked via a markdown checklist. Completion is verified by counting [ ] items. A verification gate prevents premature completion claims.
Idle autonomous mode. reflect/autonomous.py triggers self-directed tasks after 30 minutes of user inactivity. The reflect system is extensible — any Python script with a check() function can be loaded.
Slash commands. The /session.key=value command allows runtime reconfiguration of LLM backend properties (model, temperature, etc.) via chat input.
Multi-Backend LLM Support
llmcore.py implements three session types:
ClaudeSession— Anthropic Messages API with prompt caching and thinking/extended thinking supportOpenAISession— OpenAI Chat Completions and Responses APIsMixinSession— Composes multiple backends for routing or fallback
The ToolClient / NativeToolClient wrappers translate between the agent loop’s chat(tools=) interface and the raw session APIs, handling tool call formatting differences between providers.
The
MixinSessionclass (referenced but not fully visible in the analyzed code) appears to enable model switching mid-conversation, but the exact routing logic is unclear.
Assessment
Strengths
- Genuinely minimal core. The agent loop is ~125 lines, the handler ~585 lines. This is refreshingly small compared to most agent frameworks.
- Self-evolution by design. The layered memory +
code_runcombination means the agent grows capabilities organically without plugin infrastructure. - Token-efficient. The compressed history + summary-based
history_infokeeps context windows under 30K, an order of magnitude less than competitors. - Real browser integration. CDP-based injection preserves sessions and works with arbitrary websites, unlike sandbox/headless approaches.
- Multi-frontend flexibility. Supports 7+ frontends (Streamlit, Telegram, QQ, WeChat, Feishu, WeCom, DingTalk, terminal, Qt) from the same core.
Concerns
- Security surface.
code_runexecutes arbitrary Python with no sandboxing. The agent has full system access by design. This is intentional but risky — a misinterpreted LLM response couldrm -rfor worse. - Single-file architecture.
llmcore.pyat 1026 lines andga.pyat 585 lines with no module decomposition makes the codebase hard to navigate. No package structure, no__init__.pyfiles. - Chinese-language comments and prompts. Most inline documentation, system prompts, and memory SOPs are in Chinese. This limits accessibility for non-Chinese-speaking developers, though English versions exist for some files.
- Global mutable state. The
driverglobal inga.py,mykeysinllmcore.py, and file-based inter-process communication (_stop,_keyinfofiles) create implicit coupling. - No test suite. There are no tests visible in the repository. The code relies entirely on manual testing and the agent’s own self-bootstrap verification.
Recommendations
- Sandbox
code_run. Even a basic Docker or namespace-based isolation would dramatically reduce risk. - Extract modules. Split
llmcore.pyinto separate files for session types, SSE parsing, and message format conversion. - Add a test harness. At minimum, unit tests for the tool dispatch, message format conversion, and history compression logic.
- Internationalize prompts. The
lang_suffixmechanism exists but is inconsistently applied. A proper i18n approach would broaden adoption.