Skip to content

feat(proactive): draft of upgrading proactive feature with disentangled job - #488

Open
imrewce wants to merge 14 commits into
agentscope-ai:mainfrom
imrewce:refractor/proactive
Open

feat(proactive): draft of upgrading proactive feature with disentangled job#488
imrewce wants to merge 14 commits into
agentscope-ai:mainfrom
imrewce:refractor/proactive

Conversation

@imrewce

@imrewce imrewce commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

add new proactive cron job consisting of idle sensing/ extracting / topics filtering steps and structured proactive schema

Related issue

Contract and data impact

  • No public configuration, schema, CLI, endpoint, streaming, or workspace-layout contract changes
  • No user-owned memory files are deleted or rewritten
  • Derived indexes, catalogs, graphs, caches, and metadata remain rebuildable

Validation

  • Focused tests pass
  • Unit tests pass, or omitted tests are explained below
  • pre-commit run --all-files passes, or omitted checks are explained below
  • Frontend checks were run when website/ changed

Checklist

  • I reviewed the diff for unrelated changes and sensitive data
  • Tests cover intentional behavior changes
  • Defaults, schemas, and concise documentation were updated together when required
  • Long-lived clients, tasks, services, and executors follow the application lifecycle

Screenshots or additional notes

@imrewce
imrewce requested a review from jinliyl August 24, 2026 10:08
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@imrewce imrewce changed the title refractor(proactive): draft of upgrading proactive feature with disentangled job feat(proactive): draft of upgrading proactive feature with disentangled job Aug 24, 2026
Comment thread reme/components/job/base_job.py Outdated
@@ -1,5 +1,6 @@
"""Base job component for sequential step execution."""

import time

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qwenpaw的跨agent的问题

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authentic daily link original sessions -- followup detection

Comment thread reme/config/default.yaml Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

没删除

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here is the proactive job for the reading side passing interest.md to upper layer application layer, so i suppose it is supposed to be deleted.

Comment thread reme/config/default.yaml Outdated
tool_result_limit: 50000
model_config:
max_retries: 1
proactive:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里不应该有

Comment thread reme/schema/proactive.py
return 0.5


class ProactiveTopic(BaseModel):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

如果是结构化输出 可以简洁一点

@jinliyl

jinliyl commented Aug 27, 2026

Copy link
Copy Markdown
Member

感谢这次 proactive 功能的重构。整体方向是清楚的:把 idle sensing、素材提取、topic 处理和 catalog checkpoint 拆开之后,职责比原来的单一步骤更容易理解;测试覆盖也比较充分。我仔细看了完整 diff、相关 Job 生命周期、默认配置和 proactive 的 LLM/文件处理链路。目前 GitHub 上的 Python 3.11/3.12/3.13 unit tests、pre-commit、CLI smoke 和 CodeQL 都已通过。

不过,从正确性和后续维护成本来看,我建议合并前再收敛一轮,重点如下。

1. LLM 输出需要真正进行严格校验,否则可能错误 checkpoint

PR 新增了 ProactiveExtractOutput,但实际执行链没有使用它做校验。当前 parse_extract_reply() 只要解析到任意非空 mapping 就返回成功;例如模型返回:

followups:
  - title: ...

即使顶层字段拼错,它仍然是一个非空 dict,因此不会触发 parse retry。后续 _clean_output() 得到空结果,topics/finish 仍可能继续执行,并把本轮 changed files 写入 proactive catalog。这样同一批素材在 mtime 再次变化之前不会重试,可能永久漏掉本轮 topic。

建议:

  • 对 LLM 输出使用一个真正严格、最小化的 schema;
  • 顶层字段必须明确存在,未知字段应拒绝,而不是通过默认值静默吞掉;
  • schema/字段验证失败时才重试;
  • 只有成功解析并完成确定性处理后才 checkpoint;
  • 增加“非空但 schema 错误的 YAML”测试,而不只是测试完全无法解析的文本。

需要注意的是,当前 ProactiveExtractOutput 的所有字段都有默认值,Pydantic 默认还会忽略 extra,因此简单调用 model_validate({"foo": 1}) 依然可能得到一个空但合法的结果,需要同步收紧 schema。

