RuntimeLoop 44 参数构造函数收进 config dataclass(不拆类;排 #97 之后) #105

Closed
opened 2026-07-30 07:25:38 +00:00 by KumaAgent · 0 comments
Member

2026-08-14 复核(评估侧):#97 已合并(PR #110),_build_runtime_loop 按本票预期搬到了 loop_factory.py,该文件模块级 docstring 现直接点名"构造函数那 44 个参数的收拢是 issue #105"——交接干净,无需拆片,改挂 状态:智能体就绪。对照当前 main(commit 23aa13d)逐条重跑了本票的实测断言:

  • 2 个构造点、深模块判断、reflection.ReflectionCycleConfig 先例、make_loop 的 tuple 签名与"可选 kwarg 兜底"形状、ADR-0010「可测性」(原文"时钟/随机/存储可注入")——全部仍然成立,已就地更正下方随时间漂移的数字(44→42/43、16→20、__init__.pyloop_factory.py)。
  • 发现一处会误导实现的分类错误_delegate_llm_client 被原 Problem 段当作"单方法读的调参常量"例子之一,但它类型是 LLMClient(真实依赖,构造函数注释明写"必填、不给缺省"),混进 config dataclass 会撞 AC 自己那条"真实依赖不塞 config"的边界。已从例子里摘掉,且把 AC 的排除清单从按名字枚举改成按类型判断——因为同一个漏洞不止这一处:report_recalled_events/report_pending_intents(回调)、interaction_renderers(host 注入的 registry)都是"只被一个方法读"但不该进 config 的类型,纯按读取次数筛会连着一起吸进去。见下方 AC 新增的判断规则。

Problem

RuntimeLoop42 个方法、43 个构造参数(全部 keyword-only,构造函数本身约 100 行纯搬运 self._x = x)。

其中一大批字段只被恰好一个方法读(实测,精确名单以实现期按下方类型规则过滤后为准):_typing_base_delay/_typing_per_char_delay/_typing_energy_jitter 只给 _typing_delay_recall_half_life_seconds/_recall_weights 只给 _scored_events_familiarity_* 三个只给 run_delegate_max_rounds 只给 _run_delegated_task……

它们不是对象状态,是被提升到构造函数的 config 常量。 这是 Ousterhout 意义上的宽接口——一个 43 参数的构造函数,是这个类对外最宽的那一面。

为什么这不是「拆 RuntimeLoop」

明确不拆类。 结构实测的结论是 RuntimeLoop真深模块6 个公开方法 / 42 个方法run / run_callback / run_light / run_environment_signal / run_immediate_followup / assemble_context,其余 36 个只被类内调用);16 个工具 handler 共享同一个 _TurnState,而那是 ADR-0012 pre-send regime 的真不变量send_message 本轮只入队不发,同轮 self_recall/edit 要在真发出前拦下改写)。拆开就得把 _TurnState + _storage + _egress + _capabilities 全部跨新边界传——把深模块换成浅模块。三份独立评估方案在这一点上罕见地一致。

本票只收窄构造接口,一行业务逻辑都不动。

爆炸半径(已实测,很小)

RuntimeLoop( 全仓只有 2 个构造点

位置 说明
src/nonebot_plugin_arise/loop_factory.py_build_runtime_loop 内) 唯一生产构造点(issue #97 落地后从 __init__.py 搬到这里,模块 docstring 已点名本票)
tests/runtime_loop_helpers.pymake_loop 内) 唯一测试构造点

20 个测试文件用的是 make_loop不直接构造 RuntimeLoop。所以改动 = 签名 + 2 个调用点 + 内部读改成 self._config.x,机械且可控。

