GenericAgent:极简架构下的自我进化 Agent 框架
GenericAgent 是一个极简、可自我进化的自主 Agent 框架,核心代码仅约 3K 行。它通过 9 个原子工具和约百行的 Agent Loop,赋予任意 LLM 对本地计算机的系统级控制能力,覆盖浏览器、终端、文件系统、键鼠输入、屏幕视觉及移动设备(ADB)。
设计哲学:不预设技能,靠进化获得能力
GenericAgent 的核心理念是"不预设技能,靠进化获得能力"。每解决一个新任务,Agent 就将执行路径自动固化为 Skill,供后续直接调用。使用时间越长,沉淀的技能越多,形成一棵完全属于用户、从 3K 行种子代码生长出来的专属技能树
这种自我进化机制通过分层记忆系统实现,是 GenericAgent 区别于其他 Agent 框架的根本所在 。
分层记忆系统
GenericAgent 采用 5 层记忆架构来存储不同类型的信息:
| 层级 | 名称 | 存储位置 | 内容类型 |
|---|---|---|---|
| L0 | Meta Rules | memory/memory_management_sop.md |
记忆管理的元规则和公理 |
| L1 | Insight Index | memory/global_mem_insight.txt |
极简索引(≤30 行,<1K tokens) |
| L2 | Global Facts | memory/global_mem.txt |
环境事实(路径、凭证、配置) |
| L3 | Task Skills / SOPs | memory/*.md, memory/*.py |
任务 SOP 和工具脚本 |
| L4 | Session Archive | memory/L4_raw_sessions/ |
历史会话归档记录 |
记忆工作流程
记忆系统的工作流程分为记忆注入和记忆更新两个核心阶段:
🔧 固定逻辑] B --> C[读取 L1 索引] B --> D[读取固定结构] C --> E[注入系统提示词] D --> E E --> F[LLM 执行任务
🧠 大模型决策] end subgraph "任务执行阶段" F --> G{需要保存上下文?
🧠 大模型决策} G -->|是| H[update_working_checkpoint
🔧 固定逻辑] H --> I[写入 working.key_info] I --> J[每轮自动注入
_get_anchor_prompt] J --> F G -->|否| F end subgraph "任务完成阶段" F --> K{发现值得记忆的信息?
🧠 大模型决策} K -->|否| L[任务结束] K -->|是| M[start_long_term_update
🔧 固定逻辑] M --> N[读取 L0 SOP] N --> O[LLM 按规则分类信息
🧠 大模型决策] O --> P{信息类型
🧠 大模型决策} P -->|环境事实| Q[file_patch 更新 L2
🧠 大模型调用工具] P -->|任务经验| R[生成 SOP 存入 L3
🧠 大模型调用工具] Q --> S[触发 L1 同步
🧠 大模型执行] R --> S S --> T[更新 L1 索引
🧠 大模型执行] T --> L end
核心特点
分层记忆系统有几个特别之处:
-
严格的行动验证原则:只有经过工具执行验证的信息才能存储,这是 L0 元规则的核心公理:"No Execution, No Memory"
-
L1 极简索引:L1 层被严格限制在 30 行以内、< 1K tokens,确保每次任务开始时注入系统提示词而不占用过多上下文
-
单向同步机制:L2/L3 的变更会自动触发 L1 的同步更新,确保索引始终反映可用能力
-
禁止易失状态:严格禁止存储时间戳、PID、Session ID 等易失信息,确保记忆的长期有效性
提示词驱动的记忆更新
自我进化机制主要通过提示词注入和大模型自主决策实现。系统提示词在每次任务开始时构建,包含三层内容:
python
def get_system_prompt():
with open(os.path.join(script_dir, f'assets/sys_prompt{lang_suffix}.txt'), 'r', encoding='utf-8') as f: prompt = f.read()
prompt += f"\nToday: {time.strftime('%Y-%m-%d %a')}\n"
prompt += get_global_memory() # 注入全局记忆
return prompt
get_global_memory() 将 L1 索引注入系统提示词 。当 Agent 发现值得长期记忆的信息时,调用 start_long_term_update 工具,该工具返回详细的 next_prompt,指导 LLM 按照规则分类和存储信息。
这种设计的好处是灵活性和可控性:记忆更新完全由 LLM 理解和执行,而不是硬编码的规则。
浏览器注入实现
GenericAgent 通过 Chrome 扩展 + Python 服务器的 CDP(Chrome DevTools Protocol)桥接架构实现浏览器注入,核心是 tmwd_cdp_bridge 扩展和 TMWebDriver 服务器。
核心架构
页面注入"] BG["background.js
CDP 控制器"] end subgraph "Python 进程" TMWD["TMWebDriver 服务器
WebSocket/HTTP"] GA["GenericAgent
web_scan/web_execute_js"] end GA -->|工具调用| TMWD TMWD <-->|WebSocket/HTTP| BG CONTENT <-->|chrome.runtime.sendMessage| BG BG <-->|chrome.debugger| 浏览器标签页
关键特性
-
CSP 移除 :扩展在安装时自动移除
Content-Security-Policy头,允许在安全网站上执行eval()和内联脚本 -
页面注入脚本 :
content.js注入到所有网页,负责移除页面上的 CSP meta 标签、显示连接状态指示器、监听 DOM 变化并捕获 TID 命令 -
CDP 命令执行 :
background.js通过chrome.debuggerAPI 执行 CDP 命令,支持批量执行、结果引用和获取 cookies(包括 HttpOnly 和 Partitioned) -
保留登录态:通过直接控制用户浏览器并访问所有 cookies,完全保留用户状态,无需每次重新登录。
9 个原子工具
GenericAgent 提供了 9 个原子工具,赋予 LLM 对本地计算机的系统级控制能力 。这些工具定义在 assets/tools_schema.json 中 :
| 类别 | 工具名称 | 数量 |
|---|---|---|
| 代码执行 | code_run |
1 |
| 文件操作 | file_read, file_patch, file_write |
3 |
| 浏览器控制 | web_scan, web_execute_js |
2 |
| 记忆管理 | update_working_checkpoint, start_long_term_update |
2 |
| 人机交互 | ask_user |
1 |
其中专门用于浏览器的工具只有 2 个(web_scan 和 web_execute_js),其他工具覆盖代码执行、文件操作、记忆管理等领域,共同构成完整的系统控制能力。
Token 优化策略
GenericAgent 通过严格的 Token 优化机制,将实际发送给 LLM 的上下文控制在 30K 字符左右。代码中 context_win 的默认配置是 24K-28K 字符(不是 tokens),用作历史裁剪的阈值 。
压缩机制
-
历史压缩 :
compress_history_tags函数压缩历史消息中的标签内容(<thinking>,<tool_use>,<tool_result>),每 5 轮执行一次 -
历史裁剪 :
trim_messages_history在上下文超过阈值时裁剪早期消息,代码注释明确指出 "trim breaks cache, so compress more btw" -
HTML 优化 :
optimize_html_for_tokens对网页内容进行激进优化,移除所有style属性、截断长 URL、只保留必要属性 -
工具描述简化:工具描述不变时使用简化提示词,减少重复发送
-
工作记忆限制 :
update_working_checkpoint的key_info限制在 200 tokens 以内 -
历史折叠 :
_get_anchor_prompt只保留最近 30 条历史记录,早期历史被折叠为摘要
设计权衡
这种极致省 Token 的设计会降低模型缓存命中率,因为动态裁剪和压缩会破坏 prompt cache 的连续性。这是一个明确的成本 vs 缓存的权衡:GenericAgent 选择用更高的缓存成本换取更低的 token 消耗,因为分层记忆系统可以弥补小上下文的不足。
任务执行机制
在 GenericAgent 中,"任务"(task)不等于用户的一次简单对话输入,而是一个完整的执行单元,可能包含多轮 LLM 交互和工具调用 。
任务 vs 对话
| 维度 | 用户对话输入 | 任务(Task) |
|---|---|---|
| 粒度 | 单次消息 | 完整执行单元 |
| 轮数 | 1 轮 | 多轮(默认最多 40 轮) |
| 工具调用 | 0 或 1 次 | 多次(可能数十次) |
| 持续时间 | 秒级 | 分钟级甚至更长 |
| 状态管理 | 无状态 | 有状态(history_info, working memory) |
Agent 循环机制
用户只输入一次,之后的所有轮次都是 agent_runner_loop 的自动执行,不需要用户再次交互 。例如,用户输入"帮我读取微信消息",这个任务可能包含:
- Turn 1:LLM 决定安装依赖 → 调用
code_run - Turn 2:LLM 决定逆向数据库 → 调用
code_run - Turn 3:LLM 决定编写读取脚本 → 调用
file_write - Turn 4:LLM 决定测试脚本 → 调用
code_run - Turn 5:LLM 决定保存为 Skill → 调用
start_long_term_update - Turn 6:任务完成,返回结果
整个过程是一个任务,但包含 6 轮对话,全部由 agent 内部循环调用完成。
安全机制
GenericAgent 通过多层安全机制来保证安全性,主要体现在记忆治理规则、操作约束、访问控制和失败处理策略四个方面。
记忆治理规则(L0 宪法)
L0 层的 [CONSTITUTION] 定义了核心安全约束 :
markdown
[CONSTITUTION]
1. 改自身源码先请示;./内可自主实验,允许装包和portable工具
2. 决策前查记忆,有SOP/utils必用;多次失败回看SOP;未查证不断言
3. 分步执行,控制粒度,限制失败半径;3次失败请求干预
4. 密钥文件仅引用,不读取/移动
5. 写任何记忆前读META-SOP核验,memory下文件只能patch修改(除非新建)
操作层面安全规则
[RULES] 部分定义了操作层面的安全约束:
- 编码安全 :禁用 PowerShell cat/type,必须用
file_read;修改前必须先读取 - 进程安全:禁止无条件杀 python(会杀自己),必须精确 PID
- 窗口安全 :GUI 状态优先用
win32gui枚举标题 - Web JS 安全:输入用原生 setter+事件链,点击前检查 disabled
系统提示词安全原则
系统提示词中定义了行动原则 :
- 探测优先:失败时先充分获取信息(日志/状态/上下文),关键信息存入工作记忆,再决定重试或换方案。不可逆操作先询问用户
- 失败升级:1次→读错误理解原因,2次→探测环境状态,3次→深度分析后换方案或问用户。禁止无新信息的重复操作
工具执行安全限制
| 工具 | 安全限制 |
|---|---|
code_run |
有超时限制(默认 60 秒) |
file_patch |
要求精确 |
Citations
File: README.md (L18-18)
markdown
**GenericAgent** is a minimal, self-evolving autonomous agent framework. Its core is just **~3K lines of code**. Through **9 atomic tools + a ~100-line Agent Loop**, it grants any LLM system-level control over a local computer --- covering browser, terminal, filesystem, keyboard/mouse input, screen vision, and mobile devices (ADB).
File: README.md (L20-22)
markdown
Its design philosophy: **don't preload skills --- evolve them.**
Every time GenericAgent solves a new task, it automatically crystallizes the execution path into an skill for direct reuse later. The longer you use it, the more skills accumulate --- forming a skill tree that belongs entirely to you, grown from 3K lines of seed code.
File: README.md (L34-36)
markdown
## 🧬 Self-Evolution Mechanism
This is what fundamentally distinguishes GenericAgent from every other agent framework.
File: README.md (L160-168)
markdown
1️⃣ **Layered Memory System**
> _Memory crystallizes throughout task execution, letting the agent build stable, efficient working patterns over time._
- **L0 --- Meta Rules**: Core behavioral rules and system constraints of the agent
- **L1 --- Insight Index**: Minimal memory index for fast routing and recall
- **L2 --- Global Facts**: Stable knowledge accumulated over long-term operation
- **L3 --- Task Skills / SOPs**: Reusable workflows for completing specific task types
- **L4 --- Session Archive**: Archived task records distilled from finished sessions for long-horizon recall
File: ga.py (L494-509)
python
def do_start_long_term_update(self, args, response):
'''Agent觉得当前任务完成后有重要信息需要记忆时调用此工具。'''
prompt = '''### [总结提炼经验] 既然你觉得当前任务有重要信息需要记忆,请提取最近一次任务中【事实验证成功且长期有效】的环境事实、用户偏好、重要步骤,更新记忆。
本工具是标记开启结算过程,若已在更新记忆过程或没有值得记忆的点,忽略本次调用。
**如果没有经验证的,未来能用上的信息,忽略本次调用!**
**只能提取行动验证成功的信息**:
- **环境事实**(路径/凭证/配置)→ `file_patch` 更新 L2,同步 L1
- **复杂任务经验**(关键坑点/前置条件/重要步骤)→ L3 精简 SOP(只记你被坑得多次重试的核心要点)
**禁止**:临时变量、具体推理过程、未验证信息、通用常识、你可以轻松复现的细节、只是做了但没有验证的信息
**操作**:严格遵循提供的L0的记忆更新SOP。先 `file_read` 看现有 → 判断类型 → 最小化更新 → 无新内容跳过,保证对记忆库最小局部修改。\n
''' + get_global_memory()
yield "[Info] Start distilling good memory for long-term storage.\n"
path = './memory/memory_management_sop.md'
if os.path.exists(path): result = '自动读取L0内容:\n' + file_read(path, show_linenos=False)
else: result = "Memory Management SOP not found. Do not update memory."
return StepOutcome(result, next_prompt=prompt)
File: ga.py (L525-537)
python
def _get_anchor_prompt(self, skip=False):
if skip: return "\n"
h = self.history_info; W = 30
earlier = f'<earlier_context>\n{self._fold_earlier(h[:-W])}\n</earlier_context>\n' if len(h) > W else ""
h_str = "\n".join(h[-W:])
prompt = f"\n### [WORKING MEMORY]\n{earlier}<history>\n{h_str}\n</history>"
prompt += f"\nCurrent turn: {self.current_turn}\n"
if self.working.get('key_info'): prompt += f"\n<key_info>{self.working.get('key_info')}</key_info>"
if self.working.get('related_sop'): prompt += f"\n有不清晰的地方请再次读取{self.working.get('related_sop')}"
if getattr(self.parent, 'verbose', False):
try: print(prompt)
except: pass
return prompt
File: ga.py (L569-580)
python
def get_global_memory():
prompt = "\n"
try:
suffix = '_en' if os.environ.get('GA_LANG', '') == 'en' else ''
with open(os.path.join(script_dir, 'memory/global_mem_insight.txt'), 'r', encoding='utf-8', errors='replace') as f: insight = f.read()
with open(os.path.join(script_dir, f'assets/insight_fixed_structure{suffix}.txt'), 'r', encoding='utf-8') as f: structure = f.read()
prompt += f'cwd = {os.path.join(script_dir, "temp")} (./)\n'
prompt += f"\n[Memory] (../memory)\n"
prompt += structure + '\n../memory/global_mem_insight.txt:\n'
prompt += insight + "\n"
except FileNotFoundError: pass
return prompt
File: assets/tmwd_cdp_bridge/background.js (L5-15)
javascript
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [9999],
addRules: [{
id: 9999, priority: 1,
action: { type: 'modifyHeaders', responseHeaders: [
{ header: 'content-security-policy', operation: 'remove' },
{ header: 'content-security-policy-report-only', operation: 'remove' }
]},
condition: { urlFilter: '*', resourceTypes: ['main_frame', 'sub_frame'] }
}]
});
File: assets/tmwd_cdp_bridge/background.js (L84-115)
javascript
async function handleBatch(msg, sender) {
const R = [];
let attached = null;
const resolve$N = (params) => JSON.parse(JSON.stringify(params || {}).replace(/"\$(\d+)\.([^"]+)"/g,
(_, i, path) => { let v = R[+i]; for (const k of path.split('.')) v = v[k]; return JSON.stringify(v); }));
try {
for (const c of msg.commands) {
if (c.tabId === undefined && msg.tabId !== undefined) c.tabId = msg.tabId;
if (c.cmd === 'cookies') {
R.push(await handleCookies(c, sender));
} else if (c.cmd === 'tabs') {
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
R.push({ ok: true, data: tabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId })) });
} else if (c.cmd === 'cdp') {
const tabId = c.tabId || msg.tabId || sender.tab?.id;
if (attached !== tabId) {
if (attached) { await chrome.debugger.detach({ tabId: attached }); attached = null; }
await chrome.debugger.attach({ tabId }, '1.3');
attached = tabId;
}
R.push(await chrome.debugger.sendCommand({ tabId }, c.method, resolve$N(c.params)));
} else {
R.push({ ok: false, error: 'unknown cmd: ' + c.cmd });
}
}
if (attached) await chrome.debugger.detach({ tabId: attached });
return { ok: true, results: R };
} catch (e) {
if (attached) try { await chrome.debugger.detach({ tabId: attached }); } catch (_) {}
return { ok: false, error: e.message, results: R };
}
}
File: assets/tmwd_cdp_bridge/content.js (L3-24)
javascript
// Remove meta CSP tags
document.querySelectorAll('meta[http-equiv="Content-Security-Policy"]').forEach(e => e.remove());
// Indicator badge at bottom-right (userscript style)
(function(){
if(window.self!==window.top)return;
const d=document.createElement('div');
d.id='ljq-ind';
d.innerText='ljq_driver: 已连接';
d.style.cssText='position:fixed;bottom:8px;right:8px;background:#4CAF50;color:white;padding:4px 7px;border-radius:4px;font-size:11px;font-weight:bold;z-index:99999;cursor:pointer;box-shadow:0 2px 4px rgba(0,0,0,0.2);opacity:0.5;';
d.addEventListener('click',()=>alert('会话活跃\nURL: '+location.href));
(document.body||document.documentElement).appendChild(d);
})();
new MutationObserver(muts => {
for (const m of muts) for (const n of m.addedNodes) {
if (n.id === TID || (n.querySelector && n.querySelector('#' + TID))) {
const el = n.id === TID ? n : n.querySelector('#' + TID);
handle(el);
}
}
}).observe(document.documentElement, { childList: true, subtree: true });
File: assets/tools_schema.json (L1-72)
json
[
{"type": "function", "function": {
"name": "code_run",
"description": "Code executor. Prefer python. Multi-call OK, use script param. Reply code block is executed if no script arg; prefer for single call to avoid escaping. No hardcoding bulk data",
"parameters": {"type": "object", "properties": {
"script": {"type": "string", "description": "[Mutually exclusive] NEVER use this param when use reply code block."},
"type": {"type": "string", "enum": ["python", "powershell"], "description": "Code type", "default": "python"},
"timeout": {"type": "integer", "description": "in seconds", "default": 60},
"cwd": {"type": "string", "description": "Working directory, defaults to cwd"},
"inline_eval": {"type": "boolean", "description": "DO NOT USE except explicitly specified."}}}
}},
{"type": "function", "function": {
"name": "file_read",
"description": "Read file. Read before modify for latest context and line numbers",
"parameters": {"type": "object", "properties": {
"path": {"type": "string", "description": "Relative or absolute"},
"start": {"type": "integer", "description": "Start line number (1-based)"},
"count": {"type": "integer", "description": "Number of lines to read", "default": 200},
"keyword": {"type": "string", "description": "[Optional] If provided, returns first match (case-insensitive) with context"},
"show_linenos": {"type": "boolean", "description": "Show line numbers", "default": true}}}
}},
{"type": "function", "function": {
"name": "file_patch",
"description": "Replace unique old_content with new_content. Exact match required (whitespace/indentation). On failure, file_read to recheck",
"parameters": {"type": "object", "properties": {
"path": {"type": "string", "description": "File path"},
"old_content": {"type": "string", "description": "Original text block to replace (must be unique)"},
"new_content": {"type": "string", "description": "New content. Supports {{file:path:startLine:endLine}} to ref file lines, auto-expanded"}}}
}},
{"type": "function", "function": {
"name": "file_write",
"description": "Create/overwrite/append files. HUGE edits ONLY. Content must in <file_content>...</file_content> in reply body BEFORE the file_write call (not args). Supports {{file:path:startLine:endLine}}, auto-expanded",
"parameters": {"type": "object", "properties": {
"path": {"type": "string", "description": "File path"},
"mode": {"type": "string", "enum": ["overwrite", "append", "prepend"], "description": "Write mode", "default": "overwrite"}}}
}},
{"type": "function", "function": {
"name": "web_scan",
"description": "Get simplified HTML and tab list. Removes hidden/floating/covered elements. Call after switching pages",
"parameters": {"type": "object", "properties": {
"tabs_only": {"type": "boolean", "description": "Show tab list only, no HTML"},
"switch_tab_id": {"type": "string", "description": "[Optional] Tab ID to switch to"},
"text_only": {"type": "boolean", "description": "Plain text only, no HTML"}}}
}},
{"type": "function", "function": {
"name": "web_execute_js",
"description": "Execute JS. Multi-call OK with different switch_tab_id. No guessing. Act accurately to reduce web_scan calls. Execute JS in ```javascript blocks if no script arg, prefer to avoid escaping",
"parameters": {"type": "object", "properties": {
"script": {"type": "string", "description": "[Mutually exclusive] JS code or script path. NEVER use this param when use reply code block"},
"save_to_file": {"type": "string", "description": "file path; **only** for long result"},
"no_monitor": {"type": "boolean", "description": "Skip page change monitoring, saves 2-3s. Only for reads, not for page actions"},
"switch_tab_id": {"type": "string", "description": "[Optional] Tab ID to switch to before executing"}}}
}},
{"type": "function", "function": {
"name": "update_working_checkpoint",
"description": "Short-term working notepad, auto-injected each turn to prevent info loss in long tasks. Call during early/mid stages, not at end. When: (1) after reading SOP, store user needs & key constraints (skip for simple 1-2 step tasks); (2) before subtask switch or context flush; (3) after repeated failures, re-read SOP and must store new findings; (4) on new task, update content, clear old progress but keep valid constraints.\n\nDon't call: simple tasks (1-2 steps), task completed (use long-term memory tool)",
"parameters": {"type": "object", "properties": {
"key_info": {"type": "string", "description": "Replaces current notepad (<200 tokens). Incremental update: review existing, keep valid, add/remove/modify. Store: pitfalls, user requirements, key params/findings, file paths, progress, next steps. Don't store: ephemeral info, obvious context, old task info when user switched tasks. Prefer over-updating over losing key info"},
"related_sop": {"type": "string", "description": "Related SOP names, tips for further re-read"}}}
}},
{"type": "function", "function": {
"name": "ask_user",
"description": "Interrupt task to ask user when needing decisions, extra info, or facing unresolvable blockers",
"parameters": {"type": "object", "properties": {
"question": {"type": "string", "description": "Question for the user"},
"candidates": {"type": "array", "items": {"type": "string"}, "description": "Optional quick-select choices for the user"}}}
}},
{"type": "function", "function": {
"name": "start_long_term_update",
"description": "Start distilling long-term memory. Call when discovering info worth remembering (env facts/user prefs/lessons learned). Skip if memory already updated or in autonomous flow. Must call when a task that took 15+ turns is completed",
"parameters": {"type": "object", "properties": {}}}
}
File: llmcore.py (L33-64)
python
def compress_history_tags(messages, keep_recent=10, max_len=800, force=False):
"""Compress <thinking>/<tool_use>/<tool_result> tags in older messages to save tokens."""
compress_history_tags._cd = getattr(compress_history_tags, '_cd', 0) + 1
if force: compress_history_tags._cd = 0
if compress_history_tags._cd % 5 != 0: return messages
_before = sum(len(json.dumps(m, ensure_ascii=False)) for m in messages)
_pats = {tag: re.compile(rf'(<{tag}>)([\s\S]*?)(</{tag}>)') for tag in ('thinking', 'think', 'tool_use', 'tool_result')}
_hist_pat = re.compile(r'<(history|key_info|earlier_context)>[\s\S]*?</\1>')
def _trunc_str(s): return s[:max_len//2] + '\n...[Truncated]...\n' + s[-max_len//2:] if isinstance(s, str) and len(s) > max_len else s
def _trunc(text):
text = _hist_pat.sub(lambda m: f'<{m.group(1)}>[...]</{m.group(1)}>', text)
for pat in _pats.values(): text = pat.sub(lambda m: m.group(1) + _trunc_str(m.group(2)) + m.group(3), text)
return text
for i, msg in enumerate(messages):
if i >= len(messages) - keep_recent: break
c = msg['content']
if isinstance(c, str): msg['content'] = _trunc(c)
elif isinstance(c, list):
for b in c:
if not isinstance(b, dict): continue
t = b.get('type')
if t == 'text' and isinstance(b.get('text'), str): b['text'] = _trunc(b['text'])
elif t == 'tool_result':
tc = b.get('content')
if isinstance(tc, str): b['content'] = _trunc_str(tc)
elif isinstance(tc, list):
for sub in tc:
if isinstance(sub, dict) and sub.get('type') == 'text': sub['text'] = _trunc_str(sub.get('text'))
elif t == 'tool_use' and isinstance(b.get('input'), dict):
for k, v in b['input'].items(): b['input'][k] = _trunc_str(v)
print(f"[Cut] {_before} -> {sum(len(json.dumps(m, ensure_ascii=False)) for m in messages)}")
return messages
File: llmcore.py (L90-102)
python
def trim_messages_history(history, context_win):
compress_history_tags(history)
cost = sum(len(json.dumps(m, ensure_ascii=False)) for m in history)
print(f'[Debug] Current context: {cost} chars, {len(history)} messages.')
if cost > context_win * 3:
compress_history_tags(history, keep_recent=4, force=True) # trim breaks cache, so compress more btw
target = context_win * 3 * 0.6
while len(history) > 5 and cost > target:
history.pop(0)
while history and history[0].get('role') != 'user': history.pop(0)
if history and history[0].get('role') == 'user': history[0] = _sanitize_leading_user_msg(history[0])
cost = sum(len(json.dumps(m, ensure_ascii=False)) for m in history)
print(f'[Debug] Trimmed context, current: {cost} chars, {len(history)} messages.')
File: llmcore.py (L514-514)
python
self.context_win = cfg.get('context_win', 28000)
File: llmcore.py (L771-775)
python
if self.auto_save_tokens and self.last_tools == tools_json:
tool_instruction = "\n### Tools: still active, **ready to call**. Protocol unchanged.\n" if _en else "\n### 工具库状态:持续有效(code_run/file_read等),**可正常调用**。调用协议沿用。\n"
else: self.total_cd_tokens = 0
self.last_tools = tools_json
return tool_instruction
File: agent_loop.py (L42-99)
python
def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, max_turns=40, verbose=True, initial_user_content=None):
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": initial_user_content if initial_user_content is not None else user_input}
]
turn = 0; handler.max_turns = max_turns
while turn < handler.max_turns:
turn += 1; turnstr = f'LLM Running (Turn {turn}) ...'
if handler.parent.task_dir: turnstr = f'Turn {turn} ...'
if verbose: turnstr = f'**{turnstr}**'
yield f"\n\n{turnstr}\n\n"
if turn%10 == 0: client.last_tools = '' # 每10轮重置一次工具描述,避免上下文过大导致的模型性能下降
response_gen = client.chat(messages=messages, tools=tools_schema)
if verbose:
response = yield from response_gen
yield '\n\n'
else:
response = exhaust(response_gen)
cleaned = _clean_content(response.content)
if cleaned: yield cleaned + '\n'
if not response.tool_calls: tool_calls = [{'tool_name': 'no_tool', 'args': {}}]
else: tool_calls = [{'tool_name': tc.function.name, 'args': json.loads(tc.function.arguments), 'id': tc.id}
for tc in response.tool_calls]
tool_results = []; next_prompts = set(); exit_reason = {}
for ii, tc in enumerate(tool_calls):
tool_name, args, tid = tc['tool_name'], tc['args'], tc.get('id', '')
if tool_name == 'no_tool': pass
else:
if verbose: yield f"🛠️ Tool: `{tool_name}` 📥 args:\n````text\n{get_pretty_json(args)}\n````\n"
else: yield f"🛠️ {tool_name}({_compact_tool_args(tool_name, args)})\n\n\n"
handler.current_turn = turn
gen = handler.dispatch(tool_name, args, response, index=ii)
try:
v = next(gen)
def proxy(): yield v; return (yield from gen)
if verbose: yield '`````\n'
outcome = (yield from proxy()) if verbose else exhaust(proxy())
if verbose: yield '`````\n'
except StopIteration as e: outcome = e.value
if outcome.should_exit:
exit_reason = {'result': 'EXITED', 'data': outcome.data}; break
if not outcome.next_prompt:
exit_reason = {'result': 'CURRENT_TASK_DONE', 'data': outcome.data}; break
if outcome.next_prompt.startswith('未知工具'): client.last_tools = ''
if outcome.data is not None and tool_name != 'no_tool':
datastr = json.dumps(outcome.data, ensure_ascii=False, default=json_default) if type(outcome.data) in [dict, list] else str(outcome.data)
tool_results.append({'tool_use_id': tid, 'content': datastr})
next_prompts.add(outcome.next_prompt)
if len(next_prompts) == 0 or exit_reason:
if len(handler._done_hooks) == 0 or exit_reason.get('result', '') == 'EXITED': break
next_prompts.add(handler._done_hooks.pop(0))
next_prompt = handler.turn_end_callback(response, tool_calls, tool_results, turn, '\n'.join(next_prompts), exit_reason)
messages = [{"role": "user", "content": next_prompt, "tool_results": tool_results}] # just new message, history is kept in *Session
if exit_reason: handler.turn_end_callback(response, tool_calls, tool_results, turn, '', exit_reason)
return exit_reason or {'result': 'MAX_TURNS_EXCEEDED'}
File: assets/insight_fixed_structure.txt (L4-9)
text
[CONSTITUTION]
1. 改自身源码先请示;./内可自主实验,允许装包和portable工具
2. 决策前查记忆,有SOP/utils必用;多次失败回看SOP;未查证不断言
3. 分步执行,控制粒度,限制失败半径;3次失败请求干预
4. 密钥文件仅引用,不读取/移动
5. 写任何记忆前读META-SOP核验,memory下文件只能patch修改(除非新建)