2. BaseJob/CronJob 的 idle sensing 建议进一步解耦和精简

这里我认同“Job 只是辅助感知其他状态”的思路,但目前 _record_call() 从“增加调用计数”变成了“增加计数并标记 running”,等于改变了整个 Job 继承树的基础语义,容易产生遗漏。

目前有两个具体问题:

  1. 同一个 BaseJob 可以通过 HTTP 并发调用,但 running 是单个 bool。两个 auto_memory 同时执行时,第一个完成就会写 running=False,即使第二个还在运行,idle gate 也可能错误放行 proactive。
  2. StreamJob.__call__()BackgroundJob.__call__() 都会调用 _record_call(),但没有成对调用 _finish_call()。如果自定义 busy_job_patterns 匹配这些 Job,它们可能在第一次执行后永久显示为 busy。

建议把这部分收敛成一个很薄的 JobActivityTracker(放在 ApplicationContext 或作为明确的 context 字段),内部至少使用:

{
    "active_count": 2,
    "last_start": ...,
    "last_end": ...,
}

执行开始时 active_count += 1,结束时 active_count -= 1,只有从 1 变成 0 时才记录 last_end。所有 Base/Stream/Background/Cron 的真实执行边界通过同一个 context manager 或统一 helper 成对调用,并保护共享状态更新。

这样 BaseJob 可以继续专注于构造 RuntimeContext 和顺序执行 steps;CronJob 只保留 cron 调度、stop event 和异常日志,单次 step 执行可以复用公共 _execute_steps(context),不需要分别维护 _record_call() / _finish_call()

对应测试建议覆盖:

  • 同一 BaseJob 的两个并发调用;
  • StreamJob 正常、异常、取消后的状态清理;
  • Background/Cron 正常退出和异常退出后的状态清理;
  • quiet window 只在最后一个 active invocation 结束后开始计算。

3. default.yaml 建议精简,并考虑让新的 cron 显式启用

当前修改会让所有使用默认配置的升级用户自动增加每天 4 次 proactive LLM 调用,解析失败时每轮还可能重试;同时会自动新增 _proactive.yaml 并改变 interests.yaml 的生成节奏。这是实际的默认行为、成本和 workspace 输出变化,建议不要在没有显式选择的情况下直接开启。

建议:

  • 优先放进单独的 proactive 配置变体,或者增加明确的 enable 开关;
  • 默认配置只保留 cron、必要组件引用和真正需要用户调整的参数;
  • scan_dayscarry_forward_daysmax_topics 等已经有稳定构造器默认值的参数,可以避免在 YAML 再重复一遍,减少两处默认值漂移;
  • 独立 file_catalog.proactive 有独立 watermark 的语义,可以保留;
  • agent_wrapper.proactive 建议删除。这里是一次 extraction,不需要独立的 ReAct agent;可以直接复用默认 wrapper,或者更直接地调用 LLM structured output;
  • 如果最终保留默认启用,需要在 PR 描述和 proactive 文档中明确说明调用频率、成本、生成文件、升级行为和关闭方式。

4. LLM 结构化输出可以显著简化

当前要求 LLM 输出 kind、浮点 confidenceevidencepathskeywords,还要求对每个 carry-forward topic 显式返回 keep。其中很多字段可以由代码确定或派生;输出字段越多、列表越长,格式失败和局部字段错误的概率越高。

可以考虑收敛为类似:

topics:
  - type: follow_up
    title: ...
    reason: ...
    path: ...
    strength: intent

updated:
  - id: ...
    path: ...
    strength: explicit

resolved:
  - topic-id

然后由代码完成:

  • carry-forward topic 未出现在 updated/resolved 中时默认视为 keep,不要求模型重复输出;
  • 根据 type 填充 kind
  • 根据 title 生成稳定 id;
  • 根据合法 path 填充 evidencepaths
  • 根据本轮日期填充 first_seen / last_evidence_at
  • explicit / intent / inferred / weak 映射为 0.9 / 0.7 / 0.5 / 0.3,避免模型自由生成浮点数;
  • keywords 可以删除,或在代码侧从 title/reason 派生;
  • 路径白名单、去重、合并、过期、排序和 push 都继续由确定性代码处理。

