Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,28 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
const outcomeCopy: Record<string, string> = zh ? {replan: "转向", continue: "继续", stop: "停止", wait: "等待", no_change: "无变化", ask_human: "请人决定"} : {replan: "Replan", continue: "Continue", stop: "Stop", wait: "Wait", no_change: "No change", ask_human: "Ask human"};
const findingBody = (finding: ExploreHistoryEvent) => {
const sections = parseExploreFindingSections(finding.summary);
if (!sections) return <p>{finding.summary}</p>;
const decision = sections.decision;
return <dl className="research-finding-sections">
const decision = sections?.decision;
const execution = finding.execution_observation;
const measured = (value: unknown) => value === null ? (zh ? "未测量" : "Not measured") : String(value);
return <>{sections ? <dl className="research-finding-sections">
<dt>{sectionCopy.observation}</dt><dd>{sections.observation}</dd>
{decision ? <>
<dt>{zh ? "决策" : "Decision"}</dt><dd><b className={`research-outcome is-${decision.outcome}`}>{outcomeCopy[decision.outcome] ?? decision.outcome}</b></dd>
{(["changed", "stopped", "retained"] as const).map(key => decision[key].length ? <><dt key={`${key}-t`}>{sectionCopy[key]}</dt><dd key={key}><ul>{decision[key].map(item => <li key={item}>{item}</li>)}</ul></dd></> : null)}
</> : <><dt>{sectionCopy.interpretation}</dt><dd>{sections.interpretation}</dd></>}
<dt>{sectionCopy.applicability}</dt><dd className="is-muted">{sections.applicability}</dd>
<dt>{sectionCopy.inputRevision}</dt><dd><code>{sections.inputRevision}</code></dd>
</dl>;
</dl> : <p>{finding.summary}</p>}{execution ? <details><summary>{zh ? "实际执行与资源观测" : "Execution and resource observations"}</summary>
<dl className="research-finding-sections">
<dt>{zh ? "计划方法 → 实际方法" : "Planned → executed method"}</dt><dd>{String(execution.planned_method_id)} → {String(execution.executed_method_id)}</dd>
{execution.change_reason ? <><dt>{zh ? "改向原因" : "Reason for change"}</dt><dd>{String(execution.change_reason)}</dd></> : null}
<dt>{zh ? "运行秒数 / 上限秒数" : "Elapsed / limit seconds"}</dt><dd>{measured(execution.elapsed_seconds)} / {measured(execution.limit_seconds)}</dd>
<dt>{zh ? "搜索次数" : "Search iterations"}</dt><dd>{measured(execution.search_iterations)}</dd>
<dt>{zh ? "结束原因" : "Termination reason"}</dt><dd>{measured(execution.stop_reason)}</dd>
<dt>{zh ? "执行候选 SHA-256" : "Executed candidate SHA-256"}</dt><dd><code>{String(execution.candidate_sha256)}</code></dd>
{execution.result_ref ? <><dt>{zh ? "该候选的结果引用" : "Result for this candidate"}</dt><dd><code>{String(execution.result_ref)}</code></dd></> : null}
</dl><p>{zh ? "来源记录的执行观测;不等于方法有效或当前候选已通过验收。" : "Source-recorded execution observations; not proof of method benefit or current-candidate acceptance."}</p>
</details> : null}</>;
};
const findingElement = (finding: ExploreHistoryEvent) => <div className="research-recorded-finding" key={finding.event_id}><strong>{finding.title}</strong><small>{timeLabel(finding.recorded_at)} · {zh ? "来源标记" : "Source label"} · {stateLabel(finding.status)}{finding.confidence !== null ? ` · ${zh ? "来源自报置信度" : "Source-reported confidence"} ${finding.confidence}` : ""}</small>{findingBody(finding)}{finding.evidence_refs.map(ref => <code key={ref}>{ref}</code>)}</div>;
const updateRecords = (events: readonly ExploreHistoryEvent[]) => <ol className="research-history-records">{events.map(event => <li key={event.event_id}><time>{timeLabel(event.recorded_at)}</time><span>{stateLabel(event.status)} · {event.title}{event.summary ? ` — ${event.summary}` : ""}</span>{event.parent_id ? <code>{copy.group}: {event.parent_id}</code> : null}{event.evidence_refs.map(ref => <code key={ref}>{ref}</code>)}{event.run_id ? <code>{event.run_id}</code> : null}</li>)}</ol>;
Expand Down
74 changes: 74 additions & 0 deletions benchmark/runtime/RESEARCH_LOOP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Research stages and execution evidence

The heartbeat runtime's explicit `research_loop=True` option retains the
original research objective while separating an initial-candidate Todo from
its deferred research successor. Omission preserves the previous treatment.
The runtime selects the existing `evidence_conditioned_v1` replan strategy and
rejects an explicitly conflicting strategy. This standalone stage/evidence
entry retains the existing Host admission boundaries; it does not install a
per-provider-request guard. When composed with C1's native request-admission
entry, that entry checks ordinary Host admission at each actual provider send
and requires a configured Responses-compatible endpoint.

The entry creates the two stages using existing `todo_done` and successor
semantics. Finishing the candidate releases real optimization work; it cannot
by itself close that work. Research retains the best candidate, meaningful
negative results and reasoned closeout against the original objective. User
Stop, the original budget and genuine blockers remain authoritative.
Preparation records scope through the ordinary Agent Vision writer as
`surface_only`, without claiming research progress or spending a work Turn.
Existing research work is not reopened or its later Vision overwritten.

The installed `loopx-research-probe` uses the staged runner/Python. The same
producer can be called as `python -m benchmark.runtime.research_probe`:

```sh
loopx-research-probe --candidate-file candidate.py --output probe-001.json \
--input-revision public-case-v1 --planned-method-id scheduler \
--executed-method-id mask-selection --change-reason 'The control isolated mask selection first.' \
--evidence-ref probe:local-001 --limit-seconds 30 --diagnostics probe-diagnostics.json \
-- python3 candidate.py
```

The solver may write `{"search_iterations":2,"stop_reason":"search_cap"}` to
`LOOPX_RESEARCH_DIAGNOSTICS`. The host measures elapsed time, exit/timeout and
the selected candidate-file digest before/after. A changed candidate rejects
attribution. Solver counters and reasons remain reports; missing counters
remain null. Low resource use does not impose a mandatory time floor.
Malformed optional diagnostics preserve reliable host measurements and retain
a bounded diagnostic-error receipt. Evidence paths must be fresh; raw output
stays local. The probe neither submits nor accesses hidden evaluation.

The adjacent `.candidate.json` preserves selected relative paths and hashes.
`candidate_sha256` hashes the sorted JSON file manifest, not native archive
container bytes. Pins cover only declared files at the probe boundaries, not
transitive imports or independent writers. Result attribution must still prove
that the evaluated snapshot matches the selected candidate and input revision.

Attach the emitted `execution_observation` through normal Explore result
writeback. Its input revision and evidence refs must belong to the enclosing
finding; released results must identify the candidate actually evaluated.
Different planned/executed method ids require a change reason. Findings,
replan and research-history details retain this same source. They do not
certify method benefit or Goal completion. Missing observations preserve old
events; a reader for the optional field is required for new retained findings.

Result followthrough uses existing material delivery classifications.
`surface_only` and unknown historical outcomes remain readable but cannot
consume a cited work result or create a new unresolved experiment. Useful
negative results and concrete blockers remain eligible evidence.

## 中文交付边界

显式研究入口保留原 Goal,把初始候选与后续研究表示成既有 Todo 和后继依赖。
候选完成只释放后续研究,准备阶段使用 `surface_only`,不会声称已经取得收益。
Stop、原预算和真实阻塞保持有效,不要求填满时长或改变原生评测规则。
独立的阶段/证据入口沿用既有 Host 准入边界,不安装逐次 provider 请求守卫。
只有组合 C1 的原生请求准入入口后,该入口才会在每次实际 provider 发送前检查
普通 Host 准入,并要求配置兼容 Responses 的端点。

