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 @@ -634,6 +634,13 @@ Explore contributes references into its existing scoped findings/planner and
omitted-result reads; the proposal binds the revision and considered findings
with an applicability/disposition reason. Typed validation rejects missing
reviews, wrong decision/finding references and stale or unavailable bases.
Revision-bound global result pages can extend considered findings through the
optional v2 `explore_review.result_pages` coordinates. The existing Explore
owner replays at most three pages from its current source before writeback;
these facts do not change the admitted context/basis identity. Changed source
requires rereading within the original Turn. Page coordinates persist with the
decision into subsequent review; node-filtered/planner detail routes remain
outside this bounded citation path. No separate receipt store is introduced.
These are provenance and proposal-shape obligations, not automatic proof that
research was executed or useful. Ordinary Todo/Next Action carries admitted work.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,12 @@ revision、考虑的 finding 与适用/暂缓等理由,不建新图或第二
重启并发基线。这类运行用于观察路线采用与迭代收益,不单独证明因果净收益或
promotion 资格。需要相应结论时,再执行匹配接续与重复验证。

全局结果分页可通过 v2 的可选 `explore_review.result_pages` 页面坐标补充
`considered_refs`。既有 Explore owner 在写回前从当前来源重放最多三页,不改变
原上下文/证据基础身份;来源变化要求在原 Turn 内重新读取和评估。页面坐标随
决策进入后续复盘,不新增回执库。按节点筛选和 planner 详情仍不属于此引用入口,
来源核验也不证明模型采用或研究收益。

### 7.7 研究状态分支与匹配接续

可恢复研究点包含**会话位置、研究证据、代码/候选产物与观测机会**。单独源码
Expand Down
25 changes: 25 additions & 0 deletions docs/reference/protocols/goal-vision-replan-contract-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,24 @@ stale revisions and declaring unavailable evidence as empty are rejected. A
changed evidence basis requires reassessment under the original Turn identity,
not another quota debit. Disabled Explore adds no Explore assessment duty.

For a considered finding omitted from the bounded basis, follow the global
`result_page.next_command` (or increase its page size), then add
`explore_review.result_pages: [{"revision": "<page revision>", "offset": 3, "limit": 3}]`.
Copy these three coordinates from each page actually read; keep
`explore_review.revision` bound to the original Explore basis. At most three
distinct pages and three considered findings fit the existing Vision budget.
Writeback replays the requested slices of the current, strictly validated
Explore source and admits only findings present in those pages. The page
revision binds the ordered full result contents and Goal/reader scope; it is
not the bounded basis revision. Same-Goal peer findings remain reusable.
Changed or unavailable source, forged membership and node-filtered pages are
refused. Reread and reassess under the original Turn and obligation. This
optional v2 field is retained in decision/history readback; ordinary reads stay
read-only and no receipt store, claim, spawn or extra quota debit is created.
Source replay verifies the declared citation, not that the model understood or
adopted it. Existing v2 decisions without pages and historical v1 reads retain
their behavior. Other detail routes are not supplemental citation sources.

The typed owner validates shape, references and declared provenance before the
fresh Vision path exit. When an enabled Goal reaches a periodic review, every
accepted exit must also record a current `agent_vision.stride_decision`: a new
Expand Down Expand Up @@ -536,6 +554,13 @@ not improved model decisions or net efficiency.
集成、验证和回读,不扩大权限、延迟结算或捆绑独立承诺。下次复盘核查质量与完整
边际成本;合成准入链路只能证明协议采用,不能证明模型决策或净效率改善。

补读全局结果分页后,可在 `explore_review.result_pages` 中附上实际读取页面的
`revision`、`offset`、`limit`,最多三页;`explore_review.revision` 仍引用原有界
证据版本。提交时从当前可信来源重放页面并核对 `considered_refs`,支持同一 Goal
其他 Agent 产出的证据。来源变化、不可用、伪造成员或按节点筛选的页面会被拒绝,
应在原 Turn 和原复盘义务下补读并重新评估。读取不写入回执或扣额度,分页引用随
决策保留到历史和下次复盘;来源核验不证明模型理解、采用或研究收益。

### Path Delta

A vision packet may include a top-level `path_delta` object; `goal_path_delta_v0`
Expand Down
11 changes: 11 additions & 0 deletions loopx/capabilities/explore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,17 @@ ordering. These reads do not alter planner scores, graph state, claims or quota.
The dashboard's saved-evidence reader also pages through history; the CLI
options above control the agent context, not dashboard page size.

When the stride strategy is enabled, findings read from a global result page
can be cited in `explore_review.considered_refs` by also copying that page's
`revision`, `offset` and `limit` into `explore_review.result_pages` (up to three
pages). Keep the separately delivered `explore_review.revision`; the two
revision fields have different scopes. Writeback resolves membership from the
same current source, including results authored by peers in this Goal, and
rejects changed/unavailable pages. This adds no read-side writes or execution
permission. Node-filtered and planner-detail reads are not supported by this
citation path. See the [stride contract](../../../docs/reference/protocols/goal-vision-replan-contract-v0.md#structured-stride-control)
for the existing decision, budget and original-Turn recovery rules.

Planning context also keeps a bounded `typed_evidence_audit` on suggested
branches with explicit Todo/node links, and up to three existing exploring
frontier nodes. Linked findings are resolved before the recent-history limit,
Expand Down
8 changes: 6 additions & 2 deletions loopx/capabilities/explore/turn_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ def _policy(registry_path, goal_id):
return goal, graph, gate


def read_replan_explore_context(*, source_status: Mapping, goal_id: str, agent_id: str | None):
def read_replan_explore_context(*, source_status: Mapping, goal_id: str, agent_id: str | None,
review_pages=None):
"""Outer read adapter; the replan owner invokes it only for enabled reviews.

Unavailable IO is distinct from an empty or disabled evidence source. No
Expand All @@ -50,7 +51,8 @@ def read_replan_explore_context(*, source_status: Mapping, goal_id: str, agent_i
try:
return explore_turn_context(
registry_path=Path(source_status["registry"]), runtime_root=Path(source_status["runtime_root"]),
goal_id=goal_id, agent_id=agent_id, include_finding_digests=True)
goal_id=goal_id, agent_id=agent_id, include_finding_digests=True,
**({"review_pages": review_pages} if review_pages is not None else {}))
except (OSError, ValueError, RuntimeError):
return {"ok": False, "goal_id": goal_id, "agent_id": agent_id}

Expand All @@ -60,6 +62,7 @@ def explore_turn_context(
result_limit: int = 3, result_offset: int = 0,
result_node: str | None = None, result_revision: str | None = None,
include_finding_digests: bool = False,
review_pages=None,
):
if type(include_finding_digests) is not bool:
raise ValueError("include_finding_digests must be an explicit boolean")
Expand Down Expand Up @@ -119,6 +122,7 @@ def explore_turn_context(
"result_node": result_node,
"result_revision": result_revision,
**({"include_finding_digests": True} if include_finding_digests else {}),
**({"review_pages": review_pages} if review_pages is not None else {}),
},
)

Expand Down
26 changes: 26 additions & 0 deletions loopx/control_plane/capabilities/explore_result_page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** Bounded descriptors for replaying existing global Explore result pages. */
import type {JsonObject} from "../effect_program.ts";
import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
import {requireJsonObject} from "../runtime_decode.ts";
import {BARE_SHA256_PATTERN} from "../content_digest.ts";

export function normalizeExploreResultPages(value: unknown): JsonObject[] {
if (!Array.isArray(value) || !value.length || value.length > 3) {
throw new EffectRuntimeRequestError("explore_review.result_pages requires one to three distinct result pages");
}
const seen = new Set<string>();
return value.map(item => {
const row = requireJsonObject(item, "Explore result page");
if (Object.keys(row).some(key => !["revision", "offset", "limit"].includes(key)) ||
typeof row.revision !== "string" || !BARE_SHA256_PATTERN.test(row.revision) ||
!Number.isSafeInteger(row.offset) || Number(row.offset) < 0 ||
!Number.isSafeInteger(row.limit) || Number(row.limit) < 1 || Number(row.limit) > 20) {
throw new EffectRuntimeRequestError("Explore result page requires revision, nonnegative offset and limit 1..20; node-filtered pages are unsupported");
}
const page = {revision: row.revision, offset: row.offset, limit: row.limit};
const key = JSON.stringify(page);
if (seen.has(key)) throw new EffectRuntimeRequestError("Explore result pages must be distinct");
seen.add(key);
return page;
});
}
13 changes: 13 additions & 0 deletions loopx/control_plane/capabilities/explore_turn_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
import {requireInteger, requireJsonObject, requireNonEmptyString, requireStringArray} from "../runtime_decode.ts";

import {EXPLORE_WRITEBACK_SUMMARY_LIMIT, exploreResultWritebackAffordance} from "./explore_result_writeback.ts";
import {normalizeExploreResultPages} from "./explore_result_page.ts";

function rows(value: unknown): JsonObject[] {
return Array.isArray(value) ? value.map(item => requireJsonObject(item, "Explore row")) : [];
Expand Down Expand Up @@ -117,11 +118,23 @@ export function projectExploreTurnContext(params: JsonObject): JsonObject {
throw new EffectRuntimeRequestError("Continuation requires result_revision from the previous page");
}
const visibleResults = resultRows.slice(resultOffset, resultOffset + resultLimit);
// Write-time replay uses this same source snapshot and ordering. The caller
// supplies only page coordinates, never the findings admitted by that page.
const reviewPages = params.review_pages === undefined ? null : normalizeExploreResultPages(params.review_pages).flatMap(page => {
if (!graph || resultNode !== null || page.revision !== resultRevision) {
// A stale query is not an unavailable source. Retain the current bounded
// context/read route; the replan owner refuses any unresolved citation.
return [];
}
return [{...page, finding_refs: resultRows.slice(Number(page.offset), Number(page.offset) + Number(page.limit))
.map(row => row.finding_id)}];
});
const remainingResults = Math.max(0, resultRows.length - resultOffset - visibleResults.length);
const resultRead = (...args: string[]) => command("turn-context", "--agent-id", agent, ...args);
const writeback = exploreResultWritebackAffordance();
return {
ok: true, goal_id: goal, agent_id: agent, graph_enabled: graph, harness_enabled: harness,
...(reviewPages ? {review_pages: reviewPages} : {}),
graph: graph ? {
counts: projection.counts ?? {},
recent_nodes: nodes.slice(0, 3).map(row => compact(row, ["node_id", "title", "status", "blocked_reason"])),
Expand Down
6 changes: 6 additions & 0 deletions loopx/control_plane/work_items/replan_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,12 @@ export function projectReplanContext(value: unknown): JsonObject {
if (strideStrategy !== "off") context.stride_strategy = strideStrategy;
if (followthrough) context.decision_followthrough = followthrough;
if (exploreBasis) context.explore_basis = exploreBasis;
// Source-replayed supplemental pages qualify a proposal, not a new admission
// context. They must not change the original context or bounded basis IDs.
if (exploreBasis?.availability === "available" && request.explore_context != null) {
const source = requireJsonObject(request.explore_context, "Explore stride source");
if (source.review_pages !== undefined) context.explore_result_pages = source.review_pages;
}
if (omitted.length) context.omitted_evidence = {
distinct_observation_count: omitted.length,
newest_at: omitted[0].row.generated_at, oldest_at: omitted.at(-1)!.first,
Expand Down
21 changes: 20 additions & 1 deletion loopx/control_plane/work_items/stride_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import decisionSchema from "../stride_decision_schema.json" with {type: "json"};
import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
import type {JsonObject} from "../effect_program.ts";
import {requireJsonObject, requireNonEmptyString, requireStringLiteral} from "../runtime_decode.ts";
import {normalizeExploreResultPages} from "../capabilities/explore_result_page.ts";

export function normalizeStrideStrategy(value: unknown): string {
if (value === undefined) return schema.default;
Expand Down Expand Up @@ -133,14 +134,22 @@ export function normalizeStrideDecision(value: unknown, bounded: BoundedText = t
}
if (input.explore_review != null) {
const review = requireJsonObject(input.explore_review, "stride_decision.explore_review");
exactFields(review, ["revision", "disposition", "considered_refs", "reason"], "explore_review");
exactFields(review, ["revision", "disposition", "considered_refs", "reason", ...(!historical ? ["result_pages"] : [])], "explore_review");
if (review.revision !== null && !/^[a-f0-9]{64}$/.test(requireNonEmptyString(review.revision, "explore_review.revision"))) throw new EffectRuntimeRequestError("explore_review requires a delivered revision or null when unavailable");
requireStringLiteral(review.disposition, decisionSchema.explore_dispositions, "explore_review.disposition");
const considered = refs(review.considered_refs, "explore_review.considered_refs");
if (review.disposition === "used" && !considered.length) throw new EffectRuntimeRequestError("used Explore evidence requires a delivered reference");
output.explore_review = {...review, considered_refs: considered, reason: checked("explore_review.reason", review.reason, decisionSchema.review_text_limit)};
for (const key of ["revision", "disposition"]) usage[`stride_decision.explore_review.${key}`] = String(review[key] ?? "").length;
usage["stride_decision.explore_review.considered_refs"] = considered.join("").length;
if (Object.hasOwn(review, "result_pages")) {
if (review.disposition === "unavailable" || review.revision === null || !considered.length) {
throw new EffectRuntimeRequestError("Explore result pages require available evidence and considered references");
}
const pages = normalizeExploreResultPages(review.result_pages);
(output.explore_review as JsonObject).result_pages = pages;
usage["stride_decision.explore_review.result_pages"] = JSON.stringify(pages).length;
}
}
return [output, usage];
}
Expand Down Expand Up @@ -214,6 +223,7 @@ export function projectStrideExploreBasis(value: unknown, goal: string, agent: s
omitted_finding_refs: Math.max(0, byId.size - 6),
result_page: graph?.result_page ?? null,
rule: "Use the existing Explore view for scope/applicability, counterevidence and omitted-result reads. " +
"For considered findings outside this list, copy revision/offset/limit from up to three global result pages you read into explore_review.result_pages; writeback replays them against the source. Keep explore_review.revision unchanged. " +
"A planner selection or recorded finding does not certify execution, utility, refutation of other inputs, or spawn authority."};
}

Expand Down Expand Up @@ -241,6 +251,15 @@ export function strideReviewMismatch(decision: JsonObject, context: JsonObject):
}
if (explore.disposition === "unavailable") return "Available Explore evidence cannot be declared unreadable.";
const allowed = new Set((basis.finding_refs as JsonObject[]).map(row => row.finding_id));
const deliveredPages = Array.isArray(context.explore_result_pages) ? context.explore_result_pages as JsonObject[] : [];
for (const page of (explore.result_pages ?? []) as JsonObject[]) {
const resolved = deliveredPages.find(row => row.revision === page.revision &&
row.offset === page.offset && row.limit === page.limit);
if (page.revision !== (basis.result_page as JsonObject | null)?.revision || !resolved) {
return "Explore result pages require current source replay; reread global pages under the original Turn.";
}
for (const ref of resolved.finding_refs as string[]) allowed.add(ref);
}
if (considered.some(ref => !allowed.has(ref))) return "Explore review cites an undelivered or foreign finding.";
return null;
}
Expand Down
4 changes: 3 additions & 1 deletion loopx/state_refresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -1083,7 +1083,9 @@ def read_explore_context(read_goal_id: str, read_agent_id: str | None):

return read_replan_explore_context(
source_status={"registry": str(registry_path), "runtime_root": str(runtime_root)},
goal_id=read_goal_id, agent_id=read_agent_id)
goal_id=read_goal_id, agent_id=read_agent_id,
review_pages=((agent_vision or {}).get("stride_decision") or {}).get(
"explore_review", {}).get("result_pages"))

replan_qualification = qualify_refresh_replan_writeback(
replan_explore_reader=read_explore_context,
Expand Down
Loading
Loading