Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 的"找不到 / 不唯一"两种报错、
Expand Down Expand Up @@ -190,7 +190,7 @@ set TALOS_MAX_STEPS=12
| **行为类** | **模型读到守卫那条消息之后干什么** | ❌ 必须用你真在用的那个 |

「拒绝之后会不会改用 `python -c` 绕路」是那个具体模型的性格,换了模型不算数。
而**行为类正是 live 测试唯一还值钱的部分** —— 管道通不通,344 条判据已经免费覆盖了。
而**行为类正是 live 测试唯一还值钱的部分** —— 管道通不通,345 条判据已经免费覆盖了。

按这条界,**目标闸的 live 判据是行为类的**:离线判据已经证明判断器拿得到 `read_file`、
越界会被驳回、判不出来时不假装成功;它们证明不了的是**这个具体模型拿到只读工具之后
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 条离线判据,一份不糊弄人的安全说明。

<img src="docs/demo.svg" alt="Talos 终端界面:动盘之前先弹确认框" width="100%">

Expand All @@ -19,7 +19,7 @@

| 文件 | 行数 | 职责 |
|---|---|---|
| `agent.py` | 4318 | 循环 + 工具 + 权限门 + 自学习 |
| `agent.py` | 4319 | 循环 + 工具 + 权限门 + 自学习 |
| `console_ui.py` | 235 | 终端界面(可整体替换) |
| `recall.py` | 582 | 联想记忆:扩散激活检索 |
| `session.py` | 306 | 会话持久化(想换 SQLite 只改这个) |
Expand All @@ -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) 记了二十二个真实任务量出来的东西:哪条提示词生效、哪条从头到尾没生效、六次翻车、检索改动的前后数字,以及**两个被数据否掉的自己的方案**。样本小,局限写在最前面。
Expand Down Expand Up @@ -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` 里开):

Expand Down Expand Up @@ -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 # 免依赖的冒烟检查
```

Expand Down
7 changes: 4 additions & 3 deletions agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}}

Expand Down
5 changes: 3 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 27 additions & 1 deletion tests/test_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading