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-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export type CollaborationReadback = {
schema_version: "collaboration_request_readback_v0";
request_id: string;
agent_id: string;
goal_id?: string;
decision_reason?: string;
brief: {
purpose: string;
context: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ const copy = {
acceptance: "验收要求", return: "需要回传", supplied: "已提供给接收方", pending: "等待接收方读取",
decision: "接收方判断", unknown: "尚未记录", unavailable: "暂时无法读取",
adopt: "已采纳", defer: "已暂缓", reject: "未采纳", no_change: "无需调整",
result: "结论已保存", delivered: "结论已回传", details: "查看交办内容",
result: "回复等待送达", delivered: "回复已送达", unverified: "回复送达尚未核验", details: "查看交办内容",
owner: "接收方", reason: "原因",
},
en: {
title: "Delegation brief", context: "Context & corrections", constraints: "Constraints", inputs: "Inputs",
acceptance: "Acceptance", return: "Expected return", supplied: "Supplied to receiver", pending: "Awaiting receiver read",
decision: "Receiver decision", unknown: "Not recorded", unavailable: "Readback unavailable",
adopt: "Adopted", defer: "Deferred", reject: "Rejected", no_change: "No change needed",
result: "Conclusion saved", delivered: "Conclusion returned", details: "View delegation details",
result: "Reply awaiting delivery", delivered: "Reply delivered", unverified: "Reply delivery unverified", details: "View delegation details",
owner: "Recipient", reason: "Reason",
},
};

Expand All @@ -27,12 +29,14 @@ export function CollaborationCard({ request }: { request?: CollaborationReadback
const decision = request.decision as "adopt" | "defer" | "reject" | "no_change";
const conclusion = request.returns.find((reply) => reply.phase === "conclusion");
return <section className="personal-collaboration" aria-label={c.title}>
<header><strong>{brief.purpose}</strong><span>{request.agent_id}</span></header>
<header><strong>{brief.purpose}</strong><span>{c.owner}: {request.goal_id ? `${request.goal_id} / ` : ""}{request.agent_id}</span></header>
<p className="personal-collaboration-status">
<span>{request.read_status === "supplied" ? c.supplied : request.read_status === "unavailable" ? c.unavailable : c.pending}</span>
<span>{c.decision}: {c[decision] ?? (request.decision === "unavailable" ? c.unavailable : c.unknown)}</span>
{conclusion ? <span>{conclusion.status === "delivered" ? c.delivered : c.result}</span> : null}
{conclusion ? <span>{conclusion.status === "delivered" ? c.delivered
: conclusion.status === "queued" ? c.result : c.unverified}</span> : null}
</p>
{request.decision_reason ? <p>{c.reason}: {request.decision_reason}</p> : null}
<details>
<summary>{c.details}</summary>
<h4>{c.context}</h4><p>{brief.context}</p>
Expand Down
33 changes: 33 additions & 0 deletions docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,39 @@ existing creation/connect, conversation reliability, affinity handoff and
small-team Todos; do not create duplicate planning queues. Packaging/first-use
checks run with each usable phase, not at the end of an architectural rewrite.

### Nearest user-visible exit: one request, controllable work, returned result

Qualify one concrete G0/R3 journey before expanding the feature inventory. The
user says **“Prepare a community survey for LoopX; bring me a draft.”** The App
finds the qualified existing owner, retains the request, shows its actual
disposition, and returns a readable Markdown draft with sources. The correction
**“Chinese first; do not publish.”** must reach and be adopted by the actual
receiver. Publication is outside this draft-only pilot.

Freeze GQ02/GQ04/GQ08/GQ09 with these observable exits:

- No manual Agent-id lookup, old-session link, repeated context, reminder or
result relay. Recipient identity and task purpose are visible.
- Accepted, deferred, rejected, executing and returned facts remain distinct.
A delivered deferral is not task completion. Explain delay beside the request;
private receiver reasoning never enters an external audience automatically.
- One authorized driver performs the work. Correction and scoped-stop variants
require receiver/runtime readback, not only a transport ACK.
- The draft opens in the original App conversation after reload or session
replacement. Record source, packaged UI and real native execution separately
as passed, failed, blocked or not run.

Then qualify existing G1 with **“Get a small team to check the cash-flow numbers
and resolve the disagreement.”** Two or three real workers consume versioned
inputs, independently challenge a period/unit error, adopt the revision and
return a checked synthesis. A second cycle changes the consumed input basis.
This is GQ05/GQ11–13, not a new milestone or queue.

Keep WIP on the first journey and demonstrated blockers. Reuse acceptance
recovery, GoalRef and late-return changes. Shared TS refactors accompany the
affected transaction; full migration, Lark visual parity, scale and promotional
film do not block this pilot. Component PR merges do not certify the journey.

## TS and generic async inbox: migrate with the user path

### Semantic boundary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,28 @@ LoopX 还必须核验综合结果返回原请求。这些是文档陈述,非
继续既有创建、连接、会话可靠性、affinity handoff 与小队 Todos,不创建重复计划队列。
每个可用阶段都运行打包和首次使用检查,不等到架构重写结束。

### 最近的可感知交付:一句话交办,过程可控,结果回来

先验收一条 G0/R3 真实路径,再扩充功能清单。用户说:**“给 LoopX 做份社区问卷,先给我草稿。”**
App 找到合格的已有负责人,保留请求,展示对方真实判断,在原对话返回带来源的可读 Markdown 草稿。
补充 **“先做中文,别发布。”** 后,实际接收方必须采用纠偏;发布不属于本草稿试点。

以 GQ02/GQ04/GQ08/GQ09 冻结验收:

- 用户不提供 Agent ID、旧会话链接,不重复背景,不催办、不搬运结果;接收方和任务目的清晰。
- 接收、延期、拒绝、执行、回传保持区分;延期回复送达不等于任务完成。
延期原因在请求旁可见,接收方的私有判断不得自动泄露到外部受众。
- 一个获准驱动实际干活;纠偏和定向停止分别有接收方或运行时读回,传输 ACK 不够。
- 原对话可打开草稿,刷新或更换会话后仍在;源码、打包前端、真实原生执行分别记录通过、失败、阻塞或未运行。

随后验收已有 G1:**“组个小队,核对现金流,把分歧查清楚。”**
2–3 个真实 worker 消费版本化材料,独立指出期间或单位错误,采用修订后回传综合结论;
第二轮修改输入,必须改变实际消费依据。对应 GQ05/GQ11–13,不新增里程碑或任务队列。

在制工作集中于第一条路径及其实际阻碍,复用请求恢复、GoalRef 和延迟回传工作。
TS 重构随受影响事务推进;全量迁移、Lark 视觉对齐、规模化与宣传片不阻塞试点。
组件 PR 合并不代表试点已经通过。

## TS 与通用异步 inbox:随用户路径迁移

### 语义边界
Expand Down
7 changes: 6 additions & 1 deletion docs/architecture/rfcs/loopx-overall-roadmap-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ embedding a transcript or sending an inbox ACK does not complete that transition
[App conversations and asynchronous inbox](app-conversation-and-async-inbox-v0.md)
refines R1–R3 without adding a milestone: repair ordinary input and durable
entry; qualify connect/continue, truthful activity, stop/recovery and readable
results; then complete G1's two real collaboration cycles. Prioritize these
results. The nearest observable exit is one natural App request through an
existing qualified owner to a reviewable draft, with adopted correction and
recovery, without manual owner lookup, reminders or result relay. A delivered
deferral stays deferred. Then complete G1's two real collaboration cycles.
Use the [concrete pilot and exits](app-conversation-and-async-inbox-v0.md#nearest-user-visible-exit-one-request-controllable-work-returned-result).
Prioritize these
before cross-channel visual parity or scale. Lark's reusable inbox lifecycle
should converge into the existing typed coordination owner, while provider
authentication/addressing/reactions remain in its extension. Product-facing TS
Expand Down
6 changes: 5 additions & 1 deletion docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ managed 与 attached 的工作对话都应能持续在 LoopX 中进行:沿用

[App 对话与通用异步 inbox 设计](app-conversation-and-async-inbox-v0.md)
细化既有 R1–R3:先修普通输入与持久受理,再验证连接/继续、可信状态、
停止/恢复和可读结果,随后完成 G1 两轮真实协作。它们优先于各渠道外观一致或
停止/恢复和可读结果。最近的可感知交付是一句 App 请求,经合格的已有负责人,
带着实际采用的纠偏与恢复能力,把可审阅草稿返回原对话;用户不用找人、催办或搬结果。
延期回复送达仍是延期。随后完成 G1 两轮真实协作,按
[具体试点与退出条件](app-conversation-and-async-inbox-v0.zh-CN.md) 验收。
它们优先于各渠道外观一致或
扩大规模。Lark 中可复用的 inbox 生命周期逐步收敛到现有 TS coordination owner,
认证、寻址、反应和消息限制保留在扩展。产品 TS 重构按完整用户路径推进,
不能变成逐字段 RPC 增长或全量重写前置条件。
Expand Down
9 changes: 9 additions & 0 deletions docs/product/use-cases/steward/golden-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ cause remains unknown. GQ04 permits a reasoned “do not change”; the desired
answer is not predetermined. GQ06 does not authorize public posting. GQ08 is two
separate variants: correcting scope and stopping work must not be conflated.

The nearest App pilot composes GQ02/GQ04/GQ08/GQ09: **“给 LoopX 做份社区问卷,先给我草稿。”**
/ **“Prepare a community survey for LoopX; bring me a draft.”** Follow with
**“先做中文,别发布。”** / **“Chinese first; do not publish.”** Freeze public reference
materials, one qualified existing owner and its prior context. Evaluate a real
readable draft and adopted correction in the original conversation; no posting,
manual Agent-id lookup, copied context, result relay or reminder. In the deferral
variant, explain the actual delay and retain that status after reply delivery.
This composes existing cases; it is not a fixed phrase to recognize or a pass.

## Freeze a reproducible setup before running

Use disposable Goals, isolated workspaces and a test conversation. Do not modify
Expand Down
18 changes: 18 additions & 0 deletions examples/personal-workspace-browser/chat-recovery.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,24 @@ export const chatRecoveryScenario = {
await page.setViewportSize({ width: 1512, height: 982 });
await collaboration.evaluate((node) => node.scrollIntoView({ block: "start" }));
await page.screenshot({ path: resolve(outputDir, "collaboration-brief-desktop.png"), fullPage: false, animations: "disabled" });
delegatedMessage.collaboration.goal_id = "community";
delegatedMessage.collaboration.decision = "defer";
delegatedMessage.collaboration.decision_reason = "先完成正在进行的交付;问卷尚未制作。";
delegatedMessage.collaboration.returns = [{ phase: "conclusion", status: "delivered" }];
await collaboration.getByText("接收方判断: 已暂缓", { exact: true }).waitFor({ state: "visible", timeout: 10000 });
await collaboration.getByText("原因: 先完成正在进行的交付;问卷尚未制作。", { exact: true }).waitFor({ state: "visible" });
await collaboration.getByText("回复已送达", { exact: true }).waitFor({ state: "visible" });
await collaboration.getByText("接收方: community / worker", { exact: true }).waitFor({ state: "visible" });
if ((await collaboration.innerText()).includes("结论已回传")) throw new Error("Deferred reply was presented as a completed conclusion");
await collaboration.evaluate((node) => node.scrollIntoView({ block: "start" }));
await page.screenshot({ path: resolve(outputDir, "collaboration-deferred-desktop.png"), fullPage: false, animations: "disabled" });
await page.setViewportSize({ width: 390, height: 844 });
if (await collaboration.evaluate((node) => node.scrollWidth > node.clientWidth + 1)) throw new Error("Deferred explanation overflows on mobile");
await page.screenshot({ path: resolve(outputDir, "collaboration-deferred-mobile.png"), fullPage: false, animations: "disabled" });
await page.setViewportSize({ width: 1512, height: 982 });
delegatedMessage.collaboration.returns = [{ phase: "conclusion", status: "explicit_unverified" }];
await collaboration.getByText("回复送达尚未核验", { exact: true }).waitFor({ state: "visible", timeout: 10000 });
if (api.turnRequests.length !== turnsBeforeReturn) throw new Error("Disposition readback started another model turn");
pass("collaboration-brief", "Original conversation preserves context, constraints, inputs and receiver decision without a new turn");

const returnText = "处理结论:已核验新约束并关联现有计划,无需再次追问。";
Expand Down
5 changes: 4 additions & 1 deletion loopx/capabilities/manager_context/presentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
from .tracking import _entry, _receipt
from .roundtrip import reply_status
from ...chat import redact_local_paths
from ...control_plane.collaboration import conversation_scope


def project_collaboration(store, root, session_id, messages):
session = store.load_session(session_id)
if not session or session.get("channel_id") != "manager":
if not session or not conversation_scope(session)["private_conversation"]:
return messages
result = []
for message in messages:
Expand Down Expand Up @@ -57,6 +58,7 @@ def safe(value):
"schema_version": "collaboration_request_readback_v0",
"request_id": row["request_id"],
"agent_id": row["agent_id"],
"goal_id": row["goal_id"],
"brief": safe(row["brief"]),
"read_status": "unavailable"
if read_error
Expand All @@ -66,6 +68,7 @@ def safe(value):
"decision": "unavailable"
if error
else decision.get("decision", "pending"),
"decision_reason": safe(decision.get("reason", "")) if not error else "",
"returns": reply_status(root, row),
},
}
Expand Down
4 changes: 3 additions & 1 deletion loopx/capabilities/manager_context/roundtrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,9 @@ def drain(root, registry, store, external_sender, *, now=None, cancelled=lambda:
{"status": "superseded", "reason": "conclusion_ready"},
)
continue
prefix = "处理结论" if path.stem == "conclusion" else "处理进展"
# A conclusion can be a deferral or rejection. Transport completion
# is not completion of the delegated work.
prefix = "协作回复" if path.stem == "conclusion" else "协作进展"
text = f"{prefix} · {row['agent_id']} · 委托 {row['request_id'][:8]}\n\n{reply['text']}"
# Transcript writes are independently idempotent, including when
# Lark is offline. Keep the original Turn and logical conversation.
Expand Down
54 changes: 52 additions & 2 deletions tests/test_manager_context_roundtrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def flow(tmp_path):
)
store = ChatSessionStore(tmp_path)

def create(external=False, project=False):
def create(external=False, project=False, brief=None):
session = store.create_session(
goal_id="research" if project else "loopx-manager",
agent_id="codex",
Expand Down Expand Up @@ -85,7 +85,8 @@ def create(external=False, project=False):
source_id="lark:om_fixture_source",
)
receipt = deliver(
tmp_path, registry, session=session, turn=turn, request=target
tmp_path, registry, session=session, turn=turn,
request={**target, **({"brief": brief} if brief else {})}
)
store.update_turn(
session["session_id"],
Expand Down Expand Up @@ -865,3 +866,52 @@ def verify(self, route, session, turn, text, attempt):
assert again["status"] == "explicit_unverified"
assert transport.send_calls == 1
assert transport.verify_calls == 0


@pytest.mark.parametrize("project", [False, True], ids=["steward", "goal-chat"])
@pytest.mark.parametrize("decision", ["defer", "reject", "adopt", "no_change"])
def test_reply_delivery_does_not_replace_receiver_disposition(flow, project, decision):
root, registry, store, create = flow
brief = dict(
schema_version="collaboration_brief_v0", purpose="Draft a community survey",
context="Prepare a reviewable draft before publication.",
constraints=["Do not publish"], inputs=[],
acceptance=["A readable survey with concrete questions"],
return_requirement="Return the draft or explain why it is deferred",
)
session, turn, receipt = create(project=project, brief=brief)
rid = receipt["request_id"]
reason = "Recipient assessment; private evidence at /Users/example/private/notes.md"
acknowledge(root, "research", "worker", rid, decision, reason)
report(root, "research", "worker", rid, "conclusion", "The assessment is available.")
drain(root, registry, store, lambda *_: pytest.fail("private return sent externally"))
snapshot = project_chat_session_snapshot(root, ChatSessionStore(root), session["session_id"])
collaboration = next(row["collaboration"] for row in snapshot["messages"] if row.get("collaboration"))
assert collaboration["decision"] == decision
assert collaboration["goal_id"] == "research"
assert "Recipient assessment" in collaboration["decision_reason"]
assert "/Users/example" not in collaboration["decision_reason"]
assert collaboration["returns"][0]["status"] == "delivered"
reply = next(row for row in snapshot["messages"] if row.get("origin") == "manager_followup")
assert reply["text"].startswith("协作回复 · worker")
assert "处理结论" not in reply["text"]
assert "Recipient assessment" not in reply["text"]
assert len(list((store.root / "sessions" / session["session_id"] / "turns").glob("*.json"))) == 1


def test_external_reply_never_exposes_private_receiver_reason(flow):
root, registry, store, create = flow
session, _, receipt = create(external=True)
acknowledge(root, "research", "worker", receipt["request_id"], "defer", "Private receiver rationale")
report(root, "research", "worker", receipt["request_id"], "conclusion", "This request is deferred.")
sent = []
def sender(_route, _session, _turn, payload):
sent.append(payload)
return {"ok": True, "reply_verified": True, "verification_performed": True}
drain(root, registry, store, sender)
snapshot = project_chat_session_snapshot(root, store, session["session_id"])
assert "Private receiver rationale" not in json.dumps(snapshot)
assert len(sent) == 1
assert sent[0].startswith("协作回复 · worker")
assert "Private receiver rationale" not in json.dumps(sent)
assert all(not row.get("collaboration") for row in snapshot["messages"])
Loading