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:

  1. Agent Loop (agent_loop.py, ~125 lines) — The core agent_runner_loop function 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.

  2. Tool Handler (ga.py, ~585 lines) — GenericAgentHandler extends BaseHandler and implements 9 tool methods via a do_ 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 special do_no_tool handles cases where the LLM doesn’t call a tool.

  3. 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:

LayerFilePurposeSize Constraint
L0memory/memory_management_sop.mdCore axioms and update rulesFixed
L1memory/global_mem_insight.txtNavigation index, red-line rules≤30 lines
L2memory/global_mem.txtVerified environment factsGrows
L3memory/*.md, memory/*.pyTask-specific SOPs and skillsUnlimited
L4memory/L4_raw_sessions/Archived session transcriptsUnlimited

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_run tool 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 support
  • OpenAISession — OpenAI Chat Completions and Responses APIs
  • MixinSession — 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 MixinSession class (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_run combination means the agent grows capabilities organically without plugin infrastructure.
  • Token-efficient. The compressed history + summary-based history_info keeps 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_run executes arbitrary Python with no sandboxing. The agent has full system access by design. This is intentional but risky — a misinterpreted LLM response could rm -rf or worse.
  • Single-file architecture. llmcore.py at 1026 lines and ga.py at 585 lines with no module decomposition makes the codebase hard to navigate. No package structure, no __init__.py files.
  • 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 driver global in ga.py, mykeys in llmcore.py, and file-based inter-process communication (_stop, _keyinfo files) 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.py into 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_suffix mechanism exists but is inconsistently applied. A proper i18n approach would broaden adoption.