这样 LLM 只负责语义判断,代码负责结构拼装和规则执行,会更符合这里的可靠性目标。

5. 增加整个 prompt 的总预算

现在 pack_paths() 只有 limit_per_file,没有总字符/token 上限。默认每文件最多 60,000 字符,最多 20 个 resource 文件,再加上数量未受限制的 daily 文件,仅 resource 就可能达到约 120 万字符,超过默认模型的 200k context。

建议增加 max_total_chars 或 token budget,并明确材料优先级,例如:changed daily → changed resource → recent unchanged resource → carry-forward。达到预算后停止追加,而不是只对每个文件单独截断。

6. 文档和当前 PR 状态

这次新增了 _proactive.yaml truth source、v2 interests.yamlhorizon_daysmin_confidence 和新的周期性写入行为,但当前 changed files 中没有相应的 proactive 文档更新。建议合并前补充稳定用户契约和迁移/关闭说明。

另外 GitHub 当前仍将 PR 标记为 CONFLICTING,需要在以上逻辑调整后再同步解决与 main 的冲突。

总体上,我建议保留“LLM 做语义识别、代码做规则和拼装”的核心方向,但把 LLM contract、Job activity tracking 和默认配置再缩小一些。这样不仅代码量会下降,也能避免模型偶发格式错误变成不可恢复的漏处理。辛苦了,期待下一版。

- rename read-side job 'proactive' -> 'proactive_read' (less confusing vs the refresh pipeline)
- drop dedicated agent_wrapper.proactive; extraction reuses the default wrapper
- simplify schema: remove unused ProactiveExtractOutput/TopicUpdate, drop resource_paths
- extract no longer scans resource/ directly (daily notes already carry resource content)
- update tests and docs accordingly
- parse_extract_reply now requires a contract section (follow_ups/extends/updates
  as a list); non-empty replies with misspelled section names trigger the
  existing one-shot retry instead of silently checkpointing changed files
- pack_paths gains max_total_chars; extract packs newest daily material first,
  keeps the first file on overflow, and records omitted files in a trailer
  (default budget 300000 chars, configurable via max_total_chars)
- tests: schema gate unit, schema-error retry e2e, budget unit + e2e
…LM contract

