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 @@ -4,6 +4,7 @@ import {GoalExploreResults} from "./goal-explore-results";
import {goalHierarchy} from "./goal-hierarchy";
import type {WorkspaceGoal} from "./personal-workspace-model";
import {ResearchChildGoals, type ResearchChildGoal} from "./research-child-goals";
import {GoalSourceDocument} from "./goal-source-document";
import './research-case-study.css';

type ResearchGoal = ResearchChildGoal & Pick<WorkspaceGoal, 'parentGoalId' | 'sourceStrategy'>;
Expand Down Expand Up @@ -41,6 +42,9 @@ export function GoalResearchWorkbench(props: {goalId: string; active: boolean; z
zh={props.zh} onSelect={props.onSelectGoal} directoryUrl={props.directoryReadSourceUrl}
active={props.active} refreshRevision={props.refreshRevision}/> : null}
{!props.statusNotice ? <>
{props.readSourceUrl && new URL(props.readSourceUrl, window.location.href).searchParams.has("source_namespace")
? <GoalSourceDocument key={JSON.stringify([props.goalId, props.readSourceUrl])} goalId={props.goalId}
originalGoalId={source?.original_goal_id ?? props.goalId} sourceUrl={props.readSourceUrl} zh={props.zh}/> : null}
<GoalResearchHistory {...props} originalGoalId={source?.original_goal_id}
preferHistory={Boolean(source)}
onOpenSourceTasks={source ? props.onOpenTasks : undefined}/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.source-goal-document { margin-block: 0 20px; }
.source-goal-document > summary { display: flex; gap: 8px; align-items: center; width: fit-content; cursor: pointer; min-height: 36px; }
.source-goal-document[open] > summary .source-document-chevron { transform: rotate(90deg); }
.source-goal-document > section { border-block: 1px solid var(--pw-line); padding-block: 20px; margin-top: 8px; min-width: 0; }
.source-goal-document header { display: flex; gap: 12px; align-items: center; }
.source-goal-document header h2 { margin: 0; }
.source-goal-document header > button { margin-left: auto; }
.source-goal-document button { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; min-width: 44px; padding: 8px; border: 1px solid var(--pw-line); border-radius: 6px; background: var(--pw-bg); color: var(--pw-text); font: inherit; cursor: pointer; }
.source-goal-document article { max-width: 80ch; font-size: 16px; line-height: 1.7; overflow-wrap: anywhere; }
.goal-research-workbench .source-goal-document article .personal-md { font-size: var(--text-lead); line-height: var(--leading-prose); }
.source-goal-document article pre, .source-goal-document-provenance pre { max-width: 100%; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
.source-goal-document-provenance { margin-top: 24px; font-size: 14px; }
.source-goal-document-provenance dd { margin-inline-start: 0; overflow-wrap: anywhere; }
.source-goal-document-provenance pre { font-size: 14px; line-height: 1.6; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {ChevronRight, FileText, RefreshCw} from "lucide-react";
import {useEffect, useRef, useState} from "react";
import {requestJson} from "../../data/chat";
import {sourceReadApiUrl} from "../../data/local-status-query";
import {ContextHelp} from "./context-help";
import {MarkdownText} from "./markdown";
import "./goal-source-document.css";

type Document = {text: string; sha256: string; captured: boolean};
type Read = {scope: string; document?: Document; error?: string; busy: boolean};

/** An optional original-file reader, separate from the Goal's current intent. */
export function GoalSourceDocument({goalId, originalGoalId, sourceUrl, zh}: {
goalId: string; originalGoalId: string; sourceUrl: string; zh: boolean;
}) {
const namespace = new URL(sourceUrl, window.location.href).searchParams.get("source_namespace");
const scope = JSON.stringify([goalId, originalGoalId, sourceUrl]);
const details = useRef<HTMLDetailsElement>(null);
const [open, setOpen] = useState(false), [revision, setRevision] = useState(0);
const [read, setRead] = useState<Read>({scope, busy: false});
const current = read.scope === scope ? read : {scope, busy: true};
useEffect(() => {
if (!open) return;
const controller = new AbortController();
setRead(previous => ({scope, document: previous.scope === scope ? previous.document : undefined, busy: true}));
const query = new URLSearchParams({goal_id: goalId, source_namespace: namespace ?? ""});
void requestJson<{ok: boolean; goal_id: string; source: {namespace: string}; document: Document}>(
sourceReadApiUrl(sourceUrl, `/api/chat/goal-source-document?${query}`), {signal: controller.signal},
).then(value => {
if (!value.ok || value.goal_id !== originalGoalId || value.source?.namespace !== namespace
|| typeof value.document?.text !== "string" || !value.document.text.length
|| typeof value.document.captured !== "boolean"
|| !/^[a-f0-9]{64}$/.test(value.document.sha256)
|| new TextEncoder().encode(value.document.text).length > 128 * 1024) throw new Error("document identity mismatch");
if (!controller.signal.aborted) setRead({scope, document: value.document, busy: false});
}).catch(() => {
if (!controller.signal.aborted) setRead(previous => ({scope,
document: previous.scope === scope ? previous.document : undefined, busy: false,
error: zh ? "原始任务文件暂不可读。请恢复此来源或核对文档绑定后重试。" : "The original task file is unavailable. Restore this source or check its document binding, then retry."}));
});
return () => controller.abort();
}, [open, revision, scope, goalId, originalGoalId, namespace, sourceUrl, zh]);
return <details className="source-goal-document" ref={details} onToggle={event => setOpen(event.currentTarget.open)}
onKeyDown={event => {if (event.key === "Escape") {
event.stopPropagation(); if (details.current) details.current.open = false;
details.current?.querySelector("summary")?.focus();
}}}>
<summary><ChevronRight className="source-document-chevron" size={14} aria-hidden/><FileText size={15} aria-hidden/>{zh ? "原始任务" : "Original task"}</summary>
{open ? <section aria-label={zh ? "原始任务文件" : "Original task file"}>
<header><span>{current.document?.captured ? (zh ? "捕获副本" : "Captured copy") : (zh ? "来源文件" : "Source file")}</span>
<ContextHelp label={zh ? "原始任务来源" : "Original task source"}><p>{zh
? "读取原 Goal 登记的任务文件;它与后来的目标修订和 Agent 策略分别保留。浏览不会执行文档中的命令或启动分析。"
: "The task file declared by the original Goal, kept separately from later objective revisions and Agent strategies. Reading runs no document commands or analysis."}</p></ContextHelp>
<button type="button" onClick={() => setRevision(value => value + 1)} disabled={current.busy}
aria-label={zh ? "刷新原始任务" : "Refresh original task"}><RefreshCw size={15}/></button>
</header>
{current.busy ? <p role="status">{zh ? "正在读取完整原文…" : "Reading the complete original…"}</p> : null}
{current.error ? <div role="alert"><p>{current.document ? (zh ? "刷新失败,保留上次读取的原文。" : "Refresh failed; the last original is preserved.") : current.error}</p>
<button type="button" onClick={() => setRevision(value => value + 1)}>{zh ? "重试读取原始任务" : "Retry original task"}</button></div> : null}
{current.document ? <>
<article tabIndex={0}><MarkdownText text={current.document.text}/></article>
<details className="source-goal-document-provenance"><summary>{zh ? "文档来源与版本" : "Document source and version"}</summary>
{current.document.captured ? <p>{zh ? "显示此来源配置的捕获副本,按固定内容版本核对;未重新访问原路径。来源绑定不代表独立核实。" : "Showing this source's configured captured copy, checked against its pinned content version. The original path was not reread; binding does not establish independent verification."}</p> : null}
<dl><dt>Goal</dt><dd>{originalGoalId}</dd><dt>{zh ? "来源" : "Source"}</dt><dd>{namespace}</dd><dt>SHA-256</dt><dd>{current.document.sha256}</dd></dl>
<pre tabIndex={0}>{current.document.text}</pre>
</details>
</> : null}
</section> : null}
</details>;
}
26 changes: 26 additions & 0 deletions docs/development/frontend-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,32 @@ source-read CLI's `strategies` operation. These reads retain physical namespace
and original Goal scope, recheck membership after native history IO, and fail
explicitly when complete coverage exceeds the transport's existing bounds.

The optional **Original task** disclosure reads the single
`goal_doc` / `primary_goal_document` declared by the original Goal, separately
from current objective revisions and Agent strategies. It never reconstructs
intent from a display label, a Todo or an analysis. App and prepared readers
use the source CLI's registry owner for declaration reads, including strict
envelopes; the presentation adapter does not decode a parallel registry format. They
share `GET /api/chat/goal-source-document`; prepared readers and their CLI also
support `/api/goal-source/document` and `document`. Reads validate the original
Goal, namespace and declaration again after IO, retain complete UTF-8 text up
to 128 KiB, and fail rather than truncate or substitute another document.

When a retained source's declared path is no longer accessible, an Agent may
prepare its optional `goal_document_snapshot` within existing source-connection
authority: `{declared_path, path, sha256}` binds one explicit captured file to
that exact declared reference. The source must be scoped to one Goal; paths are
not guessed from basenames. The captured bytes must match the pinned SHA-256.
The reader identifies a configured captured copy in provenance; a binding alone
does not independently verify its lineage. Keep capture proof with the source.
Only this attachment can be updated on an existing link; its connection, Goal
and parent remain fixed, and snapshot admission verifies the declared file and
hash before saving. This attachment does not change physical namespace, original Goal state,
execution authority or existing history/analysis identities. Transport locator
paths stay in the adapter; document text is preserved. Missing files, changed declarations or hashes remain explicit
read failures with retry. Opening, refreshing or closing the disclosure starts
no model or document commands; source switches hide old content immediately.

Each linked Goal carries `source_access` in the workspace projection. Its
original Goal id, tasks, explicit graph edges and native measurements remain
intact; the local link owns parent organization and optional workspace routing. Source identity pins
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,26 @@ const output = resolve(root, "output/playwright/runtime-source-workspace");
const temporary = await mkdtemp(resolve(tmpdir(), "loopx-runtime-reader-"));
const failFile = resolve(temporary, "unavailable");
const backend = spawn(resolveTestPython(), ["-u", "-c", `
import json, pathlib, sys, threading
import hashlib, json, pathlib, sys, threading
from datetime import datetime, timedelta, timezone
from loopx.presentation.runtime_workspace import RuntimeWorkspaceServer
from loopx.control_plane.status.goal_source import GoalSource
from loopx.control_plane.status.goal_source import GoalSource, GoalDocumentSnapshot
from loopx.capabilities.explore.result_log import build_explore_node_event, build_explore_edge_event
class Source(GoalSource):
def __init__(self, name):
goal_id = 'other-goal' if name == 'Distinct' else 'same-goal'
super().__init__(name,name,'/registry','/runtime','/cli','/python','/bin','local','host',goal_id)
folder = pathlib.Path(sys.argv[1]).parent / name
folder.mkdir()
document = folder / 'captured.md'
document.write_text('# '+name+' original task\\n\\n## Accepted outcome\\n\\n'+
'Compare complete outcomes, including zero, negative evidence and unknown costs. '*45+
'\\n\\n<script>window.documentExecuted=true</script>\\n\\n## Original boundary\\n\\nNo extra execution authority.\\n')
registry = folder / 'registry.json'
declared = '/original/task.md'
registry.write_text(json.dumps(dict(goals=[dict(id=goal_id,repo='/original',authority_sources=[
dict(kind='goal_doc',role='primary_goal_document',path=declared)])])))
super().__init__(name,name,str(registry),'/runtime','/cli',sys.executable,'/bin','local','host',goal_id,
goal_document_snapshot=GoalDocumentSnapshot(declared,str(document),hashlib.sha256(document.read_bytes()).hexdigest()))
def read_status(self, goal_id=None):
self.check_access(goal_id)
if pathlib.Path(sys.argv[1]).exists() and self.id == 'Fresh': raise OSError('unavailable fixture')
Expand All @@ -48,6 +59,9 @@ class Source(GoalSource):
adoption_criterion='Independent acceptance.',cost_bound='Existing authorized bound.',minimum_evidence='Original result and complete cost.'),
previous_review=dict(lesson='A replan record does not demonstrate adoption.')))))
return dict(ok=True,goal_filter=self.goal_id,goals=[dict(id=self.goal_id,registry_member=True,unique_runs=len(rows),latest_runs=rows)])
def read_registry_goal(self, goal_id):
self.check_access(goal_id)
return json.loads(pathlib.Path(self.registry).read_text())
def read_events(self, goal_id):
self.check_access(goal_id)
if self.id == 'Distinct': return []
Expand Down Expand Up @@ -81,9 +95,15 @@ try {
const url = new URL("/chat/", catalog[0].status_url);
url.search = new URLSearchParams({ goalId: "same-goal", view: "overview", statusUrl: process.env.LOOPX_RUNTIME_READER_BEFORE === "1" ? catalog[0].status_url : "/status.json",
messageSession: "saved-session", message: "saved-answer" });
let holdAnalysis = true, failAnalysis = false, releaseAnalysis, readStarted;
let holdAnalysis = process.env.LOOPX_RUNTIME_READER_BEFORE !== "1", failAnalysis = false, releaseAnalysis, readStarted;
let analysisWrites = 0;
let strategyReads = 0, failStrategies = false;
let documentReads = 0, failDocument = false;
await page.route("**/api/chat/goal-source-document?*", async route => {
assert.equal(route.request().method(),"GET"); documentReads++;
if (failDocument) return route.fulfill({status:503,json:{error:"Synthetic document failure"}});
return route.continue();
});
await page.route("**/api/chat/goal-source-strategies?*", async route => {
assert.equal(route.request().method(), "GET"); strategyReads++;
if (failStrategies) return route.fulfill({status:503,json:{error:"Synthetic source failure"}});
Expand All @@ -98,7 +118,11 @@ try {
await page.goto(url.toString());
await page.getByRole("heading", { name: "Retained research", exact: true }).waitFor();
if (process.env.LOOPX_RUNTIME_READER_BEFORE === "1") {
await page.locator(".personal-goal-tabs").getByRole("button",{name:"研究",exact:true}).click();
await page.locator(".research-history-coverage").filter({hasText:"92 / 92"}).waitFor();
await page.screenshot({path:resolve(output,"before-desktop.png")});
await page.setViewportSize({width:390,height:844});
await page.screenshot({path:resolve(output,"before-phone.png")});
} else {
await page.waitForFunction(() => new URL(location.href).searchParams.get("statusUrl")?.includes("source_namespace"));
assert.equal(new URL(page.url()).searchParams.get("messageSession"), "saved-session", "initial alias binding preserves the original answer's Session");
Expand Down Expand Up @@ -139,6 +163,28 @@ try {
await history.locator(".analysis-read-loading").waitFor({state:"hidden"});
assert.equal(analysisWrites,0,"Read recovery never starts an analysis");
await page.setViewportSize({width:1512,height:982});
assert.equal(documentReads,0,"Ordinary research never eagerly reads task documents");
const originalTask = page.locator(".source-goal-document");
await originalTask.locator(":scope > summary").click();
await originalTask.getByRole("heading",{name:"Retained original task",exact:true}).waitFor();
assert.equal(await page.evaluate(()=>window.documentExecuted),undefined,"Original document text cannot execute HTML");
assert.ok(await originalTask.locator("article p").first().evaluate(element=>parseFloat(getComputedStyle(element).fontSize)>=16));
await page.screenshot({path:resolve(output,"original-task-desktop.png")});
await page.setViewportSize({width:390,height:844});
assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth),false);
await page.screenshot({path:resolve(output,"original-task-phone.png")});
failDocument = true;
await originalTask.getByRole("button",{name:"刷新原始任务",exact:true}).click();
await originalTask.getByRole("alert").waitFor();
await originalTask.getByRole("heading",{name:"Retained original task",exact:true}).waitFor();
await page.screenshot({path:resolve(output,"original-task-failure-phone.png")});
failDocument = false;
await originalTask.getByRole("button",{name:"重试读取原始任务",exact:true}).click();
await originalTask.getByRole("alert").waitFor({state:"hidden"});
await originalTask.locator("article").focus();await page.keyboard.press("Escape");
assert.equal(await originalTask.getAttribute("open"),null);
await page.waitForFunction(()=>document.activeElement?.matches('.source-goal-document > summary'));
await page.setViewportSize({width:1512,height:982});
assert.equal(strategyReads,0,"Normal Explore reading does not eagerly read strategy history");
await page.screenshot({path:resolve(output,"strategies-before-desktop.png")});
await page.locator(".research-strategy-entry").click();
Expand Down Expand Up @@ -175,6 +221,11 @@ try {
await page.getByRole("heading",{name:"Fresh research",exact:true}).waitFor();
assert.equal(new URL(page.url()).searchParams.get("research_state"),null);
assert.equal(await strategies.count(),0,"A source switch clears the old strategy reading selection");
assert.equal(await originalTask.locator("article").count(),0,"A source switch clears the previous document synchronously");
await originalTask.locator(":scope > summary").click();
await originalTask.getByRole("heading",{name:"Fresh original task",exact:true}).waitFor();
assert.equal(await originalTask.getByRole("heading",{name:"Retained original task",exact:true}).count(),0);
await originalTask.locator(":scope > summary").click();
await page.goBack();
await strategies.getByText("Retained original objective 0",{exact:true}).waitFor();
await page.setViewportSize({width:390,height:844});
Expand Down Expand Up @@ -319,6 +370,14 @@ try {
await page.screenshot({path:resolve(output,"phone.png")});
assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth),false);
assert.equal(analysisWrites,0);
await page.evaluate(()=>localStorage.setItem("loopx-pw-locale","en"));
await page.reload();
await page.getByRole("heading",{name:"Fresh research",exact:true}).waitFor();
await originalTask.locator(":scope > summary").filter({hasText:"Original task"}).click();
await originalTask.getByRole("heading",{name:"Fresh original task",exact:true}).waitFor();
await page.getByRole("region",{name:"Original task file",exact:true}).waitFor();
await page.screenshot({path:resolve(output,"original-task-phone-en.png")});
assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth),false);
assert.deepEqual(errors,[]);
}
console.log("runtime source workspace browser smoke: ok");
Expand Down
Loading
Loading