Repository navigation
Let the sidebar hand the agent loop to pi, which can ask the page's own model - #8
Conversation
What runs the loop is now a choice, like what runs the model: the built-in
harness, or pi, a coding agent that asks a model it has credentials for
(Claude, GPT…). The page draws pi's steps in the same diagram, step cards
and event list, from the events pi reports, without tokens or a prompt.
Harness
- harness/harnesses/harness.py: Harness, what the page asks of whatever
runs the loop (run, stop, decide), and which events every harness sends
and which only one that knows them.
- harness/agent.py moves to harness/harnesses/simple_harness.py, and Run
becomes SimpleHarness, beside SimpleBackend; run_start says which harness
ran the turn ("simple" or "pi").
- LiveRun takes a function that makes the harness (harness_for()).
pi
- harness/harnesses/pi_harness.py: PiHarness starts pi in RPC mode for each
question, in the conversation's folder and on its session, without the
user's extensions, skills or context files; it sets the model and the
thinking level and turns pi's retries off before the question goes, and
turns pi's records into the page's events. One of pi's turns is a step;
its streamed text is a new event, delta. pi has no step limit, so the page
aborts it past Max steps.
- harness/harnesses/pi_approve.ts: a pi extension that asks before every
tool call; its dialog becomes the page's Allow and Reject, and a refusal
reaches the model in the turn's language.
- The sidebar: What runs the loop, then pi's models (pi.probe), its tools,
its thinking level, Ask me before running a tool and Max steps. The
Context tab says pi shows no prompt; the Model tab shows pi's model,
settings and tools, which run on this computer without a sandbox.
- render.py draws a run without a family, a context or tokens: the streamed
text by channel, the provider's token counts, and pi's own wording for
its stops and its diagram.
Tests and docs
- tests/fake_pi.py speaks pi 1.4's RPC protocol from a script, so the
default suite needs neither Node nor a model; test_pi_harness.py covers
PiHarness, and with RUN_PI_TESTS=1 runs the real pi against a scripted
model on 127.0.0.1 (passes with pi 1.4.0). test_render.py and
test_app.py cover pi's runs and the page with pi picked.
- How it works gets a section on pi and the delta event; the READMEs,
AGENTS.md and the guide's excerpts follow the move and the rename.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uest pi sends With pi picked under What runs the loop, The model pi asks now picks either this page's model (the default) or one of pi's own. On the page's model, Gemma 4 E2B or Qwen3.5 0.8B on transformers or vLLM, pi's steps show the string the model read and every token, as the built-in harness's do; on pi's own models, each step's context section shows the request pi sent. The page's model for pi - harness/harnesses/model_endpoint.py serves the model in memory as an OpenAI-compatible chat completions endpoint on 127.0.0.1, behind a token, only while a turn is served. Each request becomes the conversation SimpleHarness keeps, is rendered with the chat template, encoded and generated; its context, prefill, token and model_end events go into pi's step, and the reply streams back to pi (thinking as reasoning_content, the tool calls the family parses as tool_calls). - A request waits for the step pi opened for it, since pi's records and its HTTP request reach the page by different ways. - The sidebar keeps the model menu, What runs the model and the decoding settings for it; Thinking mode reaches the model as pi's thinking level. The request pi sends - pi_approve.ts becomes pi_extension.ts, loaded into every pi: it reports each request pi sends the model (before_provider_request) as a notification, registers the page's model as the provider agent-loop, and, with Ask me before running a tool, asks before each call. - A new event, request: the request as JSON, image bytes left out, with what the step added marked. It is the context section of a step on a provider's model, and folded under the context on the page's model. Screenshots and docs - docs/screenshot-en.png and docs/screenshot-zh.png are retaken from a real Gemma run (on MPS), with What runs the loop at the top of the sidebar. - How it works: the pi section in tabs, one per kind of model pi asks, with the endpoint's code; the request event in the event table. The READMEs and AGENTS.md follow. Tests - test_model_endpoint.py asks the endpoint over HTTP with FakeLLM behind it; fake_pi.py asks it as pi does, so test_pi_harness.py, test_render.py and test_app.py cover pi on the page's model in the default suite. - With RUN_PI_TESTS=1 the real pi 1.4.0 asks the endpoint too, and with RUN_MODEL_TESTS=1 it asks real Gemma 4 E2B and Qwen3.5 0.8B. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
How it works now treats what runs the loop as it treats what runs the model and the model: a part of a section that differs by harness is told once for each, in tabs, and picking one switches every set on the page. - A new tab kind, What runs the loop: Built-in harness (SimpleHarness) and pi (PiHarness), with pi-page and pi-provider as narrower kinds of pi, as vllm-docker is of vllm, where pi asking this page's model and pi asking one of its own differ. guide.js already syncs narrower kinds. - The overview, sending a question, preparing a run, context, model, parse, stop, writing the call back, checks, environment, observation and the page each get harness tabs, with pi's code in its tabs. The separate section on pi is spread into them, so the guide has 18 sections again. - Tabs do not nest, so what the built-in harness and pi on this page's model share (the chat template, the parsers, the backends) stays outside the harness tabs, saying it is for both. - The READMEs and AGENTS.md say the guide's tabs cover the harness too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cyyeh
left a comment
There was a problem hiding this comment.
Review 摘要
結論:功能大致實作正確,架構也清楚:一個 Harness 介面,事件是唯一的介面,render 只畫收到的事件、不補沒看到的東西。內建 harness 沒有看到回歸。
需要處理的有三件:
- 一個小的並行問題:已重現,修法只要兩行。
- 幾處第二個 commit 之後過時、使用者看得到的文字。
- 兩條沒有測試的路徑。
驗證了什麼
測試與 guide
uv run pytest:919 passed, 131 skipped,沒有失敗。skip 比 PR 描述的多,是因為這台機器沒有模型權重和 sandbox image。- 只用 pygments:
tests/test_guide.py112 passed,python -m app.guide _site也建得出來。
對照 pi 1.4.0 的文件和 openai-completions provider 的原始碼,PR 依賴的行為都成立:
prompt在被接受後就回 response(disposition: "started"),事件之後才串流。tool_execution_start在 extension 的tool_call之前,所以 confirm 來時call_id已經在_calls裡。notify在 RPC 模式是 fire-and-forget 的extension_ui_request。thinkingFormat: "chat-template"配$var: "thinking.enabled"是文件寫的用法。- 端點出錯時送的
finish_reason(error: …)pi 不認得,會變成stopReason: "error"和Provider finish_reason: …,所以錯誤確實會顯示在頁面上。 reasoning_content會被讀成 thinking。
另外用 FakePi 跑了兩條沒有測試的路徑
- pi 問網頁的模型時的允許/拒絕:結果正常,拒絕的原因用這一輪的語言回到模型。
- pi 問網頁的模型時的步數上限:發現下面第 1 個問題。
沒有跑
RUN_PI_TESTS=1:這個環境用 npm 裝 pi 一直卡住,也不允許執行下載來的程式。RUN_MODEL_TESTS=1:這台機器沒有權重。
這兩項的結果請以 PR 描述為準。
問題(細節見 inline)
_abort()沒有 notify_unserved:pi 問網頁的模型、跑到步數上限時,模型端點可能抱著ep._lock白等WAIT_FOR_STEP(10 秒),下一個問題會被卡住。已重現:整輪 10.12 秒,加上 notify 後 0.45 秒。- 過時的 UI 文字:
PI_INTRO、「用什麼跑迴圈」的說明、page.py 的 docstring、pi 的模型卡和工具卡。在預設設定(pi 問這個網頁的模型)下,這些文字說的和畫面相反,例如「脈絡分頁是空的」、「沒有 token 和機率」。
測試覆蓋度
整體覆蓋得不錯:
- FakePi 走的是真的 RPC 協定和真的 HTTP 端點。
- 每個事件前綴都會 render,轉義和「英文頁沒有中文」也都有測。
缺口:
- pi 問網頁的模型 + 「執行工具前先問我」:FakePi 和真的 pi 都沒有測。
asking()只用 provider 的模型;真的 pi 的approve參數也只用在 scripted provider 上。我用 FakePi 試過,行為是對的,建議補成測試。 - pi 問網頁的模型 + 步數上限:沒有測試,而正好是第 1 個問題出現的路徑。
- 介紹文字的內容:依
pi_source該說什麼沒有被斷言,所以文字過時了也沒有測試失敗。
設計建議(不擋 merge)
- pi 預設的工具有 bash、edit、write,都在這台電腦上執行、沒有沙箱,而「執行工具前先問我」預設是關的。
- 配上 E2B、0.8B 這樣的小模型時風險更高,而且有 bash 時,需要 Python 的範例就能直接按。
- 可以考慮選 pi 時把「執行工具前先問我」預設打開。側欄已經有警語,所以這是取捨,不算 bug。
Generated by Claude Code
… the model pi asks Review of #8. Aborting pi: - `_abort()` now wakes a request that waits for its step (`_serving_step`). pi's request for the turn past Max steps can reach the endpoint before the page has read that turn's start; it then waited out WAIT_FOR_STEP (10 s) holding the endpoint's lock, so the next question's first request waited too. - The endpoint turns away a request that belongs to no step once the turn is being stopped, instead of rendering, encoding and prefilling it for a pi that has given up on it. Text that predated pi asking the page's model: - The intro on the Loop tab now depends on the model pi asks: on this page's model it says every token and the string the model reads show; on pi's own models, that the Context tab holds pi's request and that no probabilities show. - What runs the loop's help, page.py's docstring, pi's model card and its tools card no longer say the prompt and the tools' schemas stay out of view. Tests: - pi on the page's model at the step limit, with the request arriving before the page reads the turn's start: the turn ends at once, the request is turned away and the endpoint is free; with RUN_PI_TESTS=1, the same with the real pi, asking first. - Ask me before running a tool on the page's model: Allow and Reject, and what the model reads of each. - The intro says what shows for each model pi asks. - FakePi ends a message in an HTTP error, as pi's provider does. How it works §18 says the abort wakes the waiting request; tests/guide_excerpts.txt was rewritten after rereading it. Both READMEs' test tables name the new tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
謝謝 review。每一點都對照了程式碼,全部確認正確,c735e8e 修正,細節回在各個 inline comment。 問題
測試覆蓋度的缺口,都補上了
跑過的
設計建議(選 pi 時預設打開「執行工具前先問我」)這次沒有改:
這是取捨,留待決定;要改的話可以另開一個 commit。 Generated by Claude Code |
cyyeh
left a comment
There was a problem hiding this comment.
第二輪 review(c735e8e)
結論:上一輪提出的問題都修好了,也都驗證過,這一輪沒有新的問題。從 review 這邊看,這個 PR 沒有待處理的事項。
上一輪的問題
_abort()沒有 notify_unserved:已修。- 我把 notify 暫時拿掉,
test_the_step_limit_on_the_pages_model_does_not_keep_the_request_past_it_waiting會失敗,整輪跑 10.16 秒;放回去就通過。這個測試確實守住了這個問題。
- 我把 notify 暫時拿掉,
- 端點拒絕不屬於任何一步、而這一輪正在停止的請求(503):這是新加的行為。
- 對照了 pi 1.4.0 的
retryProviderRequest:重試可以被中止,預設maxRetries是 0。 - 就算 abort 送到之前 pi 先收到 503,重試的請求也會立刻再收到 503。
- 這個 turn 的
_open已經是 False,所以那則錯誤訊息不會變成error事件,停止原因仍是max_steps。
- 對照了 pi 1.4.0 的
- 過時的 UI 文字:
PI_INTRO現在依pi_source分成兩種;「用什麼跑迴圈」的說明、page.py 的 docstring、模型卡和工具卡都改了。內容和畫面一致,也有測試檢查內容。 - nit:空行已補上。
這一輪另外看了什麼
98f91a2(How it works 依 harness 分頁)- 逐段讀了所有 pi 分頁的英文說明(overview、submit、prepare、context、model、parse、stop、record、check、execute、observe、ui),都和程式碼一致。
- 在 Chromium 裡實際點了建好的頁面:
pi-page和pi-provider是同層的兩個子分頁,不會互相誤判。- 選其中一個時,只有
simple/pi的分頁組會跟著切到pi。 - 在
pi和simple之間切換時,三個分頁的組會停在選過的子分頁。 - 沒有 JS 錯誤。
- 選其中一個時,只有
- 新測試:網頁模型的允許/拒絕、步數上限(FakePi,以及
RUN_PI_TESTS=1的真 pi)、介紹文字的內容。補上了上一輪指出的缺口。
跑過的
uv run pytest:932 passed, 132 skipped,沒有失敗。skip 比你那邊多,是因為這台機器沒有權重和 sandbox image。- 只用 pygments:
tests/test_guide.py120 passed,python -m app.guide _site建得出來。 - 沒有跑:
RUN_PI_TESTS=1(這個環境裝不了 pi)和RUN_MODEL_TESTS=1(沒有權重),以你回報的結果為準。
設計建議
選 pi 時預設打開「執行工具前先問我」:同意你的理由,兩個 harness 的側欄共用同一個 approve_tools,要改就得先把預設值依 harness 分開。這個建議不擋 merge。
Generated by Claude Code
…milies
The project's two parts are now app/ and agent/. The agent is everything that runs a
turn: the harness that runs its loop, the model's family, what runs the model, and the
tools. The harness is one part of it, beside the model, so the package that holds all of
them is no longer named after it.
- harness/ is agent/, harness/backends/ is agent/model_backends/ and harness/families/ is
agent/model_families/; harnesses/, tools.py, media.py, i18n.py and sandbox/ keep their
names. Imports follow (`from agent import model_families`,
`from agent.model_backends.backend import Backend`), and so does every
`families.X` (`model_families.REVISIONS`, `model_families.VLLM_SPECULATIVE`).
- tests/test_families.py is tests/test_model_families.py.
- The sandbox builds with `docker build -t agent-loop-sandbox agent/sandbox`, which the
sidebar now says too. Only a comment in its Dockerfile changed, so an image built
before still works and SANDBOX_VERSION stays.
- AGENTS.md, both READMEs, How it works and the package docstrings describe the two
parts as app and agent ("The agent never knows the app"). All the guide's code
references point at the new paths. In tests/guide_excerpts.txt every changed line
differs only in a path or module name, which the guide's text was reread and fixed for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cyyeh
left a comment
There was a problem hiding this comment.
Review a8d22f3(harness/ → agent/ 改名)
結論:沒有發現新問題,可以合併。 這個 commit 只改名,行為沒有變。以下是檢查過的項目。
改名是否完整
- 在所有 tracked 檔案裡(包括 workflow、compose、Dockerfile、兩份 README 和 AGENTS.md)搜尋舊的路徑和模組名:
harness/backends、harness/families、from harness、harness.*、families.X、test_families。一個都沒剩。 - 只用 pygments 建出 guide,HTML 裡的 GitHub 連結有 172 個指向
agent/、31 個指向app/、1 個指向tests/,沒有指向harness/的。連結用的是 commit sha,所以合併前也點得開。 tests/guide_excerpts.txt:把舊版套上同一套改名規則再跟新版 diff,只剩一處差異,是一行註解裡的(families/)→(model_families/),一樣是路徑。commit message 說「每一行只差在路徑或模組名」,屬實。
搬家後的路徑
- 用
__file__算路徑的三處,深度都沒變:simple_backend.ROOT和MODELS_DIR、vllm_docker.COMPOSE_DIR(都是parents[2]),以及pi_harness.EXTENSION(with_name)。三處也都有測試把關:MODELS_DIR那個測試會讀根目錄的.gitignore,test_vllm_docker.py從COMPOSE_DIR讀 compose 檔。 - sandbox 的 Dockerfile 和兩個 compose 檔只改了註解,所以
SANDBOX_VERSION和 vLLM CPU image 的 tag 都不用升,之前建好的 image 照樣能用。 agent這個 top-level 名字跟.venv裡的套件不衝突(查過每個套件的top_level.txt和RECORD)。
AGENTS.md 的規則
- 用 AST 掃過
agent/**/*.py,沒有任何檔案 importapp、streamlit或舊的harness。 import agent, agent.model_families, agent.i18n, app之後,torch、transformers、streamlit、httpx、numpy、av 都還沒被載入。
跑過的
uv run pytest:932 passed、132 skipped,一共 1064 個,跟 PR 說明的 985 + 79 一樣多。skip 全部出自這台機器的環境:沒有下載模型、沒有 Docker 和 sandbox image,也沒設RUN_MODEL_TESTS、RUN_PI_TESTS、VLLM_TEST_URL、RUN_VLLM_DOCKER_TESTS。- 只用 pygments 跑
tests/test_guide.py:120 passed。python -m app.guide _site建得出zh/、en/和index.html。 - 沒跑到的:真的 pi(這個環境不允許從 npm 安裝)、真模型、chat template(模型沒下載),還有 Docker 和 vLLM。PR 說明寫了改名後這些都跑過,而這個 commit 也沒動到它們的邏輯。
測試覆蓋度
改名本身的覆蓋是夠的:
- 漏改一處舊路徑,import 就會失敗。
- guide 的每個參照由
test_guide.py對到行。 test_tools.py檢查docker build -t agent-loop-sandbox agent/sandbox這個字串。MODELS_DIR和COMPOSE_DIR的位置有測試。
有一個缺口,改名前就存在,不擋這個 PR:AGENTS.md 的「The agent never knows the app」沒有測試把關。
- 「import 不載入重的套件」這條,Pages workflow 只用 pygments 跑
test_guide.py,算是間接檢查到了。 - 「
agent/不 importapp/或 Streamlit」這條,目前只能靠人眼,這次是我手動掃的。
可以考慮加一個小測試:走過 agent/**/*.py 的 AST,斷言沒有 import app 或 streamlit。
nit(可選)
- import 順序:改名前
harness.backends<harness.families<harness.harnesses剛好照字母排。改名後agent.model_*排到了agent.harnesses、agent.i18n前面,有 15 個檔案不再照字母排,例如agent/harnesses/pi_harness.py、app/page.py、tests/test_pi_harness.py。專案沒有 linter,這純粹是外觀,調不調都可以。 - 已經 clone 的人:pull 之後,git 忽略的
harness/**/__pycache__/會留在原地,看起來像還有一個空的harness/資料夾。沒有程式會 import 它,Python 也不會載入__pycache__裡沒有原始檔的.pyc,所以無害,只是可能讓人困惑。
Generated by Claude Code
…nt never imports the app The rename to agent/ left 22 files' imports out of order (agent.model_* now sorts before agent.harnesses), nothing checked the rule that agent/ never imports app/ or Streamlit, and there was no linter or type checker at all. Now: Lint - Ruff, set up in pyproject.toml: pyflakes and pycodestyle's errors, import order, bugbear and pyupgrade. No formatter, so it never rewrites a line it has nothing to say about; zip()'s strict= and named lambdas are left off, with the reason beside them. - Imports sorted everywhere (tests/'s helpers sort with agent and app, as src), three unused imports gone, Callable and Iterator from collections.abc, two loop variables marked unused. - An f-string in test_render.py reused its quotes, which only Python 3.12 parses, though requires-python says 3.11. Types - mypy, at its default strictness, with the bodies of agent/'s and app/'s unannotated functions checked too (SimpleHarness.run, the page); the tests only where annotated. Stubs for PyYAML, Pygments and openpyxl in the dev group. - No behavior changes. SimpleBackend, VLLMBackend and FakeLLM were not Backends to a type checker: each typed its drafter (and VLLMBackend its drafter_id) as None alone. Encoded.ids is Any, the form each backend's generate() takes; pick() is generic, as the guide already called it with tuples. The rest is annotations on empty dicts and lists, Sequence where a default is (), asserts where a value is known not to be None (a tool call without errors has a tool, generate() ends with done, Popen's pipes), PiHarness.pi for the pi run() started, and format_func=names.__getitem__, which returns a str where dict.get may not. CI - .github/workflows/checks.yml runs ruff check, mypy and uv run pytest on Ubuntu on every push and pull request, each even when the one before failed. The agent never knows the app - tests/test_layout.py reads every file of agent/ for an import of app or streamlit, at the top or inside a function, and checks that the check itself sees each way of importing. Also - .gitignore: .mypy_cache/, .ruff_cache/ and .impeccable/. - .nvmrc picks Node 22 for pi with nvm (`nvm use`), which the READMEs' pi section and AGENTS.md now say; nvm keeps global packages per version, so pi is installed under it. - The READMEs tell a clone from before the rename that the empty-looking harness/ it keeps is only the __pycache__ folders git ignores, which `git clean -fdX harness` removes. - AGENTS.md and both READMEs list the commands, the workflow and the new test. - How it works: the guide's text was reread for every changed excerpt (annotations, a renamed local, the drafting condition now `mtp is not None`); none of it became untrue, and tests/guide_excerpts.txt is rewritten. One reference follows the line it points at. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cyyeh
left a comment
There was a problem hiding this comment.
Review b8e5ff1(ruff、mypy、CI 和 tests/test_layout.py)
結論:沒有發現新問題,可以合併。 上次 review 提的三點都處理了。commit 說「沒有行為改變」,我逐處對過,屬實。最後有一個建議和兩個 nit,都不擋合併。
上次提的三點
- import 順序:ruff 的
I規則加上 CI,以後順序亂了就會被抓到。 - 「agent 不 import app」沒有測試:
tests/test_layout.py補上了。- 它也測了檢查本身(
test_the_check_sees_every_way_of_importing)。 - 寫在函式裡的 import 抓得到;relative import 則跳過,這沒問題,因為它跑不出
agent/。
- 它也測了檢查本身(
- 舊 clone 留下的
harness/:README 裡的git clean -fdX harness,我在一個暫時的 repo 裡試過。它會刪掉整個harness/,而且只刪 git 忽略的檔案。
「沒有行為改變」:逐處確認
為了型別而改的地方,凡是可能改到行為的,我都對過:
SimpleBackend.generate():if drafting改成if mtp is not None。mtp只有在drafting為真時才是 dict,兩者等價,解碼迴圈不變。_encoded_media()多了encoder and:encoder是 None 時,args本來就是空的,結果一樣。- 新加的 assert:
assert done is not None(SimpleHarness 和 model_endpoint)、assert tool is not None。- 前者只在
generate()的迴圈正常結束後才執行到。 - 後者只在
errors是空的時候才執行到;而tool is None一定會產生錯誤。 - 萬一 assert 失敗,
AssertionError一樣會被except Exception接住、送出error事件,跟原本的AttributeError走同一條路。
- 前者只在
PiHarness.pi:改成self.pi的地方,都在run()建好Process之後才會執行;finally裡還是用self._pi判斷。_message_end的stopReason or "":reason只拿來比較和查表,None和""都會落到"end_turn"。PiAgent.model從None改成{}:用到它的地方都在if who.llm的 else 分支,那時一定有 pi 的模型。loop_panel多了or run is None:因為data = run.data() if run else None,data不是 None 時run一定也不是,所以等價。format_func=X.__getitem__:選項都是list(X),不會 KeyError。guide.statement_end():ast.parse產生的節點都有end_lineno,結果不變。只用 pygments 建 guide 也通過。
跑過的
- ruff:
uv sync --locked之後,uv run ruff check→ All checks passed。 - mypy:
uv run mypy→ no issues found in 44 source files。這 44 個正好是全部:agent/18、app/4、tests/21,加上streamlit_app.py。整個 repo 沒有任何type: ignore。 - pytest:
uv run pytest952 passed、132 skipped。- 比上次多 20 個,正好是
test_layout.py的 18 + 2。 - skip 的跟上次一樣,全是這台機器的環境:沒有模型、沒有 sandbox image、沒有 pi。
- 比上次多 20 個,正好是
- guide:只用 pygments,
tests/test_guide.py120 passed,guide 也建得出來。 - CI:這個 commit 的兩個
check都成功,各約 3.5 分鐘。
建議:讓 checks.yml 也只用 pygments 跑一次 guide 的測試
AGENTS.md 還有一條規則:agent/__init__.py、agent/model_families/、agent/i18n.py 和 app/__init__.py 不能 import 重的套件。目前只有 pages.yml 檢查它,而 pages.yml 只在 push 到 main 時跑。所以違反這條規則的 PR 會通過 checks.yml,合併之後 Pages 的建置才會失敗。
我在一個 worktree 的 agent/i18n.py 加了一行 import httpx 試過:
- 裝了所有套件(checks.yml 的環境):
tests/test_guide.py通過。 - 只有 pygments(pages.yml 的環境):
ModuleNotFoundError: No module named 'httpx'。
在 checks.yml 加一個 step,合併前就能抓到:
- if: ${{ !cancelled() }}
run: uv run --no-project --with pygments --with pytest pytest tests/test_guide.py這個 step 不靠 uv sync,所以不用等 sync 成功。本機跑了約 90 秒。
nit(可選)
- CI 每次 push 跑兩次:
on同時有push(所有分支)和pull_request,concurrency group 又含event_name,兩個 run 不會互相取消。所以 push 到 PR 分支時,同樣的 job 會跑兩次,每次都裝一次 torch;這個 commit 就有兩個check。把push限制在branches: [main],就只會跑一次。 .nvmrc寫的是22:pi 1.4 要 Node 22.19 以上,但nvm use會挑已經裝好的最新 22.x。如果那個版本比 22.19 舊,pi 就跑不起來。README 改寫成nvm install會比較穩:它會照.nvmrc裝好最新的 22.x 再切過去。
Generated by Claude Code
…ach push once - checks.yml has a guide job that runs what pages.yml runs before it publishes How it works: tests/test_guide.py and the build, with pygments and pytest alone. The check job installs every package, so a pull request that had app/guide.py, app/render.py, agent/i18n.py or agent/model_families/ import torch, transformers, streamlit or httpx passed it, and only the Pages build on main failed, after the merge. - Both workflows run those commands with --isolated. Without it, uv reuses a .venv that already has pygments, and with it the whole app, so the same commands run by hand in a synced checkout passed with `import httpx` in agent/i18n.py. A fresh checkout in CI has no .venv; the flag makes a run by hand mean the same. AGENTS.md lists the command. - checks.yml runs on pushes to main only, besides pull requests. A push to a pull request's branch ran every job twice, once per event (the concurrency group differs by the event, so neither cancelled the other), and installed torch twice. A push to a branch without a pull request is no longer checked. - The READMEs say `nvm install` instead of `nvm use`: .nvmrc names 22, and nvm use picks the newest 22 already installed, which can be older than the 22.19 pi 1.4 needs; nvm install installs the newest 22 and switches to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
謝謝 review。建議和兩個 nit 都對照過,全部確認,a8232b9 修正。 建議:checks.yml 也只用 pygments 跑 guide 的測試
nit
AGENTS.md 和兩份 README 講 CI 什麼時候跑、跑什麼的段落都跟著更新了。 跑過的
Generated by Claude Code |
What changes for the user
The sidebar has a new switch at the top, What runs the loop: the built-in harness, as before, or pi, a coding agent. With pi picked, The model pi asks chooses which model pi asks:
Either way, each step shows the request pi sent the model: pi's system prompt, its tools and the conversation as JSON, with what the step added marked.
How
One interface for whatever runs the loop.
agent/harnesses/harness.pydefinesHarness(run,stop,decide) and which events every harness sends (ALWAYS) and which only one that knows them (SOMETIMES).harness/agent.pymoves toagent/harnesses/simple_harness.pyandRunbecomesSimpleHarness, to sit besideSimpleBackend;run_startnow says which harness ran the turn (simpleorpi).LiveRuntakes a function that makes the harness (harness_for()).pi.
agent/harnesses/pi_harness.pystarts pi in RPC mode for each question (pi --mode rpc, JSONL on stdin and stdout):set_modelgoes to the old model);message_endbecomesparsed, tool executions becometool_call,tool_startandtool_result; on a provider's model,message_updatebecomes a new event,delta(text, thinking and tool-call arguments by channel), andmessage_endalsomodel_endwith the provider's counts;abort, and a pi that does not settle is killed. An abort also wakes a request to the page's model that waits for its step, and the endpoint turns away a request that belongs to no step once the turn is being stopped, so the model is free for the next question at once.The extension.
agent/harnesses/pi_extension.tsis loaded into every pi the page starts. It reports each request pi sends the model (before_provider_request, as a notification, which in RPC mode is anextension_ui_requeston stdout) and becomes a new event,request; it registers the page's model as the provideragent-loop; and with Ask me before running a tool it opens a confirm dialog before every tool call, which the page answers on stdin.The page's model for pi.
agent/harnesses/model_endpoint.pyserves the model in memory as an OpenAI-compatible chat completions endpoint on 127.0.0.1, behind a token, only while a turn is served, one request at a time. Each request becomes the conversationSimpleHarnesskeeps (arguments as dicts, thinking under the family's key, tool results named), is rendered with the chat template, encoded and generated; itscontext,prefill,tokenandmodel_endevents go into pi's step. A request waits for the step pi opened for it, since pi's records and its HTTP request reach the page by different ways. The reply streams back to pi: thinking asreasoning_content, the reply ascontent, the tool calls the family parses astool_calls. Decoding is the sidebar's; Thinking mode reaches the model as pi's thinking level, which pi sends aschat_template_kwargs.enable_thinking.app + agent. With more than one harness, the package that runs a turn is no longer named after one of its parts:
harness/isagent/, besideapp/. The agent is the harness that runs its loop, the model's family, what runs the model and the tools:agent/harnesses/(theHarnessinterface,SimpleHarness,PiHarness, the pi extension and the model endpoint),agent/model_backends/(washarness/backends/),agent/model_families/(washarness/families/), andtools.py,media.py,i18n.pyandsandbox/as before. Imports follow (from agent import model_families), the sandbox builds withdocker build -t agent-loop-sandbox agent/sandbox(an image built before still works),tests/test_families.pyistests/test_model_families.py, and AGENTS.md, the READMEs, How it works and the docstrings describe the two parts as app and agent.The page. With pi picked, the intro on the Loop tab says what shows of the model pi asks: every token on this page's model, text and no probabilities on pi's own.
render.pydraws a run without a family, a context or tokens, and a step'srequest(in the step card when there is no context, and folded under the context in the Context tab when there is). The Model tab shows the page's model card, or pi's model's.Docs. How it works tells the harnesses apart with the same tabs as the backends and the models: a new kind, What runs the loop, with a tab for the built-in harness and one for pi, and, where pi asking this page's model and pi asking one of its own differ,
pi-pageandpi-provideras narrower kinds ofpi(asvllm-dockeris ofvllm). Picking one switches every harness tab on the page. Twelve of the 18 sections have them (overview, sending a question, preparing a run, context, model, parse, stop, writing the call back, checks, environment, observation, the page), with pi's code in its tabs; what the built-in harness and pi on this page's model share (the chat template, the parsers, the backends) stays outside them, since tabs do not nest. The event table has thedeltaandrequestrows. The READMEs (both languages) get "pi as the harness", and the layout, event list, tests table and known limitations follow. AGENTS.md has a rule for pi and the endpoint.docs/screenshot-en.pnganddocs/screenshot-zh.pngare retaken from a real Gemma run (on MPS), with What runs the loop at the top of the sidebar.Checks. Ruff lints and mypy checks types, both set up in
pyproject.toml(no formatter), and.github/workflows/checks.ymlruns them anduv run pyteston Ubuntu on every push and pull request. The rename had left 22 files' imports out of order, which ruff now sorts and keeps sorted. Bringing mypy to zero changed no behavior: each backend (andFakeLLM) typed its drafter asNonealone, so none of them was aBackendto a type checker; the rest is annotations, narrowing where a value is known not to beNone, a genericpick(), andformat_func=names.__getitem__, which returns astrwheredict.getmay not.tests/test_layout.pychecks the rule that nothing inagent/importsappor Streamlit, at the top or inside a function. A clone from before the rename keeps an empty-lookingharness/of the__pycache__folders git ignores; the READMEs saygit clean -fdX harnessremoves it..nvmrcpicks Node 22 for pi with nvm.Testing
uv run ruff checkanduv run mypyreport nothing;uv run pytest1005 passed, 79 skipped (the 20 new are test_layout.py);RUN_MODEL_TESTS=1 uv run pytest tests/test_real_model.py tests/test_chat_template.py102 passed, 5 skipped, since the commit touches simple_backend.py and media.py, if only in annotations; the guide builds andtests/test_guide.pypasses with pygments alone, after rereading every section whose quoted code changeduv run pytest: 985 passed, 79 skipped (the usual real-model, real-server and Docker ones, plus the real-pi tests), before and after the rename toagent/tests/fake_pi.pyspeaks pi 1.4's RPC protocol from a script and, for the page's model, asks the endpoint over HTTP as pi does, so the default suite runs pi on both kinds of model with neither Node nor a model: test_pi_harness.py, test_model_endpoint.py (the endpoint over HTTP with FakeLLM behind it), pi scenarios in test_render.py (every prefix renders, escaping, an English page with no Chinese) and test_app.pyRUN_PI_TESTS=1 uv run pytest tests/test_pi_harness.py: all 46 pass with the real pi 1.4.0 (Node 22.22), again after the rename, against a scripted OpenAI-compatible model on 127.0.0.1 and against the endpoint with FakeLLM behind it (thinking on and off, and asking first up to the step limit)RUN_MODEL_TESTS=1 uv run pytest tests/test_real_model.py, before the review fixes, which touch neither parsing nor prompts nor decoding (real Gemma 4 E2B with its drafter, and Qwen3.5 0.8B, on an Apple M5, with the real pi): 62 passed, 3 skipped; the new test has the real pi ask each model through the endpoint, call bash and answer. After the rename:RUN_MODEL_TESTS=1 uv run pytest tests/test_real_model.py tests/test_chat_template.py tests/test_tools.py, 212 passed, 3 skipped, with the sandbox image built (the two real-pi tests rerun after putting pi 1.4.0 back on the machine, where they had failed on an older pi 0.74.2 that rejects--session-id)tests/test_guide.pypasses that way too; the excerpts intests/guide_excerpts.txtwere rewritten after rereading every section they named, and in the rename every changed line differs only in a path or module nameNot tested: vLLM in Docker and against a real server after the rename (
RUN_VLLM_DOCKER_TESTS,VLLM_TEST_URL; the default suite fakes both); pi against a real provider (Claude, GPT…), which needs credentials and costs money; Gemma 4 on vLLM in Docker, whose first start on a CPU takes over ten minutes.Known limits
🤖 Generated with Claude Code