Wenchao An 1b76ab9060
feat: add opt-in task notes and compacted history recall (#5382)
* feat: add opt-in task notes and compacted history recall

* fix: validate task continuity state and preserve user answers

Honor explicit opt-out, preserve clarification replies and capture failure statuses, validate notebook writes, and clear branch archive references. Update the config version and audit optional LLM credentials, with regression and integration evidence.

* fix: align Helm config version with task continuity schema

* fix: preserve mixed task history and declare continuity policies

* fix: recover malformed history and evict archives atomically
2026-09-12 21:01:46 +08:00

109 lines
5.1 KiB
Python

from __future__ import annotations
import argparse
import asyncio
import json
import time
from common import ROOT, LiveClient, digest, write_json
from retrieval import HistoryIndex
from task_eval import actor
def budget_limited(row):
if row["verified_completion"]:
return False
if any(e.get("reason") == "cumulative_context_budget" for e in row["events"]):
return True
return row["actor_cost"]["calls"] >= 8 and not any(e["type"] == "final" for e in row["events"])
async def continue_case(entry, client):
cid = entry["id"]
original = json.loads((ROOT / "results/tasks" / f"{cid}.json").read_text())
protocol = json.loads((ROOT / "continuation-protocol.json").read_text())
result = {"id": cid, "original_result_hash": digest(original), "protocol_hash": digest(protocol),
"continued_arms": {}, "unchanged_arms": [], "failures": []}
eligible = [a for a in "ABCD" if budget_limited(original["arms"][a])]
result["unchanged_arms"] = [a for a in "ABCD" if a not in eligible]
if eligible:
case = json.loads((ROOT / "cases/tasks" / f"{cid}.json").read_text())
gold = json.loads((ROOT / "gold/tasks" / f"{cid}.json").read_text())
memory = json.loads((ROOT / "memory" / f"{cid}.json").read_text())
assert memory["signature"] == original["memory_signature"]
index = HistoryIndex(case["records"], cid)
try:
await index.build_vectors(client)
for arm in eligible:
old = original["arms"][arm]
try:
new = await actor(case, gold, memory, index, client, arm,
continuation={"prefix_calls": old["actor_cost"]["calls"]})
old_events = [e for e in old["events"] if e["type"] != "stop"]
assert new["events"][:len(old_events)] == old_events, "Original tool prefix changed"
result["continued_arms"][arm] = {"before_verified": old["verified_completion"],
"before_correct_artifact": old["correct_artifact"],
"added_model_calls": new["actor_cost"]["calls"] - old["actor_cost"]["calls"],
"added_prompt_tokens": new["actor_cost"]["prompt_tokens"] - old["actor_cost"]["prompt_tokens"],
"added_completion_tokens": new["actor_cost"]["completion_tokens"] - old["actor_cost"]["completion_tokens"],
"prefix_verified_identical": True, "result": new}
except Exception as exc:
result["failures"].append({"arm": arm, "error_type": type(exc).__name__, "message": str(exc)})
finally:
index.close()
write_json(ROOT / "results/continued" / f"{cid}.json", result)
print("continued " + cid + " " + " ".join(a + "=" + str(int(v["result"]["verified_completion"]))
for a, v in result["continued_arms"].items()), flush=True)
return result
async def run(args):
primary = json.loads((ROOT / "task-manifest.json").read_text())["test"]
known = json.loads((ROOT / "known-goal-manifest.json").read_text())
entries = primary + known
client = LiveClient(args.endpoints, concurrency=4)
sem = asyncio.Semaphore(2)
scheduled = set()
jobs = []
async def one(entry):
async with sem:
return await continue_case(entry, client)
try:
while len(scheduled) < len(entries):
for entry in entries:
cid = entry["id"]
if cid in scheduled:
continue
path = ROOT / "results/tasks" / f"{cid}.json"
if path.exists():
original = json.loads(path.read_text())
# Known-goal writer adds its diagnostic identity immediately after the base result.
if cid.startswith("known-") and "diagnostic_protocol_hash" not in original:
continue
scheduled.add(cid)
dest = ROOT / "results/continued" / f"{cid}.json"
if dest.exists():
saved = json.loads(dest.read_text())
assert saved["original_result_hash"] == digest(original)
else:
jobs.append(asyncio.create_task(one(entry)))
if len(scheduled) < len(entries):
if (ROOT / "results/tasks-test-run.json").exists() and (ROOT / "results/known-goal-run.json").exists():
break
if not args.watch:
break
await asyncio.sleep(15)
rows = await asyncio.gather(*jobs)
write_json(ROOT / "results/continuation-run.json", {"selected": len(entries),
"scheduled": len(scheduled), "calls": client.calls,
"failures": [{"id": r["id"], **f} for r in rows for f in r["failures"]]})
finally:
await client.close()
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument("--endpoints", required=True)
p.add_argument("--watch", action="store_true")
asyncio.run(run(p.parse_args()))