diff --git a/README.md b/README.md index 8ab44455..2b42753d 100644 --- a/README.md +++ b/README.md @@ -351,9 +351,13 @@ Baselines across these dimensions include **Claude Code**, **OpenAI CodeX**, and ## 📅 Roadmap & News +- **2026-05-23** — 🆕 **TUI v3 released** (`frontends/tui_v3.py`). Block-based scrollback with proper resize reflow, per-terminal color profile for cross-terminal parity, and feature parity with v2. +- **2026-05-18** — 🆕 **Morphling mode**. Project-level skill absorption — extract goal + tests from any external repo, then decide per component: call, rewrite, or discard. See `memory/morphling_sop.md`. +- **2026-05-17** — 🆕 **Goal Hive mode**. Multi-worker cooperative Goal mode — BBS-coordinated master/workers running long-horizon objectives in parallel. See `memory/goal_hive_sop.md`. - **2026-05-15** — 🖥️ **Desktop GUI released**. One-line installs ship a ready-to-run desktop app (`frontends/GenericAgent.exe`). Developers launch via `python launch.pyw`. - **2026-05-14** — 🆕 **Conductor sub-agent orchestration**. Spawn, supervise, and auto-clean parallel sub-agents; first-class delegation primitives complementing `/btw` side-questions. - **2026-05-12** — 🆕 **TUI v2 released** (`frontends/tuiapp_v2.py`). Refined Textual frontend with image-paste folding, file paste, block-delete, Ctrl+C copy, history navigation, and `/llm` / `/export` / `/continue` pickers. +- **2026-05-08** — 🆕 **Goal mode** (`reflect/goal_mode.py`). Time-budget-driven self-driven loop — "keep optimizing X for N hours" with no premature delivery. - **2026-04-21** — 📄 [**Technical Report on arXiv**](https://arxiv.org/abs/2604.17091) — *GenericAgent: A Token-Efficient Self-Evolving LLM Agent via Contextual Information Density Maximization*. - **2026-04-11** — Introduced **L4 session archive memory** and scheduler cron integration. - **2026-03-23** — Personal WeChat supported as a bot frontend. @@ -388,6 +392,7 @@ Thanks to the **LinuxDo** community for the support! - [chilishark27/ga-manager](https://github.com/chilishark27/ga-manager) - [wangjc683/galley](https://github.com/wangjc683/galley) +- [FroStorM/A3Agent](https://github.com/FroStorM/A3Agent/tree/workbench) --- @@ -720,9 +725,13 @@ GenericAgent 通过 **分层记忆 × 最小工具集 × 自主执行循环** ## 📅 路线图与最新动态 +- **2026-05-23** — 🆕 **TUI v3 正式发布**(`frontends/tui_v3.py`)。基于块的滚屏回看 + 正确的 resize 重排,每终端独立配色保证跨终端一致,并与 v2 达成功能对齐。 +- **2026-05-18** — 🆕 **Morphling 模式**。项目级能力吞噬 —— 从任意外部仓库抽取目标与测例后,对每个核心组件分别决定调用、重写或舍弃。详见 `memory/morphling_sop.md`。 +- **2026-05-17** — 🆕 **Goal Hive 模式**。多 worker 协作版 Goal —— Master/Worker 通过 BBS 协同推进长程目标。详见 `memory/goal_hive_sop.md`。 - **2026-05-15** — 🖥️ **桌面 GUI 发布**。一键安装会自带可直接运行的桌面端(`frontends/GenericAgent.exe`),开发者也可用 `python launch.pyw` 启动。 - **2026-05-14** — 🆕 **Conductor 子 Agent 编排**。派发、监督、自动清理并行子 Agent;与 `/btw` 旁路子 Agent 互补,提供一等公民级的任务委派原语。 - **2026-05-12** — 🆕 **TUI v2 正式发布**(`frontends/tuiapp_v2.py`)。重做视觉风格的 Textual 前端,支持图片粘贴折叠、文件粘贴、块删除、Ctrl+C 复制、历史导航,以及 `/llm` / `/export` / `/continue` 选择器。 +- **2026-05-08** — 🆕 **Goal 模式**(`reflect/goal_mode.py`)。时间预算驱动的自驱循环 —— "持续优化 X N 小时",预算没到不准提前交付。 - **2026-04-21** — 📄 [**技术报告已发布至 arXiv**](https://arxiv.org/abs/2604.17091) — *GenericAgent: A Token-Efficient Self-Evolving LLM Agent via Contextual Information Density Maximization*。 - **2026-04-11** — 引入 **L4 会话归档记忆**,并接入 scheduler cron 调度。 - **2026-03-23** — 支持个人微信接入作为 Bot 前端。 @@ -757,7 +766,7 @@ GenericAgent 通过 **分层记忆 × 最小工具集 × 自主执行循环** - [chilishark27/ga-manager](https://github.com/chilishark27/ga-manager) - [wangjc683/galley](https://github.com/wangjc683/galley) -- https://github.com/FroStorM/A3Agent/tree/workbench +- [FroStorM/A3Agent](https://github.com/FroStorM/A3Agent/tree/workbench) --- diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 8d4525f1..eec10a58 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -48,9 +48,36 @@ python3 --version ### 下载项目 -1. 打开 [GitHub 仓库页面](https://github.com/lsdefine/GenericAgent) -2. 点绿色 **Code** 按钮 → **Download ZIP** -3. 解压到你喜欢的位置 +最方便的方式是 **一键安装**(自带隔离 Python 环境 + Git + 桌面端): + +**Windows PowerShell** + +```powershell +powershell -ExecutionPolicy Bypass -c "irm http://fudankw.cn:9000/files/ga_install.ps1 | iex" +``` + +**Linux / macOS** + +```bash +curl -fsSL http://fudankw.cn:9000/files/ga_install.sh | bash +``` + +或者手动 clone(开发者): + +```bash +git clone https://github.com/lsdefine/GenericAgent.git +cd GenericAgent +uv venv && uv pip install -e ".[ui]" +``` + +也可以走最朴素的 ZIP:[GitHub 仓库页面](https://github.com/lsdefine/GenericAgent) → 点绿色 **Code** → **Download ZIP** → 解压到喜欢的位置。 + +> 💡 **让 Claude / Codex 等 Agent 帮你装**:把下面这条 curl 丢给它,它会按官方指南替你完成安装: +> ```bash +> curl -fsSL https://raw.githubusercontent.com/lsdefine/GenericAgent/refs/heads/main/docs/installation_zh.md +> ``` +> +> 📖 平台差异、排障、升级流程见 [`docs/installation_zh.md`](installation_zh.md)。 ### 创建配置文件 @@ -62,62 +89,55 @@ python3 --version ### 配置示例 -**最常见的用法:** +**推荐首选:Claude 原生协议**: ```python -# 变量名含 'oai' → 走 OpenAI 兼容格式 (/chat/completions) -oai_config = { - 'apikey': 'sk-你的密钥', - 'apibase': 'http://你的API地址:端口', - 'model': '模型名称', +# 变量名同时含 'native' 和 'claude' → NativeClaudeSession(API 原生工具字段) +native_claude_config = { + 'name': 'claude', # /llms 显示名 & mixin 引用名 + 'apikey': 'sk-xxx', # sk-ant- 走 x-api-key;其它走 Bearer + 'apibase': 'https://api.anthropic.com', # 官方直连;反代渠道填对应地址 + 'model': 'claude-opus-4-7', # [1m] 后缀触发 1M 上下文 beta + # 'fake_cc_system_prompt': True, # CC switch / 反代渠道必须置 True } ``` -```python -# 变量名含 'claude'(不含 'native')→ 走 Claude 兼容格式 (/messages) -claude_config = { - 'apikey': 'sk-你的密钥', - 'apibase': 'http://你的API地址:端口', - 'model': 'claude-sonnet-4-20250514', -} -``` +**也支持:OpenAI 原生协议**: ```python -# MiniMax 使用 OpenAI 兼容格式,变量名含 'oai' 即可 -# 温度自动修正为 (0, 1],支持 M2.7 / M2.5 全系列,204K 上下文 -oai_minimax_config = { - 'apikey': 'eyJh...', - 'apibase': 'https://api.minimax.io/v1', - 'model': 'MiniMax-M2.7', +# 变量名同时含 'native' 和 'oai' → NativeOAISession +native_oai_config = { + 'name': 'gpt', # /llms 显示名 & mixin 引用名 + 'apikey': 'sk-xxx', + 'apibase': 'https://api.openai.com/v1', # 自动补 /v1/chat/completions + 'model': 'gpt-5.5', } ``` -**使用标准工具调用格式(适合较弱模型):** +**进阶:Mixin 故障转移**(多 session 自动切换,最稳的玩法): ```python -# 变量名同时含 'native' 和 'claude' → Claude 标准工具调用格式 -native_claude_config = { - 'apikey': 'sk-ant-你的密钥', - 'apibase': 'https://api.anthropic.com', - 'model': 'claude-sonnet-4-20250514', +# llm_nos 按优先级排列;首项失败按指数退避切下一项 +mixin_config = { + 'llm_nos': ['claude', 'gpt'], # 与上面 native_* 的 name 字段对应 + 'max_retries': 10, + 'base_delay': 0.5, } ``` -> 💡 还支持 `native_oai_config`(OpenAI 标准工具调用)、`sider_cookie`(Sider)等,详见 `mykey_template.py` 中的注释。 +> 💡 完整字段说明(`thinking_type` / `reasoning_effort` / `context_win` / `proxy` / Zhipu / MiniMax / Kimi / OpenRouter 等渠道示例)见 `mykey_template.py` 顶部注释。 ### 关键规则 -**变量命名决定接口格式**(不是模型名决定的): +**变量命名决定 Session 类型**(不是模型名决定的): -| 变量名包含 | 触发的 Session | 适用场景 | -|-----------|---------------|---------| -| `oai` | OpenAI 兼容 | 大多数 API 服务、OpenAI 官方 | -| `claude`(不含 `native`) | Claude 兼容 | Claude API 服务 | -| `native` + `claude` | Claude 标准工具调用 | 较弱模型推荐,工具调用更规范 | -| `native` + `oai` | OpenAI 标准工具调用 | 较弱模型推荐,工具调用更规范 | - -> 例:用 Claude 模型,但 API 服务提供的是 OpenAI 兼容接口 → 变量名用 `oai_xxx`。 -> 例:用 MiniMax 模型 → 变量名用 `oai_minimax_config`,MiniMax 走 OpenAI 兼容接口。 +| 变量名包含 | 触发的 Session | 工具协议 | 适用场景 | +|-----------|---------------|---------|---------| +| `native` + `claude` | NativeClaudeSession | API 原生 tool 字段 | **推荐首选** — Claude 原生协议 | +| `native` + `oai` | NativeOAISession | API 原生 tool 字段 | GPT/o 系列、OAI 兼容渠道 | +| `mixin` | MixinSession | 多 session 故障转移 | 最稳;要求被引用 session 全为 native | +| `claude`(不含 `native`) | ClaudeSession | 文本协议工具 | **deprecated**,后续版本可能移除 | +| `oai`(不含 `native`) | LLMSession | 文本协议工具 | **deprecated**,后续版本可能移除 | **`apibase` 填写规则**(会自动拼接端点路径): @@ -167,13 +187,16 @@ Agent 会自己读代码、找出需要的包、全部装好。 ### 升级到图形界面 -依赖装完后,就可以用 GUI 模式了: +依赖装完后,可以选择适合你的前端: -```bash -python3 launch.pyw -``` +| 前端 | 启动命令 | 说明 | +|------|---------|------| +| **桌面端** | 双击 `frontends/GenericAgent.exe`(Windows 一键安装自带) | 真原生窗口,零终端依赖 | +| **TUI v3** | `python frontends/tui_v3.py` | 基于块的滚屏回看、resize 重排、每终端独立配色,跨终端体验一致 | +| **TUI v2** | `python frontends/tuiapp_v2.py` | Textual 键盘驱动界面,图片粘贴折叠、`/llm`/`/export`/`/continue` 选择器 | +| **Streamlit / 悬浮窗** | `python launch.pyw` | 浏览器中打开的 Streamlit UI,附带桌面悬浮窗 | -启动后会出现一个桌面悬浮窗,直接在里面输入任务指令。 +> 💡 Windows 下推荐用 **Git Bash** 跑 TUI;PowerShell / cmd 对 Unicode 和键位支持较弱。仍异常时请直接告诉 Agent:「参考 Claude Code 在 Windows 终端的最佳配置帮我把 TUI 修一遍」。 ### 可选:让 Agent 帮你做的事 @@ -244,6 +267,10 @@ Agent 会自动配好。如果你电脑上没有 Git,它也会帮你下载 por | **Plan(规划)** | `查看你的代码,告诉我你的 plan 模式怎么启用` | | **SubAgent(子代理)** | `查看你的代码,告诉我你的 subagent 模式怎么启用` | | **自主探索** | `查看你的代码,告诉我你的自主探索模式怎么启用` | +| **Goal** | `查看你的代码,告诉我 goal 模式怎么启用` | +| **Goal Hive(多 worker 协作)** | `查看你的代码,告诉我 goal hive 模式怎么启用` | +| **Conductor(多 subagent 编排)** | `查看你的代码,告诉我 conductor 模式怎么启用` | +| **Morphling(吞噬外部项目)** | `查看你的代码,告诉我 morphling 模式怎么启用` | > 💡 这就是 GenericAgent 的核心设计理念:**代码即文档**。Agent 能读懂自己的源码,所以任何功能你都可以直接问它。 @@ -268,4 +295,4 @@ GenericAgent 不预设技能,而是**靠使用进化**。每完成一个新任 > Agent 会自动 pull 最新代码并解读 commit log,告诉你新增了什么能力。 -> 更多细节请参阅 [README.md](../README.md) 或 [详细版图文教程](https://my.feishu.cn/wiki/CGrDw0T76iNFuskmwxdcWrpinPb)。 \ No newline at end of file +> 更多细节请参阅 [README.md](../README.md) 或 [详细版图文教程](https://my.feishu.cn/wiki/CGrDw0T76iNFuskmwxdcWrpinPb)。 diff --git a/frontends/slash_cmds.py b/frontends/slash_cmds.py index 46415073..f6298562 100644 --- a/frontends/slash_cmds.py +++ b/frontends/slash_cmds.py @@ -343,18 +343,20 @@ def start_service(name: str) -> tuple[bool, str]: def _match_service(cmdline: list[str], svc: dict) -> bool: """Does this OS process belong to `svc`? Match on the trailing script arg (`reflect/foo.py` for reflect tasks, `frontends/bar.py` for apps), - which is invariant across `python` vs `pythonw` vs venv shims.""" + which is invariant across `python` vs `pythonw` vs venv shims. + + Reflect detection used to require BOTH `agentmain.py` AND the reflect + path in cmdline. That rejected tasks launched directly (`python + reflect/scheduler.py`) by launch.pyw, dev scripts, or by an earlier + TUI run that used a different launcher — they showed unticked in + /scheduler even when alive. Path-only match handles both styles; the + Python-process pre-filter in `running_services` keeps false positives + (greps, editors with the file open) from sneaking in.""" if not cmdline: return False rel = svc["name"] # 'reflect/foo.py' | 'frontends/bar.py' - if svc["kind"] == "reflect": - # agentmain.py --reflect reflect/foo.py - has_main = any("agentmain.py" in (a or "") for a in cmdline) - has_rel = any(rel.replace("/", os.sep) in (a or "") or rel in (a or "") - for a in cmdline) - return has_main and has_rel - # frontend: either `python frontends/foo.py` or `python -m streamlit run frontends/stapp.py …` - return any(rel.replace("/", os.sep) in (a or "") or rel in (a or "") + rel_norm = rel.replace("/", os.sep) + return any(rel_norm in (a or "") or rel in (a or "") for a in cmdline) @@ -503,7 +505,8 @@ def start_reflect_task(name: str) -> tuple[bool, str]: ("/goal", "[goal]", "进入 Goal 模式(需 condition 约束)"), ("/hive", "[target]", "进入 Hive 多 worker 协作模式"), ("/conductor", "[task]", "调用 frontends/conductor.py 多 subagent 编排"), - ("/scheduler", "", "多选启动 reflect 任务 / 查看 cron"), + ("/scheduler", "", "多选启动/停止 reflect 任务(cron 由 reflect/scheduler.py 驱动)"), + ("/resume", "", "列出最近会话并恢复其中一个(GA 端展开 prompt)"), ] diff --git a/frontends/tui_v3.py b/frontends/tui_v3.py index 3ef2f2b6..d63e2abe 100644 --- a/frontends/tui_v3.py +++ b/frontends/tui_v3.py @@ -102,6 +102,8 @@ "Tip: /scheduler is live — untick a running row to stop it; tick again to relaunch.", "Tip: Ctrl+S stashes your draft input — it's waiting for you next time you open a picker.", "Tip: /scheduler lists reflect tasks and starts them via `/scheduler start a,b,c`.", + "Tip: prefix `!` runs the rest as a host shell command — output is folded into LLM history.", + "Tip: /resume lists recent sessions you can pick from to restore prior context.", ], 'zh': [ "Tip: 按 / 唤起命令面板 —— 方向键选择,Enter 落入输入框。", @@ -119,6 +121,8 @@ "Tip: /update 是双分支 upstream 同步 —— 先 diff 预演,再分别快进。", "Tip: /scheduler 里再点一下已勾选的任务可以 stop —— 取消勾选 = 停止。", "Tip: Ctrl+S 把当前输入 stash 起来,下次 / 打开 picker 时还在。", + "Tip: 以 `!` 开头直接跑 shell —— 命令与输出都会进入 LLM 历史,agent 可以引用。", + "Tip: /resume 列出最近会话,可挑选一个恢复之前的上下文。", ], } @@ -146,6 +150,14 @@ 'help.export': ' /export [sub] Export last reply: clip / file [name] / all', 'help.stop': ' /stop Abort current task', 'help.language': ' /language [code] View / switch interface language', + 'help.update': ' /update [note] Preview upstream commits & diff, then pull (no commit)', + 'help.autorun': ' /autorun [seed] Enter autonomous-operation mode', + 'help.morphling': ' /morphling [target] Distill / absorb an external skill', + 'help.goal': ' /goal [goal] Enter Goal mode (asks for budget / worker cap)', + 'help.hive': ' /hive [target] Enter Hive multi-worker mode', + 'help.conductor': ' /conductor [task] Hand task to conductor.py for multi-subagent run', + 'help.scheduler': ' /scheduler Multi-pick reflect tasks / view cron', + 'help.emoji': ' /emoji [style] Pick the spinner pet face (picker / direct switch)', 'help.quit': ' /quit Quit', 'help.esc': ' Esc Cancel ask · clear draft · stop task (no exit)', 'help.cc': ' Ctrl+C × 2 Quit (when idle; only aborts the task while running)', @@ -193,7 +205,9 @@ 'cmd.hive.desc': 'enter Hive multi-worker mode', 'cmd.conductor.arg': '[task]', 'cmd.conductor.desc': 'hand task to frontends/conductor.py for multi-subagent orchestration', - 'cmd.scheduler.desc': 'multi-pick reflect tasks / show cron', + 'cmd.scheduler.desc': 'multi-pick start/stop reflect tasks (cron is driven by reflect/scheduler.py)', + 'cmd.emoji.arg': '[style]', + 'cmd.emoji.desc': 'pick the spinner pet face — opens picker; arg switches directly', # status line (one-liner above input box) 'status.asking': '◉ waiting · Esc cancel', @@ -296,9 +310,19 @@ # llm picker 'llm.title': 'Switch LLM', + # emoji picker (pet style) + 'emoji.title': 'Pick spinner pet style', + 'emoji.switched': 'pet style → `{style}`', + 'emoji.unknown': 'unknown style `{choice}` — valid: {valid}', + 'emoji.row.current': '● {name:<8} {sample}', + 'emoji.row.other': ' {name:<8} {sample}', + 'emoji.row.off': '(hide pet)', + # /scheduler picker (multi-pick reflect tasks / frontends) 'scheduler.pick.title': 'Pick services — checked = running (untick to stop)', 'scheduler.pick.hint': 'Space toggle · ↑↓ move · Enter next · Esc cancel · or /scheduler start a,b,c', + 'scheduler.cron.active': 'cron: {n} task(s) in sche_tasks/*.json · active (reflect/scheduler.py running)', + 'scheduler.cron.inactive': 'cron: {n} task(s) in sche_tasks/*.json · inactive (start reflect/scheduler.py to schedule)', 'scheduler.empty': '(no startable services: both reflect/*.py and frontends/*app*.py are empty)', 'scheduler.no_pick': '(no service picked)', 'scheduler.no_change': '(no change vs running set)', @@ -333,6 +357,23 @@ # answer prefix when committing user reply to ask_user 'msg.answer_prefix': '[ans] {text}', + + # pending input preview (queued while agent is busy) + 'pending.head_running': 'queued {n} (agent busy) · ↑ amend · Esc clear', + 'pending.head_cooldown': 'queued {n} · sending in {sec:.1f}s · ↑ amend · Esc cancel', + 'pending.cleared': 'cleared {n} pending message(s)', + 'pending.queued_marker': '[queued] {text}', + + # shell-mode magic (`!` prefix) + 'shell.hint': '! for shell mode', + 'shell.timeout': '[shell: timeout {sec}s]', + 'shell.error': '[shell error: {err}]', + 'shell.empty': '(no output)', + 'shell.history': '[!shell] {cmd}\n```\n{out}\n```\n(exit {rc})', + + # /resume + 'cmd.resume.desc': 'list recent sessions and pick one to recover', + 'help.resume': ' /resume List recent sessions and recover one', }, 'zh': { @@ -354,6 +395,14 @@ 'help.export': ' /export [sub] 导出最后回复:clip / file [name] / all', 'help.stop': ' /stop 中止当前任务', 'help.language': ' /language [code] 查看 / 切换界面语言', + 'help.update': ' /update [备注] 预览 upstream 提交与 diff,再 git pull(不 commit)', + 'help.autorun': ' /autorun [seed] 进入 autonomous_operation 自主模式', + 'help.morphling': ' /morphling [target] 启用 Morphling 蒸馏 / 吞噬外部技能', + 'help.goal': ' /goal [goal] 进入 Goal 模式(需 condition 约束)', + 'help.hive': ' /hive [target] 进入 Hive 多 worker 协作模式', + 'help.conductor': ' /conductor [task] 交给 conductor.py 做多 subagent 编排', + 'help.scheduler': ' /scheduler 多选启动 reflect 任务 / 查看 cron', + 'help.emoji': ' /emoji [style] 切换 spinner 宠物样式(picker / 直接传参)', 'help.quit': ' /quit 退出', 'help.esc': ' Esc 撤回提问 · 清草稿 · 停任务(不退出)', 'help.cc': ' Ctrl+C × 2 退出(空闲时;运行中只 abort 任务)', @@ -401,7 +450,9 @@ 'cmd.hive.desc': '进入 Hive 多 worker 协作模式', 'cmd.conductor.arg': '[任务]', 'cmd.conductor.desc': '调用 frontends/conductor.py 做多 subagent 编排', - 'cmd.scheduler.desc': '多选启动 reflect 任务 / 查看 cron', + 'cmd.scheduler.desc': '多选启动/停止 reflect 任务(cron 由 reflect/scheduler.py 驱动)', + 'cmd.emoji.arg': '[样式]', + 'cmd.emoji.desc': '切换 spinner 宠物表情 — 打开 picker;带参数则直接切换', # status line 'status.asking': '◉ 待答 · Esc 撤回提问', @@ -504,9 +555,19 @@ # llm picker 'llm.title': '切换 LLM', + # emoji picker + 'emoji.title': '选择 spinner 宠物样式', + 'emoji.switched': '宠物样式 → `{style}`', + 'emoji.unknown': '未知样式 `{choice}` — 可选:{valid}', + 'emoji.row.current': '● {name:<8} {sample}', + 'emoji.row.other': ' {name:<8} {sample}', + 'emoji.row.off': '(隐藏 pet)', + # /scheduler picker (multi-pick reflect tasks / frontends) 'scheduler.pick.title': '挑选要启动的服务(已勾选 = 运行中,取消勾选即停止)', 'scheduler.pick.hint': 'Space 勾选 · ↑↓ 移动 · Enter 下一步 · Esc 取消 · 或 /scheduler start a,b,c', + 'scheduler.cron.active': 'cron:sche_tasks/*.json 共 {n} 个任务 · 已激活(reflect/scheduler.py 在运行)', + 'scheduler.cron.inactive': 'cron:sche_tasks/*.json 共 {n} 个任务 · 未激活(启动 reflect/scheduler.py 才会调度)', 'scheduler.empty': '(没有可启动的服务:reflect/*.py 与 frontends/*app*.py 均为空)', 'scheduler.no_pick': '(未选择任何服务)', 'scheduler.no_change': '(与当前运行集合相比无变化)', @@ -541,6 +602,23 @@ # answer prefix 'msg.answer_prefix': '[答] {text}', + + # pending input preview + 'pending.head_running': '已排队 {n} 条(agent 忙)· ↑ 回看修改 · Esc 清空', + 'pending.head_cooldown': '已排队 {n} 条 · {sec:.1f}s 后发送 · ↑ 回看修改 · Esc 取消', + 'pending.cleared': '已清空 {n} 条待发送消息', + 'pending.queued_marker': '[排队] {text}', + + # shell-mode magic (`!` prefix) + 'shell.hint': '! 进入 shell 模式', + 'shell.timeout': '[shell:{sec}s 超时]', + 'shell.error': '[shell 错误:{err}]', + 'shell.empty': '(无输出)', + 'shell.history': '[!shell] {cmd}\n```\n{out}\n```\n(退出码 {rc})', + + # /resume + 'cmd.resume.desc': '列出最近会话并恢复其中一个', + 'help.resume': ' /resume 列出最近会话并恢复其中一个', }, } @@ -1004,7 +1082,11 @@ def _cleanup(): # Keep SGR (color) codes, strip everything else _ANSI_SGR_RE = re.compile(r'\x1b\[[0-9;]*m') -_TURN_MARKER_RE = re.compile(r'\*\*LLM Running \(Turn (\d+)\).*?\*\*') +# agent_loop.py emits `**LLM Running (Turn N) ...**` by default but switches +# to the short `**Turn N ...**` when `handler.parent.task_dir` is set +# (agent_loop.py:52). TUI v3 sets task_dir to enable the _intervene +# injection hook, which silently activates the short form — match both. +_TURN_MARKER_RE = re.compile(r'\*\*(?:LLM Running \()?Turn (\d+)\)?[^\n]*?\*\*') _META_TAG_RE = re.compile(r'<(?:thinking|summary|tool_use|file_content)>.*?', re.DOTALL) _TOOL_USE_BLOCK_RE = re.compile(r'```json\s*\{[^}]*"tool_name"[^}]*\}\s*```', re.DOTALL) _TOOL_USE_TAG_RE = re.compile(r'\s*\{.*?"tool_name"\s*:\s*"([^"]+)".*?\}\s*', re.DOTALL) @@ -1017,7 +1099,9 @@ def _cleanup(): # `` regex uses a negative lookahead so two adjacent summaries don't # merge; the title cleaner strips fenced code + thinking before extraction. _FENCE4_STASH_RE = re.compile(r'^`{4,}.*?^`{4,}\n?', re.DOTALL | re.MULTILINE) -_TURN_SPLIT_FOLD_RE = re.compile(r'(\*\*LLM Running \(Turn \d+\) \.\.\.\*\*)') +# Same as _TURN_MARKER_RE but capturing the WHOLE match for str.split() to +# keep the marker as a separator token in the result list. +_TURN_SPLIT_FOLD_RE = re.compile(r'(\*\*(?:LLM Running \()?Turn \d+\)? \.\.\.\*\*)') _SUMMARY_PERTURN_RE = re.compile(r'\s*((?:(?!).)*?)\s*', re.DOTALL) _TITLE_CLEAN_RE = re.compile(r'`{3,}.*?`{3,}|.*?', re.DOTALL) _TITLE_ARGS_TAIL_RE = re.compile(r',?\s*args:.*$') @@ -1229,6 +1313,14 @@ def __init__(self, llm_no: int = 0): self.agent.llmclient = self.agent.llmclients[llm_no % len(self.agent.llmclients)] self.agent.inc_out = True self.agent.verbose = True + # Give the agent a `task_dir` so the per-turn `_intervene` file hook + # in ga.turn_end_callback (ga.py:576) can fire — that's how we + # smuggle pending user messages into the next LLM call while a + # turn is still in flight. Dedicated PID-scoped dir so concurrent + # TUI v3 processes don't step on each other's intervene files. + self.agent.task_dir = os.path.join(_ROOT, 'temp', f'_tui_v3_{os.getpid()}') + try: os.makedirs(self.agent.task_dir, exist_ok=True) + except Exception: pass self.ask_user_queue: queue.Queue[AskUserEvent] = queue.Queue() self._install_hook() self._healthy = True @@ -1239,6 +1331,43 @@ def __init__(self, llm_no: int = 0): self._runner = threading.Thread(target=self._run_safe, daemon=True, name=f'ga-tui-agent') self._runner.start() + def inject_intervene(self, text: str) -> bool: + """Write `text` to `/_intervene`. ga.turn_end_callback + consumes the file at the next turn boundary and APPENDS the content + to next_prompt as `[MASTER] ...`, so the agent sees it as part of + the upcoming user message without breaking the current turn. + + Returns False if the agent isn't actually mid-turn (idle) — caller + should fall back to put_task in that case. + + Append-mode write so that if the agent's `consume_file` reads and + deletes the file between our existence check and write, our text + lands in a freshly-created file (and fires the NEXT turn) instead + of duplicating already-consumed content from a read-modify-write + TOCTOU window. The `\\n\\n` separator only goes in when the file + already has content right before we open it — race-narrow but + idempotent because the worst case is a leading blank line.""" + td = getattr(self.agent, 'task_dir', None) + if not td or not getattr(self.agent, 'is_running', False): + return False + try: + os.makedirs(td, exist_ok=True) + except Exception: + return False + fp = os.path.join(td, '_intervene') + try: + sep = '' + try: + if os.path.getsize(fp) > 0: + sep = '\n\n' + except OSError: + pass # file gone — fresh create on append, sep stays empty + with open(fp, 'a', encoding='utf-8') as f: + f.write(sep + text) + return True + except Exception: + return False + def _run_safe(self): try: self.agent.run() @@ -1353,16 +1482,36 @@ def drain_display_queue(self, dq: queue.Queue, timeout: float = 0.25): # 256 inverse (which renders muddy on Win Terminal dark themes) to truecolor. _TILE_U = '\x1b[48;2;55;55;55m\x1b[38;2;230;230;230m' _MARK = _ACCENT + '❯' + _RST # prompt mark — the single accent +# Shell-mode (`!` magic prefix) accents — vivid pink so it stands out from +# the normal accent purple without clashing with the heat-counter reds. +_SHELL_ACCENT = '\x1b[38;5;205m' # hot pink for border / prompt mark +_SHELL_BG = '\x1b[48;2;65;60;65m' # 65,60,65 charcoal-magenta (per spec) +_SHELL_MARK = _SHELL_ACCENT + '!' + _RST +# Full-row tile for committed shell rows (echo + each output line), so the +# pair reads as one block in scrollback, matching cc-style. Slightly +# warmer than _TILE_U (55,55,55) so the two row kinds are distinguishable +# when interleaved. Black-terminal only — light themes get the bare band. +_TILE_SHELL = '\x1b[48;2;65;60;65m\x1b[38;2;230;230;230m' _BG_TOK = {str(n) for n in list(range(40, 48)) + [49] + list(range(100, 108))} _SGR_RE = re.compile(r'\x1b\[([0-9;]*)m') _CSI_ERASE_RE = re.compile(r'\x1b\[[0-9;?]*[JK]') _SGR_TOKEN_RE = re.compile(r'\x1b\[[0-9;]*m') -def _tile(s: str, style: str) -> str: - # re-assert style after every reset so muted-markdown \x1b[0m can't punch - # a hole in the block; \x1b[K fills to the edge (CJK-safe full-width tile). - return style + s.replace(_RST, _RST + style) + '\x1b[K' + _RST +def _tile(s: str, style: str, width: int | None = None) -> str: + # Re-assert style after every reset so muted-markdown \x1b[0m can't punch + # a hole in the block. When `width` is provided we pad with explicit + # bg-active spaces — prompt-toolkit's cell renderer doesn't honour + # \x1b[K (erase-to-EOL) inside its own buffer, so PTK-bound scrollback + # would otherwise leave the row gap exposed. Fall back to \x1b[K for + # legacy callers writing straight to the terminal (no PTK), where the + # erase command still fills correctly. + body = style + s.replace(_RST, _RST + style) + if width is None: + return body + '\x1b[K' + _RST + visible = _SGR_TOKEN_RE.sub('', s) + pad = max(0, width - cell_len(visible)) + return body + ' ' * pad + _RST def _border(left: str, right: str, width: int, style: str = _BORDER) -> str: @@ -1399,8 +1548,8 @@ def repl(m: re.Match) -> str: _PLACEHOLDER_RES = (_PASTE_PH_RE, _IMG_PH_RE, _FILE_PH_RE) _IMAGE_EXTS = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff', '.tif', '.ico'} _TURN_MK_RE = re.compile( - r'\*\*LLM Running \(Turn \d+\)[^\n]*\*\*' # native live-format marker - r'|^[ \t]*Turn \d+\s*\.{3,}[ \t]*$', # plain subagent form on its own line + r'\*\*(?:LLM Running \()?Turn \d+\)?[^\n]*\*\*' # native + task-mode short form + r'|^[ \t]*Turn \d+\s*\.{3,}[ \t]*$', # plain subagent form on its own line re.M) _TOOL_RE = re.compile( r'🛠️ Tool: `([^`]+)`[^\n]*\n' # 1 = name @@ -1408,7 +1557,9 @@ def repl(m: re.Match) -> str: r'(?:' r'(`{5,})[^\n]*\n(.*?)\n\4[ \t]*\n*' # 4 = result fence (5-bt), 5 = body r'|' # ─ OR ─ - r'(.*?)(?=^🛠️ Tool: `|^\*\*LLM Running|^|\Z)' # 6 = live exec trace + # Trail-end sentinel: either form of the `**Turn N ...**` marker, or a + # bare `Turn N ...` on its own line, or the next tool / summary tag. + r'(.*?)(?=^🛠️ Tool: `|^\*\*(?:LLM Running \()?Turn \d+|^Turn \d+ \.\.\.$|^|\Z)' # 6 = live exec trace r')', re.DOTALL | re.MULTILINE) # Prompted-style tool wrappers GA models emit AS TEXT in saved logs (no @@ -1449,6 +1600,15 @@ def _tool_status(result: str, trailing: str) -> str: return 'error' if re.match(r'^(?:Error[:\s]|Exception[:\s]|Traceback|❌|⛔)', s.lstrip(), re.I): return 'error' + # ga.do_ask_user yields a 'Waiting for your answer ...' marker BEFORE the + # user has actually answered (it's the "I'm blocking on input" signal). + # The plain s.strip() truthy check below would otherwise light the chip + # ✓ ok the instant that marker appears — making the user think the tool + # has finished while the input prompt is, in reality, still waiting. + # Mark it pending (· …) until something else (the answer, a real status + # line) lands in the result. + if 'Waiting for your answer' in s and '✅' not in s and '成功' not in s: + return '?' if '✅' in s or '成功' in s or s.strip(): return 'ok' return '?' @@ -1640,6 +1800,8 @@ def _cmds() -> list[tuple[str, str, str]]: ('/export', _t('cmd.export.arg'), _t('cmd.export.desc')), ('/stop', '', _t('cmd.stop.desc')), ('/language', _t('cmd.language.arg'), _t('cmd.language.desc')), + ('/emoji', _t('cmd.emoji.arg'), _t('cmd.emoji.desc')), + ('/resume', '', _t('cmd.resume.desc')), ('/quit', '', _t('cmd.quit.desc')), ] @@ -1675,18 +1837,50 @@ def _gerund(el: float) -> str: # width, making `(>_<)` look "fat" and shoving the heat counter sideways. # `/emoji ascii` switches to bracketed glyphs that stay single-width on # every terminal. `/emoji off` hides the pet entirely. -_PETS_ASCII = ( - ('[:)] ', '[:)] ', '[:)] ', '[:|] '), - ('[:|] ', '[;|] ', '[:|] ', '[|:] '), - ('[-_-]', '[-_-]', '[---]', '[-_-]'), - ('[>_<]', '[@_@]', '[>_<]', '[T_T]'), +# Cat head — calm tier uses • (sleepy/cute look), `o` reserved for the +# focused tier, `-` for the sleepy tier so each row's mood reads +# distinctly. Each tier's 4 frames share a width within the tier. +_PETS_CAT = ( + ('=^•.•^=', '=^•.•^=', '=^-.-^=', '=^•.•^='), + ('=^o.o^=', '=^o.-^=', '=^o.o^=', '=^-.o^='), + ('=^-.-^=', '=^-.-^=', '=^v.v^=', '=^-.-^='), + ('=^>.<^=', '=^@.@^=', '=^>.<^=', '=^T.T^='), +) +# Bracketed dot-eye — same mood arc; `•` for calm, `o` for focused. +_PETS_DOT = ( + ('[•.•]', '[•.•]', '[-.-]', '[•.•]'), + ('[o.o]', '[o.-]', '[o.o]', '[-.o]'), + ('[-.-]', '[-.-]', '[v.v]', '[-.-]'), + ('[>.<]', '[@.@]', '[>.<]', '[T.T]'), ) -_PET_STYLES = {'unicode': _PETS_UNICODE, 'ascii': _PETS_ASCII} -_pet_style = 'unicode' # mutated by /emoji