- extract/plan/agenda now draw a user profile block from <digest_dir>/personal/*.md
  (frontmatter description + body excerpt, per-file budget, profile.md fallback)
- all daily access honours the configured daily_dir (prompt paths parameterized,
  config-driven fallbacks) so workspaces using e.g. memory/ work unchanged
- schema trim: drop dead fields errors/material_paths, carry_forward_all -> count
- shrink LLM output contract: new topics emit title/reason/confidence/paths only;
  keywords removed end-to-end, evidence derived from paths[0] (updates keep it)
…er retry

Two consecutive unparseable replies now short-circuit the round without
checkpointing, so the same material is retried next round instead of being
silently consumed (closes the residual audit agentscope-ai#1 gap: the structural gate
detected schema-wrong output but a double failure still checkpointed).
@imrewce

imrewce commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

Opinion #1 — Strict LLM output validation: ADDRESSED
Removed dead Pydantic models; parse_extract_reply now enforces a structural gate (at least one of follow_ups/extends/updates must be a list). Double parse failure short-circuits the round with no checkpoint upsert, so material is retried next round. Covered by three new tests.

Opinion #2 — Concurrency guard: DESIGNED, DEFERRED
Full design ready (active count, timestamps, context manager, lock); user requested changes stay inside proactive files for now.

Opinion #3 — default.yaml hygiene: PARTIALLY ADDRESSED
Removed redundant agent_wrapper.proactive; new steps declared without duplicated defaults. Cron reduced from 4×/day to once daily at 18:00. Disable how-to paragraph still pending.

Opinion #4 — Leaner structured output: PARTIALLY ADDRESSED
Keywords removed end-to-end; new-topic surface trimmed from 6 → 4 fields (title/reason/confidence/paths). Deferred: strength enum (real runs only emit 0.5/0.7/0.9, low risk) and implicit-keep for carry-forward topics.

Opinion #5 — Total prompt budget: ADDRESSED
pack_paths gains max_total_chars (default 300k), newest-first priority, and an omission note. Material now scans daily notes only, eliminating the ~1.2M-char resource risk. Two new tests.

Opinion #6 — Docs & PR state: PARTIALLY ADDRESSED
Proactive docs and READMEs updated for renames and agenda field. Pending: migration/opt-out section for _proactive.yaml and upstream conflict re-sync.

Beyond the audit: generative scenario-card plan step + list-wise agenda with auditable silence; digest-personal profile personalization; config-driven daily_dir; schema/contract trim. All validated by a 19-round replay over a real user workspace.

@imrewce

imrewce commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Opinion #2 — Concurrency guard: DESIGNED, DEFERRED Full design ready (active count, timestamps, context manager, lock); user requested changes stay inside proactive files for now.

Addressed in the latest commit

@jinliyl

jinliyl commented Sep 1, 2026

Copy link
Copy Markdown
Member

感谢这次 proactive 链路的重构,整体拆分方向比较清晰,尤其是把 extract/topics/plan/agenda/finish 分成独立 Step,以及为失败路径补充 fallback 和 checkpoint 语义,后续维护会更容易。

我本地 checkout 后完整看了一遍改动,并运行了相关测试。这里整理了一些建议在合并前确认的问题,其中前几项可能直接影响默认配置下的运行或兼容性。

1. 默认配置下 proactive v2 与 dream cron 的格式不兼容

当前默认配置同时启用了:

  • proactive_refresh_cron:18:00 写入 v2 daily/<date>/interests.yaml
  • dream_cron:23:00 再次读取并写入同一个文件

DreamTopicsStep 会通过 load_yaml_topics(..., strict=True) 读取当天已有的 interests.yaml,strict validator 只接受旧版 topic 字段。proactive v2 topic 新增的 idkindconfidencefirst_seenlast_evidence_at 会被判断为未知字段。

本地用 proactive 的 render_interests() 生成 v2 文件后,再交给 load_yaml_topics(strict=True),可以稳定复现:

ValueError: topics[0] has unknown field(s): confidence, first_seen, id, kind, last_evidence_at

这意味着 proactive 当天成功写入后,23:00 的默认 dream_cron 很可能在 Topics 阶段失败。建议在以下方案中明确选择一个:

  • 统一 interests.yaml 的 writer 和 schema;
  • 让 Dream Topics 兼容读取 v2 topic;
  • 或避免两个默认 cron 写入同一个曝光文件。

最好补一个按照默认 cron 顺序运行 proactive → dream 的回归测试。

2. proactive_read 遇到坏 YAML 时会移动用户文件,但仍返回成功

ProactiveStep._read_single() 在 YAML 解析失败时会调用 quarantine_interests(),把原始 interests.yaml 重命名为 interests.corrupt-<timestamp>.yaml,然后按空数据继续执行。

本地复现的结果是:

response.success = True
answer = Read 0 proactive topic(s)
原 interests.yaml 已不存在

这里有两个问题:

  1. read job 实际修改了用户文件,与文档中“proactive 不修改文件”的说明不一致;
  2. 调用方无法从成功响应中知道解析失败。

建议 reader 保持只读,解析失败时返回 success=False 和明确错误。若确实需要 quarantine,建议只在显式修复或 writer 流程中执行,并做好锁和唯一文件名处理。

3. proactive 重命名为 proactive_read 会破坏现有公开接口

默认配置删除了原来的 proactive job,只保留 proactive_read。这样已有的:

reme proactive

以及 HTTP/MCP 客户端、job allowlist 或外部集成中的旧 job 名都会失效。目前测试是直接改成了新名称,因此没有覆盖向后兼容。

如果重命名不是必须的 breaking migration,建议保留一个 proactive alias,内部继续复用同一个 proactive_step;同时再逐步将文档和示例迁移到 proactive_read

当前文档中也仍有一些旧描述,例如配置片段仍写 proactive:,正文仍说明该能力只读取 Auto Dream 的结果,需要一起统一。

4. Finish checkpoint 可能吞掉 LLM 运行期间的新修改

Extract 阶段根据当时的 mtime 计算 changed_paths,但 ProactiveState 只保存路径,没有保存本轮实际读取的 mtime。经过 extract/plan/agenda 等多次、可能持续数分钟的 LLM 调用后,Finish 又重新执行 stat(),并将最新 mtime 写入 catalog。

如果某个 daily 文件在 LLM 处理中再次被修改,Finish 会 checkpoint 新 mtime,但新内容实际上没有进入本轮 prompt。下一轮会错误地将它判断为 unchanged,从而漏处理用户的新内容。

建议 Extract 保存本轮读取的 path → mtime 快照;Finish 只 checkpoint mtime 仍与该快照一致的文件。mtime 已变化的路径应留给下一轮继续处理。可以补一个“LLM 执行期间修改源文件”的测试。

5. Job activity tracker 当前没有真正共享锁

BaseJob._tracked_execution() 每次调用都会创建新的 JobActivityTracker(metadata),而 tracker 在实例内部创建自己的 threading.Lock。因此两个并发 invocation 实际使用的是两把不同的锁,并不能保护同一份 metadata["__job_activity"]

另外:

  • snapshot() 当前没有调用方;
  • WaitForIdleStep 直接遍历共享 activity dict,也没有通过同一把锁读取;
  • 如果 thread-pool background job 正在首次新增 entry,reader 与 writer 之间仍可能竞态。

建议将锁保存在共享 metadata 中,采用现有 counter utility 类似的惰性初始化方式,并把实现收敛成同步 context manager,例如:

with track_job_activity(metadata, job_name):
    ...

再提供一个使用同一把锁的 snapshot helper 给 WaitForIdleStep。这样可以同时精简 JobActivityTracker 类、未使用的 snapshot() 和不必要的包级导出。

6. Plan/Agenda 的 fallback 没有覆盖普通 provider 异常

模块说明写的是 LLM 失败时使用 deterministic fallback,但 ProactivePlanStep._llm_cards()ProactiveAgendaStep._llm_agenda() 目前只捕获 asyncio.TimeoutError

网络错误、provider 限流、wrapper 异常等会直接中止 cron。此时 Topics 已经更新 truth source 并写入 interests.yaml,但 Finish 尚未 checkpoint,形成部分提交,下一轮还会重复处理和调用模型。

建议将普通 provider 异常也降级到 fallback,同时保持 cancellation 正常向上传播。

7. profile fallback 路径缺少 workspace containment

load_personal_profile_block() 直接使用:

fallback = ws / fallback_rel_path

绝对路径或包含 ../ 的路径可以逃出 workspace,而且读到的内容随后会进入模型 prompt。建议复用现有 workspace path containment 逻辑,拒绝 absolute、home-relative 和 traversal escape。

8. horizon_days > 1include_content=true 返回空内容

配置中 include_content 的说明是是否返回 raw YAML,但 horizon reader 读取 _proactive.yaml 后没有设置 result.content。因此响应里会出现:

{"content": ""}

建议在 horizon 模式返回 _proactive.yaml 原文;如果该模式明确不支持 raw content,则应从 answer 中省略字段并更新参数说明。

9. carry-forward 的过期边界不一致

load_carry_forward() 只过滤 (day - first_seen).days > carry_forward_days,因此恰好第 N 天仍会进入 prompt;但 trim_state_file() 使用 first_seen <= cutoff,又会在同一轮把它删除。

建议统一边界定义,避免 topic 被发送给 LLM 后立即从 truth source 移除。

验证结果

本地执行:

pytest tests/unit/test_proactive_refresh.py tests/unit/test_auto_dream.py tests/unit/test_job.py -q
109 passed

完整 unit suite:

1198 passed, 1 failed

唯一失败是未被本 PR 修改的 test_logging_config.py::test_stdlib_forwards_screen_and_file_logs_to_qwenpaw,单独运行仍失败,看起来与本 PR 无直接关系。

另外,Black 的 Python 3.11 配置检查通过。涉及真实模型/provider 的 integration tests 没有运行。

以上供参考。建议优先处理 1~5,它们分别涉及默认流程可运行性、用户文件安全、公开接口兼容、增量处理正确性和并发正确性;其余问题也适合在本次重构中补齐对应测试。

@imrewce

imrewce commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

New commit addresses the #1 issue: Dream currently only produces digest content. The dream package contains zero references to proactive / _proactive and performs no writes to interests.yaml.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants