From 05a82dbdc945d6cfa1c72444a0ed27ce069802a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 14:44:11 +0000 Subject: [PATCH] =?UTF-8?q?=E6=B5=81=E5=BC=8F=E6=94=B9=E6=88=90=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=BC=80,TALOS=5FSTREAM=3D0=20=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 没设 TALOS_STREAM 就流;设成 0 / false / no / off(不分大小写、容空格)回到整份返回那条老路。 README 那段和 agent.py 的注释跟着改:哪家跑出毛病,先关掉它,再来看是哪一块没拼对。 判据要真的起一个进程:开关在 import 时读环境变量,而 conftest 为了让几百条用例的假客户端 照旧回整份,把每条测试里的 STREAM 都钉成了关 —— 在测试进程里看它永远是关的。 cwd 放在空目录,别让本机的 .env 替判据做决定。旧代码下当场红在「没设时没开」。 端到端走了一遍真的 once()(也就是 agent.py -p 那条路),本地起一个 OpenAI 兼容的假服务 吐流式块,在伪终端里跑:转圈从「模型在写 write_file(hello.py) · 800 字」涨到 14,435 字, 文件 14,000 字符原样落地,两次请求都带了 stream + include_usage,token 和缓存命中照常入账。 还是没对哪一家真调用过。 345 判据:343 过、5 skip、1 xfail(原有);TALOS_STREAM=1 下同样绿。selfcheck 绿。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014gdkHx6rLSMVsUmQiErVSn --- DEVELOPMENT.md | 4 ++-- README.md | 10 +++++----- agent.py | 7 ++++--- tests/conftest.py | 5 +++-- tests/test_loop.py | 28 +++++++++++++++++++++++++++- 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index e9dc45c..33de5ac 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -150,7 +150,7 @@ def check_permission(state, cls, name, args): # 决策 + ```bash python agent.py --selfcheck # 零依赖、零网络、零 key。改完先跑这个 -python -m pytest tests/ -q # 344 条。CI 跑 ubuntu/windows × 3.10/3.13 +python -m pytest tests/ -q # 345 条。CI 跑 ubuntu/windows × 3.10/3.13 ``` `--selfcheck` 覆盖 4 个工具的读/写/改、`edit_file` 的"找不到 / 不唯一"两种报错、 @@ -190,7 +190,7 @@ set TALOS_MAX_STEPS=12 | **行为类** | **模型读到守卫那条消息之后干什么** | ❌ 必须用你真在用的那个 | 「拒绝之后会不会改用 `python -c` 绕路」是那个具体模型的性格,换了模型不算数。 -而**行为类正是 live 测试唯一还值钱的部分** —— 管道通不通,344 条判据已经免费覆盖了。 +而**行为类正是 live 测试唯一还值钱的部分** —— 管道通不通,345 条判据已经免费覆盖了。 按这条界,**目标闸的 live 判据是行为类的**:离线判据已经证明判断器拿得到 `read_file`、 越界会被驳回、判不出来时不假装成功;它们证明不了的是**这个具体模型拿到只读工具之后 diff --git a/README.md b/README.md index 8f4809b..257ae22 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![tests](https://github.com/Jerry-TZ/Talos/actions/workflows/test.yml/badge.svg)](https://github.com/Jerry-TZ/Talos/actions/workflows/test.yml) -**一个你能完整读完的编程 agent。** 5441 行 Python,344 条离线判据,一份不糊弄人的安全说明。 +**一个你能完整读完的编程 agent。** 5442 行 Python,345 条离线判据,一份不糊弄人的安全说明。 Talos 终端界面:动盘之前先弹确认框 @@ -19,7 +19,7 @@ | 文件 | 行数 | 职责 | |---|---|---| -| `agent.py` | 4318 | 循环 + 工具 + 权限门 + 自学习 | +| `agent.py` | 4319 | 循环 + 工具 + 权限门 + 自学习 | | `console_ui.py` | 235 | 终端界面(可整体替换) | | `recall.py` | 582 | 联想记忆:扩散激活检索 | | `session.py` | 306 | 会话持久化(想换 SQLite 只改这个) | @@ -33,7 +33,7 @@ 极简 agent 赛道很挤,有人用 Zig 做到 678KB 二进制。**Talos 不比谁小,它比谁都好读。** - **能读完** — 四个文件,注释解释的是*为什么*,不是*是什么*。几乎每条防御旁边都写着它挡的那次真实翻车。 -- **能验证** — 344 个测试,**离线、免 API key、几秒跑完**,CI 在 Linux/Windows × Python 3.10/3.13 上都跑。clone 下来立刻知道它没坏。 +- **能验证** — 345 个测试,**离线、免 API key、几秒跑完**,CI 在 Linux/Windows × Python 3.10/3.13 上都跑。clone 下来立刻知道它没坏。 - **不吹牛** — [`SECURITY.md`](SECURITY.md) 明写 `create_tool` 就是进程内 RCE、正则黑名单只是减速带。**没有沙箱就是没有沙箱** —— 真要隔离,[三条现成方案](SECURITY.md#真要隔离怎么办)按代价从低到高列在那儿。 - **有考卷** — [`EXAM.md`](EXAM.md) / [`EXAM2.md`](EXAM2.md) 是两份可复现的能力测试,带标准答案和作弊检测(比如逐个核验 arXiv ID 真伪,防止编造引用)。记录的是"我怎么验证它真的有用",不是功能列表。 - **有实测** — [`FINDINGS.md`](FINDINGS.md) 记了二十二个真实任务量出来的东西:哪条提示词生效、哪条从头到尾没生效、六次翻车、检索改动的前后数字,以及**两个被数据否掉的自己的方案**。样本小,局限写在最前面。 @@ -159,7 +159,7 @@ tail -3 .talos/recall_trace.jsonl **一次性模式** — `agent.py -p "任务"` 跑完即退,方便脚本和计时。 -**流式(试验,默认关)** — `TALOS_STREAM=1` 让主循环边生成边收,转圈那一行实时显示「模型在写 write_file(src/app.py) · 12,000 字」,分得清慢和卡死。拼回来的回复跟整份返回的逐项相同(判据钉着),断流按原来的重试规矩整次重来。几家的流式形状只照文档和造出来的数据核对过,**还没真调用过** —— 跑出问题关掉,就回到原来那条路。 +**流式(默认开,`TALOS_STREAM=0` 关)** — 主循环边生成边收,转圈那一行实时显示「模型在写 write_file(src/app.py) · 12,000 字」,分得清慢和卡死。拼回来的回复跟整份返回的逐项相同(判据钉着),断流按原来的重试规矩整次重来。几家的流式形状只照文档和造出来的数据核对过,**还没真调用过** —— 跑出问题先设 `TALOS_STREAM=0`,就回到整份返回那条老路。 **可选自动化**(默认关闭,在 `talos.bat` 里开): @@ -202,7 +202,7 @@ Ctrl+C 停下当前这轮 —— 做过的都留着,可以直接说 ```bash .venv\Scripts\python.exe -m pip install -r requirements-dev.txt -.venv\Scripts\python.exe -m pytest tests/ -q # 344 条,约 8 秒,不联网、不需要 key +.venv\Scripts\python.exe -m pytest tests/ -q # 345 条,约 8 秒,不联网、不需要 key python agent.py --selfcheck # 免依赖的冒烟检查 ``` diff --git a/agent.py b/agent.py index e8b4ea4..111ff03 100644 --- a/agent.py +++ b/agent.py @@ -262,10 +262,11 @@ def _env_block() -> str: CONNECT_TIMEOUT = float(os.environ.get("TALOS_CONNECT_TIMEOUT", "5")) SLOW_CALL = float(os.environ.get("TALOS_SLOW_CALL", "15")) # 超过这么久的调用才报耗时 # 流式:主循环那一次调用边生成边收,转圈那一行实时报「在写 write_file(x.py) · 12,000 字」。 -# **默认关**:六家的流式细节各不一样(Gemini 不给 index、Kimi 把用量塞进 choices[0]), -# 写它的时候手上没有 key,只照文档和造出来的块核对过。开着跑稳了再改默认。 +# **默认开,`TALOS_STREAM=0` 关。** 六家的流式细节各不一样(Gemini 不给 index、Kimi 把 +# 用量塞进 choices[0]),写它的时候手上没有 key,只照文档和造出来的块核对过 —— 所以哪家 +# 跑出毛病,先关掉它回到整份返回那条老路,再来看是哪一块没拼对。 # 只影响看得见的那一处 —— 判断器、压缩摘要、复盘你看不到,流了也没用。 -STREAM = os.environ.get("TALOS_STREAM", "").strip() in ("1", "true", "yes", "on") +STREAM = os.environ.get("TALOS_STREAM", "").strip().lower() not in ("0", "false", "no", "off") # 不带 include_usage,流式默认**不回用量**:会话预算提醒和缓存命中率就静悄悄地全是 0 _STREAM_KW = {"stream": True, "stream_options": {"include_usage": True}} diff --git a/tests/conftest.py b/tests/conftest.py index febdf1b..9cd2b90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -85,8 +85,9 @@ def _no_test_ever_writes_the_real_talos(tmp_path, monkeypatch): # 就会往真实的 `.talos/sessions/` 里塞垃圾,而那批文件正是往事检索和燃尽表的语料。 # 上面那段说「枚举永远落后一步」,这就是下一步:**加功能等于给这片面新开一条路。** monkeypatch.setattr(session, "SESS_DIR", os.path.join(d, "sessions")) - # 开着 `TALOS_STREAM=1` 的终端里跑 pytest,几百条用例的假客户端会收到 stream=True - # 而照旧回整份 —— 全套红一片,红的原因跟被测的东西毫无关系。要测流式的自己打开。 + # 流式默认开,而几百条用例的假客户端照旧回整份 —— 不钉成关,全套红一片, + # 红的原因跟被测的东西毫无关系。要测流式的自己打开;默认值本身由 + # test_streaming_is_on_unless_you_turn_it_off 在子进程里查。 monkeypatch.setattr(agent, "STREAM", False) @pytest.fixture(autouse=True) diff --git a/tests/test_loop.py b/tests/test_loop.py index fe21388..e992804 100644 --- a/tests/test_loop.py +++ b/tests/test_loop.py @@ -2689,8 +2689,34 @@ def turn(client, model, messages, state, top=False): assert b_human not in asked, "已删掉的会话的原话还在参与「点名」判断" -# ── 流式(TALOS_STREAM=1):拼回来的必须跟整份返回的是同一个东西 ───────────────── +# ── 流式(默认开,TALOS_STREAM=0 关):拼回来的必须跟整份返回的是同一个东西 ────── # 形状照 openai 2.x 的 `ChatCompletionChunk` 造(真对象在本机核对过),不 import openai。 +def test_streaming_is_on_unless_you_turn_it_off(tmp_path): + """默认开,`TALOS_STREAM=0`(或 false/no/off)关。 + + 必须**真的起一个进程**:这个开关在 import 时读环境变量,而 conftest 为了让假客户端 + 照旧回整份,把每条测试里的 `STREAM` 都钉成了关 —— 在测试进程里看,它永远是关的。 + 同 `test_no_provider_key_survives_startup` 那条的道理。 + + cwd 放在空的 tmp 目录:启动时会读当前目录的 `.env`,别让本机那份替判据做决定。""" + import os + import subprocess + import sys + import agent as A + code = f"import sys; sys.path.insert(0, {A.HOME!r}); import agent; print(agent.STREAM)" + base = {k: v for k, v in os.environ.items() if k != "TALOS_STREAM"} + def run(**env): + r = subprocess.run([sys.executable, "-c", code], cwd=str(tmp_path), capture_output=True, + text=True, encoding="utf-8", errors="replace", + env=dict(base, PYTHONIOENCODING="utf-8", **env)) + assert r.returncode == 0, "探针进程没起来:" + (r.stderr or "")[-800:] + return r.stdout.strip() + assert run() == "True", "没设 TALOS_STREAM 时流式没开" + for off in ("0", "false", "no", "off", " OFF "): + assert run(TALOS_STREAM=off) == "False", f"TALOS_STREAM={off!r} 没关掉流式" + assert run(TALOS_STREAM="1") == "True" + + def _d(content=None, reasoning=None, tool_calls=None): """一块增量。`reasoning_content` 是 DeepSeek / Kimi 的字段,SDK 不认识,挂在 extra 上。""" delta = types.SimpleNamespace(content=content, tool_calls=tool_calls,