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 # 343 条。CI 跑 ubuntu/windows × 3.10/3.13
python -m pytest tests/ -q # 344 条。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 测试唯一还值钱的部分** —— 管道通不通,343 条判据已经免费覆盖了。
而**行为类正是 live 测试唯一还值钱的部分** —— 管道通不通,344 条判据已经免费覆盖了。

按这条界,**目标闸的 live 判据是行为类的**:离线判据已经证明判断器拿得到 `read_file`、
越界会被驳回、判不出来时不假装成功;它们证明不了的是**这个具体模型拿到只读工具之后
Expand Down
8 changes: 4 additions & 4 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。** 5435 行 Python,343 条离线判据,一份不糊弄人的安全说明。
**一个你能完整读完的编程 agent。** 5441 行 Python,344 条离线判据,一份不糊弄人的安全说明。

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

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

| 文件 | 行数 | 职责 |
|---|---|---|
| `agent.py` | 4312 | 循环 + 工具 + 权限门 + 自学习 |
| `agent.py` | 4318 | 循环 + 工具 + 权限门 + 自学习 |
| `console_ui.py` | 235 | 终端界面(可整体替换) |
| `recall.py` | 582 | 联想记忆:扩散激活检索 |
| `session.py` | 306 | 会话持久化(想换 SQLite 只改这个) |
Expand All @@ -33,7 +33,7 @@
极简 agent 赛道很挤,有人用 Zig 做到 678KB 二进制。**Talos 不比谁小,它比谁都好读。**

- **能读完** — 四个文件,注释解释的是*为什么*,不是*是什么*。几乎每条防御旁边都写着它挡的那次真实翻车。
- **能验证** — 343 个测试,**离线、免 API key、几秒跑完**,CI 在 Linux/Windows × Python 3.10/3.13 上都跑。clone 下来立刻知道它没坏。
- **能验证** — 344 个测试,**离线、免 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 @@ -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 # 343 条,约 8 秒,不联网、不需要 key
.venv\Scripts\python.exe -m pytest tests/ -q # 344 条,约 8 秒,不联网、不需要 key
python agent.py --selfcheck # 免依赖的冒烟检查
```

Expand Down
10 changes: 8 additions & 2 deletions agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2412,12 +2412,18 @@ def _reasoning(msg) -> str:
return str(extra.get("reasoning_content") or extra.get("reasoning") or "")

def _usage(resp):
"""Token usage from a response (0s if the provider doesn't report it). -> (in, out, cached)."""
"""Token usage from a response (0s if the provider doesn't report it). -> (in, out, cached).

缓存命中先认 OpenAI 那一处;没有再认两家自己的老字段 —— DeepSeek 的
`prompt_cache_hit_tokens`、Kimi 顶层的 `cached_tokens`。现在的文档里两家也都填
OpenAI 那处,但只带老字段的响应还有(旧接口、中转网关),那时命中静悄悄地记成 0。"""
u = getattr(resp, "usage", None)
if not u:
return (0, 0, 0)
d = getattr(u, "prompt_tokens_details", None)
cached = (getattr(d, "cached_tokens", 0) or 0) if d is not None else 0
cached = (((getattr(d, "cached_tokens", 0) if d is not None else 0)
or getattr(u, "prompt_cache_hit_tokens", 0) or getattr(u, "cached_tokens", 0))
or 0)
return (getattr(u, "prompt_tokens", 0) or 0, getattr(u, "completion_tokens", 0) or 0, cached)

CHAT_TRIES = 4 # 一次调用最多试几趟(含第一趟)
Expand Down
26 changes: 26 additions & 0 deletions tests/test_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -2830,6 +2830,32 @@ def test_stream_usage_is_read_from_wherever_the_provider_puts_it(monkeypatch):
assert A._usage(A._collect(kimi_style)) == (100, 20, 60), "Kimi 放在 choices[0] 里的用量没认出来"


def test_cached_tokens_are_read_from_whichever_field_the_provider_fills():
"""缓存命中数原来只认 OpenAI 那一处(`prompt_tokens_details.cached_tokens`)。

按现在的文档,DeepSeek、Kimi、GLM、Gemini 也都填这一处 —— FINDINGS 里那些实测命中率
(81.5%、94%)就是这么量出来的,所以**日常不是 0**。但 DeepSeek 和 Kimi 各有自己的老字段,
而只带老字段的响应还在(旧接口、中转网关;any-llm、Operit 都报过 DeepSeek 命中被丢成 0):
DeepSeek 的 `prompt_cache_hit_tokens`,Kimi 顶层的 `cached_tokens`。
撞上的时候命中**静悄悄地记成 0** —— 不报错,只是账不对,而 cache_trace 正是拿来
判断缓存优化有没有效的样本。"""
import agent as A
U = types.SimpleNamespace
def resp(**u):
return U(usage=U(prompt_tokens=100, completion_tokens=20, **u))
assert A._usage(resp(prompt_tokens_details=U(cached_tokens=60))) == (100, 20, 60)
assert A._usage(resp(prompt_tokens_details=None, prompt_cache_hit_tokens=60,
prompt_cache_miss_tokens=40)) == (100, 20, 60), "DeepSeek 的老字段没认"
assert A._usage(resp(cached_tokens=60)) == (100, 20, 60), "Kimi 顶层的 cached_tokens 没认"
# SDK 给的 details 可能在、但 cached_tokens 是 None
assert A._usage(resp(prompt_tokens_details=U(cached_tokens=None),
prompt_cache_hit_tokens=60)) == (100, 20, 60)
# 流式里 Kimi 那份用量是原样 dict,经 `_ns` 之后同样认
assert A._usage(U(usage=A._ns({"prompt_tokens": 100, "completion_tokens": 20,
"cached_tokens": 60}))) == (100, 20, 60)
assert A._usage(resp()) == (100, 20, 0), "哪家都没报就是 0,不是报错"


def test_a_stream_that_breaks_midway_starts_over_instead_of_splicing(monkeypatch):
"""流到一半断了:整次重来,不接着拼。

Expand Down
Loading