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
2 changes: 2 additions & 0 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { HANDOFF_MODES, EXECUTION_HANDOFF_MODES, type ExecutionHandoffMode } fro
import { normalizeGoalDraft } from "../../../../../loopx/control_plane/collaboration/goal_draft.js";
import { parseTurnStep, type TurnStep } from "./turn-steps.js";
import { z } from "zod";
import {normalizeExploreMeasurements} from "../../../../../loopx/control_plane/capabilities/explore_measurements.js";
import { actionSourceBasisSchema, todoUpdateSourceBasisSchema } from "./action-source-basis.js";

import {
Expand Down Expand Up @@ -2665,6 +2666,7 @@ const exploreResultPageSchema = z.object({
finding_id: z.string(), finding: z.string(), summary: z.string(), status: z.string(),
node_id: z.string(), question: z.string(), scope: z.string(), agent_id: z.string(),
evidence_refs: z.array(z.string()), last_updated_at: z.string(),
measurements: z.unknown().transform(normalizeExploreMeasurements).optional(),
linked_todos: z.array(z.object({
todo_id: z.string(), text: z.string(), status: z.string(), claimed_by: z.string(),
})),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {ExploreMeasurement, ExploreMeasurementFeedback} from "../../../../../../loopx/control_plane/capabilities/explore_measurements.js";

export const formatExploreMeasurementValue = (value: number | null, zh: boolean) => value === null
? (zh ? "未测量" : "Not measured")
: new Intl.NumberFormat(zh ? "zh-CN" : "en", {maximumSignificantDigits: 12}).format(value);

/** Values and comparisons come from Explore's shared owner, never a UI scorer. */
export function ExploreMeasurements({measurements, feedback = [], zh, title}: {
measurements: ExploreMeasurement[]; feedback?: ExploreMeasurementFeedback[]; zh: boolean; title?: string;
}) {
if (!measurements.length) return null;
const number = (value: number | null) => formatExploreMeasurementValue(value, zh);
const label = title ?? (zh ? "量化结果" : "Measured results");
return <section className="explore-measurements" aria-label={label}>
<h4>{label}</h4>
{measurements.map((measurement, index) => {
const comparison = feedback.find(item => item.metric === measurement.metric && item.unit === measurement.unit
&& item.higher_is_better === measurement.higher_is_better && item.comparison_scope === measurement.comparison_scope
&& item.input_revision === measurement.input_revision && item.evaluation_ref === measurement.evaluation_ref);
const delta = comparison?.improvement;
return <div key={index}>
<p><strong>{measurement.metric} · {number(measurement.value)} {measurement.unit}</strong>
<small> · {measurement.higher_is_better ? (zh ? "越高越好" : "Higher is better") : (zh ? "越低越好" : "Lower is better")}</small></p>
{comparison ? <p>{zh ? "同口径 best" : "Best in this scope"}: {number(comparison.best?.value ?? null)}
{delta !== null && delta !== undefined ? <> · {zh ? "相对先前 best 的改善" : "Improvement over prior best"}: {delta > 0 ? "+" : ""}{number(delta)}</> : null}</p> : null}
<details><summary>{zh ? "评测范围与来源" : "Evaluation scope and sources"}</summary>
<p>{zh ? "比较范围" : "Comparison scope"}: {measurement.comparison_scope}<br/>
{zh ? "输入" : "Input"}: {measurement.input_revision}<br/>
{zh ? "评测" : "Evaluation"}: {measurement.evaluation_ref}<br/>
{zh ? "候选" : "Candidate"}: {measurement.candidate_ref}<br/>
{zh ? "结果引用" : "Result reference"}: {measurement.evidence_ref}</p>
{comparison?.best ? <p>{zh ? "best 候选 / 来源" : "Best candidate / source"}: {comparison.best.candidate_ref} · {comparison.best.evidence_ref}</p> : null}
<p>{zh ? "同一输入、评测和预算条件下的来源记录;best 不代表当前保留版本或验收通过。" : "Source-recorded values under the same input, evaluation and budget conditions; best does not establish the retained version or acceptance."}</p>
</details>
</div>;
})}
</section>;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {useEffect, useRef, useState} from "react";
import {RefreshCw} from "lucide-react";
import {fetchExploreResults, type ExploreResultPage} from "../../data/chat";
import {ExploreMeasurements} from "./explore-measurements";

/** Read-only saved evidence, including history when Explore has been disabled. */
export function GoalExploreResults({goalId, zh}: {goalId: string; zh: boolean}) {
Expand Down Expand Up @@ -44,6 +45,7 @@ export function GoalExploreResults({goalId, zh}: {goalId: string; zh: boolean})
{selected ? <div ref={reader} tabIndex={-1} className="goal-team-result-reader" style={{overflowWrap: "anywhere"}}>
<h4>{selected.question || selected.finding}</h4>
{selected.scope ? <p>{zh ? "问题范围" : "Question scope"}: {selected.scope}</p> : null}
{selected.measurements ? <ExploreMeasurements measurements={selected.measurements} zh={zh}/> : null}
<p style={{whiteSpace: "pre-wrap", overflowWrap: "anywhere"}}>{selected.summary}</p>
<p>{zh ? "记录状态" : "Recorded status"}: {selected.status}</p>
<p>{zh ? "记录者" : "Recorded by"}: {selected.agent_id || "—"}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {sourceReadApiUrl} from "../../data/local-status-query";
import {ContextHelp} from "./context-help";
import {groupUnlinkedHistoryQuestions, layoutExploreHistory, parseExploreHistoryPage, projectExploreHistory, type ExploreHistoryEvent} from "../../../../../../loopx/control_plane/presentation/explore_history.js";
import "./goal-research-history.css";
import {ExploreMeasurements, formatExploreMeasurementValue} from "./explore-measurements";

type Selection = {nodes: string[]; day: string | null};
type Lens = "timeline" | "graph";
Expand Down Expand Up @@ -78,7 +79,7 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
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">
return <>{finding.measurements ? <ExploreMeasurements measurements={finding.measurements} zh={zh}/> : null}{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>
Expand All @@ -99,7 +100,7 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
</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>;
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.measurements ? <ExploreMeasurements measurements={event.measurements} zh={zh}/> : null}{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>;

useEffect(() => {
const pop = () => setSelection(selectedFromUrl(goalId));
Expand Down Expand Up @@ -147,6 +148,12 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
catch (failure) {return {value: null, error: String(failure)};}
}, [read.events, goalId]);
const value = history.value;
const measurementFeedback = value?.measurementFeedback ?? [];
const measurementSummary = (nodeId: string) => {
const feedback = measurementFeedback.filter(item => item.node_id === nodeId);
return feedback.length ? <ExploreMeasurements title={zh ? "最近记录与历史最佳" : "Latest recorded and historical best"}
measurements={feedback.map(item => item.latest.measurement)} feedback={feedback} zh={zh}/> : null;
};
const grouped = useMemo(() => groupUnlinkedHistoryQuestions(value?.nodes ?? [], value?.edges ?? []), [value]);
const layout = useMemo(() => layoutExploreHistory(grouped.nodes, value?.edges ?? []), [grouped, value]);
const timeline = useMemo(() => value ? buildExploreTimeline(value.nodes, value.edges, value.events) : null, [value]);
Expand Down Expand Up @@ -223,11 +230,15 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
return <path key={`parent:${item.node.id}`} className="is-grouping" d={`M${parent.x + 206},${parent.y + 47} C${parent.x + 226},${parent.y + 47} ${item.x - 20},${item.y + 47} ${item.x},${item.y + 47}`} />;
})}
</svg>
{layout.map(({node, x, y}) => <button key={node.id} type="button" className={`research-history-node is-${node.latest.status}${selection.nodes.includes(node.id) ? " is-selected" : ""}`}
{layout.map(({node, x, y}) => {
const measured = measurementFeedback.find(item => item.node_id === node.id);
return <button key={node.id} type="button" className={`research-history-node is-${node.latest.status}${selection.nodes.includes(node.id) ? " is-selected" : ""}`}
style={{left: x, top: y}} aria-pressed={selection.nodes.includes(node.id)} aria-label={`${node.latest.title} · ${stateLabel(node.latest.status)}`} onClick={() => choose(node.id)}>
<span className="research-node-meta">{node.latest.node_kind === "experiment" ? <FlaskConical size={13} aria-hidden/> : <Circle size={12} aria-hidden/>}{kindLabel(node.latest.node_kind)}<small>{stateLabel(node.latest.status)}</small></span>
<strong>{node.latest.title}</strong><span className="research-node-count"><time>{timeLabel(node.updates[0]?.recorded_at)}</time>{node.updates.length > 1 ? ` · ${node.updates.length} ${copy.changes}` : ""}{node.findings.length ? ` · ${node.findings.length} ${copy.findings}` : ""}</span>
</button>)}
<strong>{node.latest.title}</strong>{measured ? <span className="research-node-count" title={`${measured.metric} (${measured.unit}) · ${measured.comparison_scope} · ${measured.evaluation_ref}`}>
{measured.metric} {formatExploreMeasurementValue(measured.latest.measurement.value, zh)} · best {formatExploreMeasurementValue(measured.best?.value ?? null, zh)} {measured.higher_is_better ? "↑" : "↓"}
</span> : <span className="research-node-count"><time>{timeLabel(node.updates[0]?.recorded_at)}</time>{node.updates.length > 1 ? ` · ${node.updates.length} ${copy.changes}` : ""}{node.findings.length ? ` · ${node.findings.length} ${copy.findings}` : ""}</span>}
</button>})}
{grouped.groups.map((group, index) => <button type="button" key={`day:${group.day}`} className={`research-history-node research-history-group${selectedGroup === group ? " is-selected" : ""}`} style={{left: (index % 4) * 230 + 24, top: clusterY + Math.floor(index / 4) * 124}}
aria-pressed={selectedGroup === group} onClick={() => select({nodes: [], day: group.day})}>
<span className="research-node-meta"><Circle size={12} aria-hidden/>{zh ? "未关联问题" : "Unlinked questions"}</span>
Expand All @@ -239,6 +250,7 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
<p className="research-detail-kind"><GitBranch size={14} aria-hidden/>{kindLabel(selected.latest.node_kind)} · {stateLabel(selected.latest.status)}</p>
<h3>{selected.latest.title}</h3><p>{selected.latest.summary || (zh ? "此节点尚无详细说明。" : "No detail was recorded for this node.")}</p>
{selected.latest.blocked_reason ? <p className="research-history-error">{selected.latest.blocked_reason}</p> : null}
{measurementSummary(selected.id)}
<h4>{copy.evidence}</h4>
{selected.findings.length ? selected.findings.map(findingElement) : <p>{zh ? "此节点没有附属结论记录。" : "No finding is attached to this node."}</p>}
<h4>{copy.versions}</h4>{selected.latest.evidence_refs.length ? selected.latest.evidence_refs.map(ref => <code key={ref}>{ref}</code>) : <p>{zh ? "产物版本与来源引用未记录。" : "Artifact version and source references were not recorded."}</p>}
Expand All @@ -249,6 +261,7 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
{selectedMembers?.length ? <div className="research-history-detail" ref={detail} role="region" aria-label={selectedGroup ? (zh ? "按日期收纳的未关联问题" : "Unlinked questions by date") : (zh ? "相邻研究项的全部记录" : "All records for nearby research items")} tabIndex={-1} onKeyDown={event => {if (event.key === "Escape") {event.stopPropagation(); choose("", false); graph.current?.focus();}}}>
<h3>{selectedGroup ? `${selectedGroup.day || copy.unknown} · ${zh ? "未关联问题" : "Unlinked questions"}` : (zh ? `相邻研究项 · ${selectedMembers.length}` : `Nearby research items · ${selectedMembers.length}`)}</h3>
{selectedMembers.map(node => <article className="research-history-group-member" key={node.id} data-research-node={node.id}><h4>{node.latest.title}</h4><p>{stateLabel(node.latest.status)} · {node.latest.summary}</p>{node.latest.evidence_refs.map(ref => <code key={ref}>{ref}</code>)}
{measurementSummary(node.id)}
{node.findings.map(findingElement)}
{updateRecords(node.revisions)}
</article>)}
Expand Down
40 changes: 40 additions & 0 deletions loopx/capabilities/explore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,46 @@ This records producer evidence, not verified execution or utility. Findings,
replan context and the existing research-history detail retain it; omission
preserves old events. See the [local probe producer](../../../benchmark/runtime/EXECUTION_OBSERVATIONS.md).

## Quantitative findings

An ordinary result attachment can include `measurements`, saved on its existing
node-linked finding. The graph and research-history detail retain the values;
`explore turn-context` supplies bounded `measurement_feedback` to the next turn.
Manual capture uses `explore finding --node <id> --evidence-ref result:a
--measurements-json measurements.json`. The file contains one to eight items:

```json
[{"metric":"quality","value":8,"unit":"points","higher_is_better":true,
"comparison_scope":"validation:fixed-budget-v1","input_revision":"dataset-v1",
"evaluation_ref":"evaluator:v1","candidate_ref":"candidate:a","evidence_ref":"result:a"}]
```

Each result reference must belong to the finding's `evidence_refs`; ordinary
attachments also require the same `input_revision`. Use a new finding id per
evaluated candidate, and reuse an id only to correct that finding. Missing values
are explicit `null`; zero and negative values remain valid measurements.

Comparisons require the same node, metric, unit, direction, comparison scope,
input revision and evaluator. Producers must change the scope when task,
model/resource budget or evaluation conditions change, and separate native
evaluation from local proxies. Explore validates the declared comparison keys,
not their truth. It computes the latest recorded measurement, historical best
candidate/source and direction-adjusted improvement against the prior best,
before applying the context's display limit. Latest follows source-log append
order, including tied or backfilled timestamps; its ordinal is derived, not
another persisted fact. Corrected findings replace earlier
versions in comparisons; original versions remain in history. Counts are records,
not independent trials. Feedback pages reuse `--result-limit` (1..20, default 3),
`--result-offset`, `--result-revision` and the optional `--result-node` filter.
Follow `measurement_feedback_page.next_command` for additional groups, including
multiple metrics or conditions on one node. The shared revision binds all compared
sources, including manual findings; a changed source requires restarting the read.
The next-turn guidance uses this feedback to investigate
regressions or revisit better candidates before repeating work. Best does not
identify the current retained candidate, establish causality, satisfy acceptance
or authorize execution. Actual adoption and outcome benefit require subsequent
work and evidence; automatic branch rankings remain unchanged.

## At a Glance

Explore Harness is a supported, default-off optional capability for
Expand Down
32 changes: 32 additions & 0 deletions loopx/capabilities/explore/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,38 @@
不认证执行真实性或收益;finding、replan 和既有研究历程详情保留相同信息,缺省仍兼容
旧事件。参见[本地探针生产路径](../../../benchmark/runtime/EXECUTION_OBSERVATIONS.md)。

## 量化结论

普通结果附件可附带 `measurements`,保存到既有节点所属的 finding。关系图和
研究历程详情保留分数;`explore turn-context` 将有界的 `measurement_feedback`
交给下一轮。手动记录使用 `explore finding --node <id> --evidence-ref result:a
--measurements-json measurements.json`,文件包含一至八项:

```json
[{"metric":"quality","value":8,"unit":"points","higher_is_better":true,
"comparison_scope":"validation:fixed-budget-v1","input_revision":"dataset-v1",
"evaluation_ref":"evaluator:v1","candidate_ref":"candidate:a","evidence_ref":"result:a"}]
```

结果引用必须属于 finding 的 `evidence_refs`;普通附件还要求相同 `input_revision`。
每个受评测候选使用新 finding id,仅纠正同一结论时复用 id。未测量显式记为 `null`,
零分和负分仍为有效测量。

只有节点、指标、单位、方向、比较范围、输入版本和评测器全部一致时才比较。
任务、模型或资源预算、评测条件改变时,生产者必须更换比较范围;原生评测与本地
代理指标分别记录。Explore 校验声明的比较键,不认证声明真实性。它在上下文裁剪前
计算最近记录的测量、历史最佳候选及来源、相对先前最佳的方向归一化改善幅度。
最近记录按源日志追加顺序确定;时间相同或事后回填不改变此顺序,序号仅从日志
派生,不另行持久化。
纠正结论会在比较中替换旧版本,原始版本仍可在历史中读取;计数表示记录数,
不表示独立试验次数。反馈分页复用 `--result-limit`(1..20,默认 3)、
`--result-offset`、`--result-revision` 和可选的 `--result-node` 过滤。
沿 `measurement_feedback_page.next_command` 读取其余组,同一节点的多个指标或条件
也可完整翻页。共同 revision 绑定全部比较来源,包含手动 finding;来源变化须从头读取。
下一轮指导使用这些反馈排查退步、回看更好候选,再决定是否
重复尝试。best 不代表当前保留版本、因果收益、验收通过或执行授权;是否实际采用
并产生收益,仍需后续工作和证据确认。自动分支排序保持原有行为。

## 概览

LoopX Explore 是面向长程探索目标(软件研究、安全攻击面测绘、领域调研)的受支持、默认关闭的可选能力。它把“到处看看”变成有界、可观测、有门控的流程,包含三根支柱:
Expand Down
Loading
Loading