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
36 changes: 34 additions & 2 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,38 @@ export type DelegationInventory = {
has_more: boolean; next_cursor: string | null; page_readback_complete: boolean;
};
export type {DelegationPreflight} from "./delegation-preflight.js";
export type DelegationDependency = {
operation_id: string; ref: string; sha256: string; input_ref: string;
relation: "responds_to" | "revises" | "uses"; state: "current" | "unavailable";
};
export type DelegationAdoption = {
requester_agent_id: string; consumer_operation_id: string; consumer_request_id: string;
consumer_agent_id: string; consumer_todo_id: string; state: "current" | "unavailable";
source_artifacts: Array<{ref: string; sha256: string}>;
consumer_artifacts: Array<{ref: string; sha256: string}>;
};
export type DelegationReadback = {
operation_id: string; request_id: string; agent_id: string; todo_id: string;
status: string; worker_active: boolean; recovery_required: boolean;
artifacts?: Array<{ref: string; sha256: string; text: string}>; error?: string;
dependencies?: DelegationDependency[]; adoptions?: DelegationAdoption[];
};
export function readLoopXTeamWork(sessionId: string, operationId: string) {
return requestJson<DelegationReadback>(`/api/chat/sessions/${sessionId}/loopx`, {
method: "POST", body: JSON.stringify({operation: "read", operation_id: operationId}),
});
}
// Keep inventory and selected-operation labels consistent; unknown states stay unknown.
export function delegationStateLabel(row: {status: string; worker_active?: boolean; recovery_required: boolean | null}, zh: boolean) {
if (row.status === "unavailable") return zh ? "无法核验" : "Unavailable";
if (row.status === "accepted") return zh ? "已通过当前验收" : "Currently accepted";
if (row.status === "rejected") return zh ? "未通过验收" : "Rejected";
if (row.recovery_required) return zh ? "需要恢复原执行" : "Original execution needs recovery";
if (row.status === "running" && row.worker_active) return zh ? "执行中" : "Executing";
if (row.status === "turn_returned" && row.worker_active) return zh ? "正在验收" : "Validating";
if (["prepared", "running", "turn_returned"].includes(row.status)) return zh ? "已派发,等待执行回读" : "Dispatched; awaiting execution readback";
return zh ? "状态未知" : "Unknown state";
}
export function fetchLoopXTeamWork(sessionId: string, cursor?: string) {
return requestJson<DelegationInventory>(`/api/chat/sessions/${sessionId}/loopx`, {
method: "POST", body: JSON.stringify({operation: "operations", limit: 10, ...(cursor ? {cursor} : {})}),
Expand All @@ -912,9 +944,9 @@ export function updateLoopXMode(sessionId: string, operation: string, settings?:
method: "POST", body: JSON.stringify({operation, operation_id: operationId, ...(settings ? {settings} : {})}),
});
}
export function sendLoopXMessage(sessionId: string, message: string, deliveryMode: "queue" | "inbox" | "steer") {
export function sendLoopXMessage(sessionId: string, message: string, deliveryMode: "queue" | "inbox" | "steer", operationId: string = crypto.randomUUID()) {
return requestJson<{ok: true; status: string; delivery_mode: string}>(`/api/chat/sessions/${sessionId}/loopx`, {
method: "POST", body: JSON.stringify({operation: "message", operation_id: crypto.randomUUID(), message, delivery_mode: deliveryMode}),
method: "POST", body: JSON.stringify({operation: "message", operation_id: operationId, message, delivery_mode: deliveryMode}),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,17 @@
.goal-loopx-mode-settings { grid-template-columns: 1fr; }
.goal-loopx-dialog-content { padding: 16px; }
}

.goal-team-evidence { margin-block: 16px; padding: 16px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 12px; background: var(--pw-surface, #fff); }
.goal-team-evidence h3, .goal-team-evidence h4 { margin: 0; font-size: 13px; }
.goal-team-evidence article { margin-block: 16px; }
.goal-team-evidence pre { max-height: 320px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.6 var(--font-mono, monospace); padding: 12px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 6px; }
.goal-team-evidence label { display: grid; gap: 8px; font-weight: 500; }
.goal-team-evidence textarea { box-sizing: border-box; resize: vertical; min-height: 88px; width: 100%; padding: 8px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 6px; background: var(--pw-bg, #fafafa); color: inherit; font: inherit; }
.goal-team-evidence textarea:focus-visible, .goal-team-evidence pre:focus-visible { outline: 2px solid #0070f3; outline-offset: 2px; }
.goal-team-control { border-top: 1px solid var(--pw-line, #ebebeb); padding-top: 16px; }
.goal-team-evidence [role="alert"] { color: var(--pw-red, #b42318); }
.goal-team-lineage { margin-block: 16px; padding-block: 16px; border-block: 1px solid var(--pw-line, #ebebeb); }
.goal-team-lineage ul { list-style: none; padding: 0; margin: 12px 0; }
.goal-team-lineage li { display: grid; gap: 8px; padding: 0 0 16px 16px; border-left: 2px solid var(--pw-line, #ebebeb); overflow-wrap: anywhere; }
.goal-team-lineage button { justify-self: start; max-width: 100%; overflow-wrap: anywhere; text-align: start; }
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function GoalLoopXMode({sessionId, onPrepare, onExecute, onChange}: {
if (!sessionId) return;
setBusy(true); setError("");
try {const result = await updateLoopXMode(sessionId, operation, operation === "configure" ? settings : undefined);
setSnapshot(result); onChange(result); setEditing(false);
setSnapshot(result); onChange(result); if (operation === "configure" || operation === "exit") setEditing(false);
} catch (failure) {setError(failure instanceof Error ? failure.message : String(failure));}
finally {setBusy(false);}
}
Expand All @@ -104,7 +104,12 @@ export function GoalLoopXMode({sessionId, onPrepare, onExecute, onChange}: {
{panel ? <div className="goal-loopx-dialog-content">
<header><h2 id="goal-loopx-dialog-title">{panel === "team" ? (zh ? "团队执行情况" : "Team execution") : (zh ? "运行设置" : "Execution settings")}</h2><button type="button" autoFocus aria-label={zh ? "关闭" : "Close"} onClick={() => setPanel(null)}><X size={18}/></button></header>
{error || readError ? <p className="personal-composer-error" role="alert">{error || readError}</p> : null}
{panel === "team" && sessionId ? <GoalTeamWork key={`${sessionId}:${snapshot?.settings.agent_id}:${snapshot?.settings.execution_config}`} sessionId={sessionId} members={snapshot?.members ?? []} zh={zh}/> : null}
{panel === "team" && sessionId && !readError ? <GoalTeamWork key={`${sessionId}:${snapshot?.settings.agent_id}:${snapshot?.settings.execution_config}`} sessionId={sessionId} members={snapshot?.members ?? []} zh={zh} canMessage={active && !snapshot?.paused && !readError} ingress={snapshot?.ingress ?? []}/> : null}
{panel === "team" ? <div className="goal-team-control">
<button type="button" disabled={busy || !active || Boolean(readError)} onClick={() => void mutate("pause")}>{zh ? "暂停协调员" : "Pause coordinator"}</button>
<p role="status">{snapshot?.paused ? (active ? (zh ? "已暂停后续调度,等待当前协调轮次停止回读。" : "Further dispatch paused; awaiting coordinator turn stop readback.") : (zh ? "协调员已暂停。" : "Coordinator paused.")) : null}
{zh ? "此操作不会停止已派发成员;成员状态以上次执行回读为准。当前入口不支持停止整个团队。" : "This does not stop dispatched members; their states are last-read observations. Whole-team stop is unavailable here."}</p>
</div> : null}
{editing ? <div className="goal-loopx-mode-settings"><label>{zh ? "已注册的协调身份" : "Registered coordinator"}<select value={settings.agent_id} onChange={event => setSettings({...settings, agent_id: event.target.value})}><option value="">{zh ? "选择已授权身份" : "Select authorized identity"}</option>{snapshot?.registered_agents.map(id => <option key={id} value={id}>{id}</option>)}</select></label>
<label>{zh ? "协调员总 token 额度" : "Coordinator total token allowance"}<input type="number" min={1} max={2147483647} value={settings.token_budget || ""} onChange={event => setSettings({...settings, token_budget: Number(event.target.value)})}/></label>
<label>{zh ? "成员执行绑定文件(Goal 配置)" : "Member execution bindings (Goal configuration)"}<input readOnly value={snapshot?.settings.execution_config ?? (zh ? "未配置" : "Not configured")}/></label>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {GoalTeamLineage} from "./goal-team-lineage";
import {useEffect, useRef, useState} from "react";
import {delegationStateLabel, readLoopXTeamWork, sendLoopXMessage, type DelegationReadback, type LoopXModeSnapshot} from "../../data/chat";

/** Owner-only artifact readback. Text is evidence, never rendered as executable markup. */
export function GoalTeamEvidence({sessionId, operationId, zh, canMessage, ingress, onInspect}: {
sessionId: string; operationId: string; zh: boolean; canMessage: boolean;
ingress: LoopXModeSnapshot["ingress"]; onInspect: (operationId: string) => void;
}) {
const [result, setResult] = useState<DelegationReadback | null>(null);
const [observedAt, setObservedAt] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [message, setMessage] = useState("");
const [sending, setSending] = useState(false);
const [sendError, setSendError] = useState("");
const [receipt, setReceipt] = useState<{id: string; status: string} | null>(null);
const submission = useRef<{id: string; text: string} | null>(null);
const generation = useRef(0);
useEffect(() => {
void read();
return () => {generation.current++;};
}, [sessionId, operationId]);
async function read() {
const current = ++generation.current;
setResult(null); setObservedAt(""); setError(""); setBusy(true);
try {
const value = await readLoopXTeamWork(sessionId, operationId);
if (current === generation.current) {
setResult(value); setObservedAt(new Date().toLocaleTimeString());
}
} catch (failure) {
if (current === generation.current) setError(failure instanceof Error ? failure.message : String(failure));
} finally {if (current === generation.current) setBusy(false);}
}
async function send() {
if ((!result || !message.trim()) && !submission.current) return;
// A retry after transport failure must carry the original identity and exact evidence.
const attempt = submission.current ?? {id: crypto.randomUUID(), text: [
`${zh ? "针对团队执行" : "Regarding team execution"}: ${operationId}`,
...(result?.artifacts ?? []).map(artifact => `${artifact.ref} · sha256:${artifact.sha256}`),
message.trim(),
].join("\n")};
submission.current = attempt;
setSending(true); setSendError("");
try {
const value = await sendLoopXMessage(sessionId, attempt.text, "inbox", attempt.id);
setReceipt({id: attempt.id, status: value.status});
} catch (failure) {setSendError(failure instanceof Error ? failure.message : String(failure));}
finally {setSending(false);}
}
const status = receipt ? ingress.find(row => row.client_ingress_id === receipt.id)?.status ?? receipt.status : "";
const receiptLabel = status === "pending" ? (zh ? "已进入协调员收件箱,等待读取" : "In the coordinator inbox; awaiting read")
: status === "delivered" ? (zh ? "已交给协调员;尚无应用回执" : "Delivered to coordinator; application not confirmed")
: (zh ? "投递状态待核实" : "Delivery requires reconciliation");
return <section className="goal-team-evidence" aria-label={zh ? "执行证据" : "Execution evidence"} aria-busy={busy}>
<div className="goal-team-work-actions"><h3>{zh ? "执行证据" : "Execution evidence"}</h3>
<button type="button" disabled={busy} onClick={() => void read()}>{zh ? "重新读取证据" : "Recheck evidence"}</button></div>
<code>{operationId}</code>
{busy ? <p role="status">{zh ? "正在核验绑定、验收与文件…" : "Checking bindings, acceptance and files…"}</p> : null}
{error ? <p role="alert">{zh ? "无法核验,已清除上次证据。" : "Cannot verify; previous evidence cleared."} {error}</p> : null}
{result ? <>
<p role="status"><strong>{result.agent_id} · {delegationStateLabel(result, zh)}</strong>{" · "}{observedAt}</p>
<p>{zh ? "按需读取的当前观察,不是持续在线状态;验收不代表协调员已采用。" : "An on-demand observation, not continuous liveness; acceptance does not establish coordinator adoption."}</p>
<GoalTeamLineage result={result} zh={zh} onInspect={onInspect}/>
{result.error ? <p role="alert">{result.error}</p> : null}
{result.status === "accepted" && result.artifacts?.length ? result.artifacts.map(artifact => <article key={artifact.ref}>
<h4>{artifact.ref}</h4>
<pre tabIndex={0} aria-label={`${zh ? "证据内容" : "Evidence content"}: ${artifact.ref}`}>{artifact.text}</pre>
<details><summary>{zh ? "版本与来源标识" : "Version and source identifiers"}</summary>
<code>sha256:{artifact.sha256}</code><code>{result.request_id}</code><code>{result.todo_id}</code>
</details>
</article>) : <p>{zh ? "本次读取没有可展示的已验收产物。" : "No accepted artifact is available in this readback."}</p>}
<form onSubmit={event => {event.preventDefault(); void send();}}>
<label>{zh ? "向协调员反馈此执行" : "Send feedback about this execution"}
<textarea value={message} maxLength={6000} rows={3} disabled={Boolean(submission.current)} onChange={event => setMessage(event.target.value)}/>
</label>
<p>{zh ? "附上执行标识及当前产物版本,交给原协调员收件箱;不会直接改写任务或中断成员。" : "Includes the operation and observed artifact versions in the original coordinator inbox; does not change tasks or interrupt members."}</p>
{!canMessage ? <p>{zh ? "协调员未在执行或当前观察不可用;恢复执行后可发送。" : "Coordinator inactive or observation unavailable; resume execution to send."}</p> : null}
<button type="submit" disabled={!canMessage || sending || Boolean(receipt) || !message.trim()}>{sending ? (zh ? "正在投递…" : "Sending…") : sendError ? (zh ? "重试同一条反馈" : "Retry this feedback") : (zh ? "发送反馈" : "Send feedback")}</button>
</form>
</> : null}
{sendError ? <p role="alert">{zh ? "未确认投递,请重试同一条反馈,避免重复发送。" : "Delivery not confirmed; retry the same feedback to avoid duplicates."} {sendError}</p> : null}
{receipt ? <p role="status">{receiptLabel}</p> : null}
</section>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type {DelegationReadback} from "../../data/chat";

/** Request intent, accepted output and requester decision retain distinct strength. */
export function GoalTeamLineage({result, zh, onInspect}: {
result: DelegationReadback; zh: boolean; onInspect: (operationId: string) => void;
}) {
const relations = zh ? {responds_to: "回应此版本", revises: "修订此版本", uses: "使用此版本"}
: {responds_to: "Respond to this version", revises: "Revise this version", uses: "Use this version"};
return <section className="goal-team-lineage" aria-label={zh ? "版本与采用关系" : "Versions and adoption"}>
{result.dependencies?.length ? <>
<h4>{zh ? "请求中的版本依据" : "Versions in the request"}</h4>
<ul>{result.dependencies.map((link, index) => <li key={`${link.operation_id}:${link.input_ref}:${index}`}>
<strong>{relations[link.relation] ?? (zh ? "未知关系" : "Unknown relationship")}</strong>
<button type="button" onClick={() => onInspect(link.operation_id)}>{link.operation_id}</button>
<span>{link.state === "current" ? (zh ? "源产物与接收方输入一致" : "Source and receiver input match")
: (zh ? "此版本无法核验" : "This version cannot be verified")}</span>
<details><summary>{zh ? "指定版本" : "Referenced version"}</summary>
<code>{link.ref} · sha256:{link.sha256}</code><code>{link.input_ref}</code>
</details>
</li>)}</ul>
<p>{zh ? "这是请求的关系;完成修订或采用仍需结果和回执。" : "These are requested relationships; revision or adoption still needs results and receipts."}</p>
</> : null}
<h4>{zh ? "请求方采用" : "Requester adoption"}</h4>
{result.adoptions?.length ? <ul>{result.adoptions.map(row => <li key={row.consumer_operation_id}>
<strong>{row.state === "current" ? (zh ? "已记录采用 · 后续结果验收有效" : "Adoption recorded · downstream result currently accepted")
: (zh ? "采用证据已失效或无法核验" : "Adoption evidence stale or unavailable")}</strong>
<span>{row.requester_agent_id} → {row.consumer_agent_id}</span>
<button type="button" onClick={() => onInspect(row.consumer_operation_id)}>{zh ? "查看后续结果" : "Inspect downstream result"}</button>
<details><summary>{zh ? "采用版本与结果版本" : "Source and result versions"}</summary>
{row.source_artifacts.map(artifact => <code key={`source:${artifact.ref}`}>{zh ? "来源" : "Source"}: {artifact.ref} · sha256:{artifact.sha256}</code>)}
{row.consumer_artifacts.map(artifact => <code key={`result:${artifact.ref}`}>{zh ? "结果" : "Result"}: {artifact.ref} · sha256:{artifact.sha256}</code>)}
</details>
</li>)}</ul> : <p>{zh ? "尚无请求方采用记录。" : "No requester adoption is recorded."}</p>}
</section>;
}
Loading
Loading