mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-21 00:22:45 +00:00
* refractor(proactive): upgrade proactive feature with disentangled job and steps * refactor(proactive): apply audit fixes - 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 * feat(proactive): strict extract-output gate and prompt total budget - 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 * feat(proactive): add scenario-card plan step and generative agenda step * feat(proactive): digest-personal profile personalization and leaner LLM 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) * fix(proactive): skip checkpoint when extract reply stays unusable after 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 #1 gap: the structural gate detected schema-wrong output but a double failure still checkpointed). * fix(proactive): replace running bool with reference-counted job activity tracker for the idle gate * refactor(proactive): remove job activity tracking and idle gate, restore job tree to upstream * fix(proactive): address second audit round (readonly reader, mtime checkpoint, wider fallbacks, profile containment, horizon content, expiry boundary) * refactor(dream): strip interests.yaml ownership from dream, proactive is now the sole writer * refactor(dream): separate proactive topic generation * ci: update renamed auto dream smoke test * fix(proactive): complete refresh migration and docs --------- Co-authored-by: jinli.yl <jinli.yl@alibaba-inc.com>
79 lines
3.4 KiB
Python
79 lines
3.4 KiB
Python
"""Proactive finish step: checkpoint only the proactive catalog (F3)."""
|
|
|
|
from ...base_step import BaseStep
|
|
from .._evolve import passthrough_response
|
|
from ....components import R
|
|
from ....schema import FileNode, ProactiveState
|
|
from ..dream.utils import workspace_dir
|
|
|
|
|
|
@R.register("proactive_finish_step")
|
|
class ProactiveFinishStep(BaseStep):
|
|
"""Upsert this round's changed material into the proactive catalog.
|
|
|
|
interests.yaml is deliberately NOT checkpointed (v5 R6): the catalog only
|
|
serves change-detection watermarking and interests.yaml is excluded from
|
|
the material set (INV-11), so no consumer would ever read it back.
|
|
Never calls ``refresh_day_index`` and never touches the dream catalog
|
|
(INV-2). When the short-circuit flag is set, no checkpoint happens so the
|
|
same material stays "changed" for the next round (INV-7).
|
|
"""
|
|
|
|
def __init__(self, persist: bool = True, skip_key: str = "proactive_skip", **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.persist = persist
|
|
self.skip_key = skip_key
|
|
|
|
async def execute(self):
|
|
assert self.context is not None
|
|
if self.context.get(self.skip_key):
|
|
return passthrough_response(self, self.skip_key)
|
|
if self.file_catalog is None:
|
|
raise RuntimeError("proactive_finish_step requires file_catalog")
|
|
raw_state = self.context.get("proactive")
|
|
if not raw_state:
|
|
self.context.response.success = True
|
|
self.context.response.answer = "Skipped finish: no proactive extract state in context"
|
|
return self.context.response
|
|
state = ProactiveState.model_validate(raw_state)
|
|
if state.early_exit:
|
|
self.context.response.success = True
|
|
self.context.response.answer = f"Skipped finish: {state.early_exit}"
|
|
return self.context.response
|
|
ws = workspace_dir(self)
|
|
|
|
checkpoint = [rel for rel in state.changed_paths if (ws / rel).is_file()]
|
|
snapshot = state.changed_mtimes
|
|
nodes: list[FileNode] = []
|
|
deferred = 0
|
|
for rel in dict.fromkeys(checkpoint):
|
|
try:
|
|
mtime = (ws / rel).stat().st_mtime
|
|
except OSError:
|
|
continue
|
|
if rel in snapshot and mtime != snapshot[rel]:
|
|
# Modified while extract/plan/agenda were running: the new
|
|
# content never reached this round's prompt, so leave the path
|
|
# un-checkpointed for the next round (audit item 3).
|
|
deferred += 1
|
|
continue
|
|
nodes.append(FileNode(path=rel, st_mtime=mtime))
|
|
self.logger.info(
|
|
f"[{self.name}] start checkpoint={len(nodes)} deferred={deferred} persist={self.persist}",
|
|
)
|
|
if nodes:
|
|
await self.file_catalog.upsert(nodes)
|
|
if self.persist and nodes:
|
|
await self.file_catalog.dump()
|
|
|
|
state.checkpoint_paths = [n.path for n in nodes]
|
|
data = state.model_dump()
|
|
self.context["proactive"] = data
|
|
self.context.response.metadata["proactive"] = data
|
|
self.context.response.success = True
|
|
answer = f"Proactive finished: checkpointed {len(nodes)} path(s)"
|
|
if deferred:
|
|
answer += f", deferred {deferred} path(s) modified during the round"
|
|
self.context.response.answer = answer
|
|
self.logger.info(f"[{self.name}] finish checkpointed={len(nodes)}")
|
|
return self.context.response
|