本地探针固定所选文件,记录真实耗时、退出与超时状态;可选 solver 报告与宿主
测量分开,坏诊断不能抹去可靠测量。所选文件清单哈希不是原生归档哈希。
证据通过既有 Explore 写回并进入 replan 和研究历程;缺失测量保持未知,负结果
继续可用。该切片交付本地执行证据与阶段接续,原生提交、自动反馈依赖和成本
归约由后续组合切片接入。工程回归不证明首次分数或长期收益已经改善。
28 changes: 27 additions & 1 deletion benchmark/runtime/harbor.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def __init__(
turn_envelope=False,
native_goal_continue=False,
task_objective=None,
research_loop=False,
goal_id=_GOAL_ID,
agent_id=_AGENT_ID,
defer_codex_home=False,
Expand Down Expand Up @@ -112,6 +113,9 @@ def __init__(
self.replan_after_todos = int(3 if replan_after_todos is None else replan_after_todos)
if not 1 <= self.replan_after_todos <= 5:
raise ValueError("replan_after_todos must be between 1 and 5")
from .research_loop import research_stride_strategy

stride_strategy = research_stride_strategy(research_loop, stride_strategy)
if stride_strategy is not None and not self.execution.uses_loopx:
raise ValueError("stride_strategy requires a LoopX execution mode")
self.stride_strategy = (normalize_stride_strategy(stride_strategy)
Expand All @@ -122,6 +126,9 @@ def __init__(
if not self.execution.uses_loopx:
raise ValueError("planning_horizon_goal requires a LoopX execution mode")
self.planning_horizon_goal = planning_horizon_goal
if not isinstance(research_loop, bool) or (research_loop and self.execution.mode != "heartbeat"):
raise ValueError("research_loop requires heartbeat execution")
self.research_loop = research_loop
self.checkpoint_request_path = None
self._checkpoint_declaration = None
if checkpoint_request is not None:
Expand Down Expand Up @@ -327,6 +334,12 @@ async def install(self, environment: BaseEnvironment) -> None:
raise RuntimeError("LoopX doctor returned invalid JSON") from exc
if doctor_payload.get("ok") is not True:
raise RuntimeError(f"LoopX doctor failed: {doctor_payload}")
if self.research_loop:
wrapper = (f"#!/bin/sh\nexec env PYTHONPATH={shlex.quote(_SRC)} "
f"{_PYTHON}/bin/python3 -m benchmark.runtime.research_probe \"$@\"\n")
await self.exec_as_agent(environment,
command=f"printf %s {shlex.quote(wrapper)} > {_PROFILE}/bin/loopx-research-probe; "
f"chmod 0755 {_PROFILE}/bin/loopx-research-probe", env=self._profile_env())

receipt = {
"loopx_commit": actual_commit,
Expand All @@ -335,6 +348,7 @@ async def install(self, environment: BaseEnvironment) -> None:
"execution_mode": self.execution.mode,
"iteration_context": self.execution.context,
"task_entry": self.execution.task_entry,
**({"research_loop": True} if self.research_loop else {}),
**self._turn_envelope_receipt(),
**self._planning_receipt(),
"home_scope": "trial",
Expand Down Expand Up @@ -494,8 +508,12 @@ async def _prepare_phase(
cwd=cwd,
)

if self.execution.task_entry == "seeded-todo":
if self.execution.task_entry == "seeded-todo" or self.research_loop:
await self._seed_phase(environment, cwd=cwd)
if self.research_loop:
from .research_loop import prepare_research_continuation

await prepare_research_continuation(self, environment, cwd=cwd)

cadence = await self._loopx(
environment, ["configure-goal", "--goal-id", self.goal_id], cwd=cwd,
Expand All @@ -521,6 +539,13 @@ async def _seed_phase(self, environment: BaseEnvironment, *, cwd: str) -> None:
"implement and validate against the task's full requirements and acceptance "
"criteria. Keep unmet requirements explicit when judging task completion."
)
if self.research_loop:
text = (
f"[P0] Build and evaluate the strongest feasible initial candidate for {self._task_document}. "
"Check the actual interface, correctness and resource use before the first submission. "
"Preserve the tested candidate and released feedback. Completing this candidate stage "
"does not complete the separate original research work."
)
if self._seeded_todo_id:
listed = await self._loopx(environment, [
"todo", "list", "--goal-id", self.goal_id, "--role", "agent",
Expand Down Expand Up @@ -582,6 +607,7 @@ def _worker_env(self, *, cwd: str) -> dict[str, str]:
"LOOPX_SHARED_SKILLS": _SHARED_SKILLS,
"LOOPX_EXECUTION_MODE": self.execution.mode,
"LOOPX_TASK_ENTRY": self.execution.task_entry,
**({"LOOPX_RESEARCH_LOOP": "1"} if self.research_loop else {}),
**({"LOOPX_CHECKPOINT_REQUEST": self.checkpoint_request_path}
if self.checkpoint_request_path is not None else {}),
**({"LOOPX_TURN_ENVELOPE": "1"} if self.execution.turn_envelope else {}),
Expand Down
7 changes: 6 additions & 1 deletion benchmark/runtime/planning.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ def render_planning_prompt(packet: dict, env: dict[str, str]) -> str:
value = float(env[key])
if not math.isfinite(value) or value <= 0:
raise ValueError(f"{key} must be finite and positive")
return ("$loopx\n\nBenchmark research planning guidance:\n" + _RESEARCH_PLANNING_GUIDANCE
research = ""
if env.get("LOOPX_RESEARCH_LOOP") == "1":
from .research_loop import research_execution_guidance

research = research_execution_guidance()
return ("$loopx\n\nBenchmark research planning guidance:\n" + _RESEARCH_PLANNING_GUIDANCE + research
+ "\n\nHost-supplied planning checkpoint:\n" + json.dumps(packet, ensure_ascii=False))


Expand Down
73 changes: 73 additions & 0 deletions benchmark/runtime/research_loop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Compose research scope with ordinary Todo/wait owners, not a second planner."""
from __future__ import annotations


def research_stride_strategy(enabled, strategy):
"""One treatment definition shared by native CLI and programmatic entries."""
if not enabled:
return strategy
if strategy not in (None, "evidence_conditioned_v1"):
raise ValueError("research_loop requires evidence_conditioned_v1, not a disabled or different review strategy")
return "evidence_conditioned_v1"


async def prepare_research_continuation(runtime, environment, *, cwd):
"""A candidate stage cannot close a separately retained research outcome.

The deferred successor contains actual optimization work, not a recurring
instruction to replan. Its existing resume owner releases it when the
initial stage is done; budget, Stop and evidence-backed closeout stay native.
"""
initial = runtime._seeded_todo_id
if not initial:
raise ValueError("research continuation requires its initial candidate stage")
listed = await runtime._loopx(environment, ["todo", "list", "--goal-id", runtime.goal_id,
"--role", "agent"], cwd=cwd)
matches = [todo for todo in listed["todos"] if todo.get("target_key") == "research-continuation"]
if len(matches) > 1:
raise ValueError("research continuation identity is ambiguous")
if matches:
current = matches[0]
if current.get("claimed_by") != runtime.agent_id:
raise ValueError("research continuation belongs to another Agent")
return current # Never reopen completed work or clear a real wait on preparation.
research = await runtime._loopx(environment, [
"todo", "add", "--goal-id", runtime.goal_id, "--role", "agent",
"--claimed-by", runtime.agent_id, "--task-class", "advancement_task",
"--action-kind", "benchmark_task", "--target-key", "research-continuation",
"--status", "deferred", "--resume-when", "todo_done:" + initial,
"--text", f"[P1] Improve the original research objective in {runtime._task_document} through "
"complete, evidence-driven experiments. Preserve the best validated candidate. Use released "
"results and actual resource diagnostics to choose between a local repair and a different "
"mechanism; retain negative results. Candidate correctness and a successful submission settle "
"only that candidate stage. Close this research work only with evidence against the original "
"objective and remaining useful authorized work. Explicit user Stop, the original budget limit "
"and genuine blockers remain valid stops; do not manufacture work or run merely to fill time.",
"--execute"], cwd=cwd)
await runtime._loopx(environment, ["todo", "update", "--goal-id", runtime.goal_id,
"--agent-id", runtime.agent_id, "--todo-id", initial,
"--successor-todo-id", research["todo_id"], "--execute"], cwd=cwd)
await runtime._loopx(environment, ["refresh-state", "--goal-id", runtime.goal_id,
"--agent-id", runtime.agent_id, "--classification", "research_scope_initialized",
"--delivery-batch-scale", "single_surface", "--delivery-outcome", "surface_only",
"--vision-state", "active", "--vision-advancement-policy", "as_needed",
"--vision-summary", "Build a strong initial candidate, then execute evidence-driven improvements while retaining the best validated artifact.",
"--vision-acceptance", "Candidate validity settles its stage, not the original research objective. Respect user Stop, original budget and genuine blockers; justify research closeout from remaining useful authorized work.",
"--no-global-sync", "--suppress-external-sinks"], cwd=cwd)
return research


def research_execution_guidance() -> str:
return (
'\nResearch-loop execution evidence:\nThe initial candidate and ongoing research are separate canonical Todos. '
'Do not close the research successor merely because the candidate passes tests. Preserve the original '
'objective and existing Stop/budget/blocked conditions. Before the first native submission, run a relevant '
'local candidate check and retain actual resource observations, including unknown measurements. Use '
'`loopx-research-probe --help` for the local probe wrapper: it pins explicit candidate files, measures '
'runtime and enforces the selected local limit. A solver can write search_iterations and stop_reason as JSON '
'to LOOPX_RESEARCH_DIAGNOSTICS; absent values remain unknown, and solver reports are not independently '
'verified. This is not another evaluation or a requirement to use every available second. Attach the emitted '
'execution_observation in the normal Explore result writeback. Preserve its input revision and evidence '
'refs. Record changed method ids and the reason when actual implementation differs from the proposal. '
'Resolve this evidence at the next replan.\n'
)
Loading
Loading