Acceptance criteria

  • 把「只被一个方法读、且本质是静态配置」的构造参数收进一个 config dataclass(命名与分组由实现期定;可参考 reflection.ReflectionCycleConfig 既有先例——那次正是把可调参数收进 config 对象)。
  • 判断一个参数该不该进 config,按类型判断,不能只按"读取次数"判断(2026-08-14 复核发现纯读取次数筛会连带吸入不该收的类型):
    • 不进 config:类型是端口/客户端/回调/registry 的,即便只被一个方法读,也保持独立参数。已知清单:llm_client / egress / storage / embedding_client / persona / get_tools / sleep / silence_window / delegate_task_registry / delegate_llm_client(原例子誤把它和 delegate_max_rounds 并列,它是 LLMClient 依赖,不是调参常量)/ report_recalled_events / report_pending_intents(回调)/ interaction_renderers(host 注入的 registry,tag_renderers 同理但它是多方法读,本来就不在候选里)。
    • 进 config:从 Config/env 派生的标量、frozenset,以及像 recall_weightsfallback_multimodal_models 这样由 config 组装出的复合值对象。
    • 实现期自行判断、不必逐字照抄上面清单:清单本身就是本次复核才补全的(发现时已经漏了一个),构造函数以后还会长参数,靠类型判断比靠名字枚举更不容易再漏。
  • RuntimeLoop6 个公开方法签名一行不改run / run_callback / run_light / run_environment_signal / run_immediate_followup / assemble_context);42 个方法的行为一行不改。
  • 两个构造点同步更新;make_loop 保持既有的「可选 kwarg + 内部默认值兜底」形状,不改变它返回的元组签名(issue #55 建立的先例:给 RuntimeLoop 加必需参数时,测试侧靠 make_loop 兜底,20 个测试文件不受影响)。
  • 全量测试通过,且不新增/不改写任何既有断言——本票是纯结构收窄,若有既有测试需要改断言,那说明行为变了,停下来查。

Not in scope

  • 不拆 RuntimeLoop(理由见上,三份评估一致)。
  • 不动 storage.py(#97 已判:只买可导航性,不够格;重评触发条件见 #97 评论)。
  • 不碰 __init__.py 的拆分本身(#97 的任务一,已落地)。
> **2026-08-14 复核(评估侧)**:#97 已合并(PR #110),`_build_runtime_loop` 按本票预期搬到了 `loop_factory.py`,该文件模块级 docstring 现直接点名"构造函数那 44 个参数的收拢是 issue #105"——交接干净,无需拆片,改挂 `状态:智能体就绪`。对照当前 `main`(commit 23aa13d)逐条重跑了本票的实测断言: > > - 2 个构造点、深模块判断、`reflection.ReflectionCycleConfig` 先例、`make_loop` 的 tuple 签名与"可选 kwarg 兜底"形状、ADR-0010「可测性」(原文"时钟/随机/存储可注入")——**全部仍然成立**,已就地更正下方随时间漂移的数字(44→42/43、16→20、`__init__.py`→`loop_factory.py`)。 > - **发现一处会误导实现的分类错误**:`_delegate_llm_client` 被原 Problem 段当作"单方法读的调参常量"例子之一,但它类型是 `LLMClient`(真实依赖,构造函数注释明写"必填、不给缺省"),混进 config dataclass 会撞 AC 自己那条"真实依赖不塞 config"的边界。已从例子里摘掉,且把 AC 的排除清单从**按名字枚举**改成**按类型判断**——因为同一个漏洞不止这一处:`report_recalled_events`/`report_pending_intents`(回调)、`interaction_renderers`(host 注入的 registry)都是"只被一个方法读"但**不该**进 config 的类型,纯按读取次数筛会连着一起吸进去。见下方 AC 新增的判断规则。 ## Problem `RuntimeLoop` 有 **42 个方法、43 个构造参数**(全部 keyword-only,构造函数本身约 100 行纯搬运 `self._x = x`)。 **其中一大批字段只被恰好一个方法读**(实测,精确名单以实现期按下方类型规则过滤后为准):`_typing_base_delay`/`_typing_per_char_delay`/`_typing_energy_jitter` 只给 `_typing_delay`;`_recall_half_life_seconds`/`_recall_weights` 只给 `_scored_events`;`_familiarity_*` 三个只给 `run`;`_delegate_max_rounds` 只给 `_run_delegated_task`…… **它们不是对象状态,是被提升到构造函数的 config 常量。** 这是 Ousterhout 意义上的宽接口——一个 43 参数的构造函数,是这个类对外最宽的那一面。 ## 为什么这不是「拆 RuntimeLoop」 **明确不拆类。** 结构实测的结论是 `RuntimeLoop` 是**真深模块**:**6 个公开方法 / 42 个方法**(`run` / `run_callback` / `run_light` / `run_environment_signal` / `run_immediate_followup` / `assemble_context`,其余 36 个只被类内调用);16 个工具 handler 共享同一个 `_TurnState`,而那是 ADR-0012 pre-send regime 的**真不变量**(`send_message` 本轮只入队不发,同轮 `self_recall`/`edit` 要在真发出前拦下改写)。拆开就得把 `_TurnState` + `_storage` + `_egress` + `_capabilities` 全部跨新边界传——**把深模块换成浅模块**。三份独立评估方案在这一点上罕见地一致。 **本票只收窄构造接口,一行业务逻辑都不动。** ## 爆炸半径(已实测,很小) `RuntimeLoop(` 全仓**只有 2 个构造点**: | 位置 | 说明 | |---|---| | `src/nonebot_plugin_arise/loop_factory.py`(`_build_runtime_loop` 内) | 唯一生产构造点(issue #97 落地后从 `__init__.py` 搬到这里,模块 docstring 已点名本票) | | `tests/runtime_loop_helpers.py`(`make_loop` 内) | 唯一测试构造点 | 20 个测试文件用的是 `make_loop`,**不直接构造 `RuntimeLoop`**。所以改动 = 签名 + 2 个调用点 + 内部读改成 `self._config.x`,机械且可控。 ## Acceptance criteria - [ ] 把「只被一个方法读、且本质是静态配置」的构造参数收进一个 config dataclass(命名与分组由实现期定;可参考 `reflection.ReflectionCycleConfig` 既有先例——那次正是把**可调参数**收进 config 对象)。 - [ ] **判断一个参数该不该进 config,按类型判断,不能只按"读取次数"判断**(2026-08-14 复核发现纯读取次数筛会连带吸入不该收的类型): - **不进 config**:类型是端口/客户端/回调/registry 的,即便只被一个方法读,也保持独立参数。已知清单:`llm_client` / `egress` / `storage` / `embedding_client` / `persona` / `get_tools` / `sleep` / `silence_window` / `delegate_task_registry` / **`delegate_llm_client`**(原例子誤把它和 `delegate_max_rounds` 并列,它是 `LLMClient` 依赖,不是调参常量)/ `report_recalled_events` / `report_pending_intents`(回调)/ `interaction_renderers`(host 注入的 registry,`tag_renderers` 同理但它是多方法读,本来就不在候选里)。 - **进 config**:从 `Config`/env 派生的标量、`frozenset`,以及像 `recall_weights`、`fallback_multimodal_models` 这样由 config 组装出的复合值对象。 - **实现期自行判断、不必逐字照抄上面清单**:清单本身就是本次复核才补全的(发现时已经漏了一个),构造函数以后还会长参数,靠类型判断比靠名字枚举更不容易再漏。 - [ ] `RuntimeLoop` 的 **6 个公开方法签名一行不改**(`run` / `run_callback` / `run_light` / `run_environment_signal` / `run_immediate_followup` / `assemble_context`);42 个方法的行为一行不改。 - [ ] 两个构造点同步更新;`make_loop` 保持既有的「可选 kwarg + 内部默认值兜底」形状,**不改变它返回的元组签名**(issue #55 建立的先例:给 `RuntimeLoop` 加必需参数时,测试侧靠 `make_loop` 兜底,20 个测试文件不受影响)。 - [ ] 全量测试通过,且**不新增/不改写任何既有断言**——本票是纯结构收窄,若有既有测试需要改断言,那说明行为变了,停下来查。 ## Not in scope - **不拆 `RuntimeLoop` 类**(理由见上,三份评估一致)。 - **不动 `storage.py`**(#97 已判:只买可导航性,不够格;重评触发条件见 #97 评论)。 - 不碰 `__init__.py` 的拆分本身(#97 的任务一,已落地)。
Yushu closed this issue 2026-08-11 03:54:38 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ProjectKuma/arise#105
No description provided.