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 @@ -688,6 +688,12 @@ recovery, and does not change context storage, Session identity or authority.

The shared [conversation work surface](intelligent-review-presentation-surfaces-v0.md#88-reusable-conversation-work-surface) owns adaptive reports, truthful event presentation, Turn-scoped stop/steer, reconnect and cross-channel density for all LoopX conversations. This RFC applies those same rules to the steward's owner conversation; it owns recipient selection, receiver assessment and the original-route return. A manager-specific answer format or transport must not become a second presentation authority.

The shared handoff acknowledgement presents a bounded task preview and readable
result/boundary bullets, with the current delivery or execution observation.
Selection evidence and historical context remain available in the complete
receiver brief/readback. This presentation slice does not shorten the receiver
contract, establish adoption, or qualify the full A21–A24 journey.

Routing uses §5.5 rather than a static Agent name list. For a product-design request addressed to the steward, first inspect authorized current Goal, registration, claimed work and fresh session reachability; then rank eligible receivers by responsibility and context, with model/profile fit and actual capacity as separate constraints. Explain the selected recipient or the exact gap. The receiver must acknowledge and assess the full corrected intent, then either work or defer with an owner and condition. The original conversation receives the assessment and final evidenced result through §5.6; the catalog, a stored inbox request and a spinner are three distinct incomplete states. This must pass with a real active worker plus stopped, registered-only, stale and model-mismatched decoys before advertising automatic delegation.

Delivery now prioritizes one complete supported intent→receiver→work→result journey, with the shared report, activity and stop/steer behavior needed by that journey. Do not make routing wait for presentation polish across every channel. Frontend and Lark still need separate real acceptance before an equivalence claim. Characterize and retire duplicated answer-shape prose and message/Turn correlation rules where parity is proven.
Expand Down
Binary file modified docs/assets/handoff-ack/after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 22 additions & 16 deletions loopx/capabilities/manager_context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ an audience-safe conclusion with the existing `manager-inbox report` path, in
addition to its peer result. Launch submission, receiver conclusion, canonical
acceptance and provider delivery remain separate evidence. The existing return
service replies to the original conversation; it does not start another model
thread. Plain inbox delivery now says that execution has not started.
thread. Plain inbox delivery leaves receiver activity unverified; it does not claim
that execution has not started.
The native postcondition entry retires only its exact operation-owned temporary
host input before checking a clean delivery worktree; unrelated files and actual
artifact changes still fail canonical validation. Validation recovery resumes the
Expand Down Expand Up @@ -554,23 +555,28 @@ this brief and live receiver/return facts in place. Compatible requests without
a brief retain their existing shape and identity. A changed brief under the same
ingress identity is a conflict, not a second delegation.

The shared Chat handoff acknowledgment summarizes the existing brief's purpose,
recipient-selection context, constraints, acceptance and return requirement.
Long fields and lists have a visibly abbreviated preview; the receiver gets the
complete brief unchanged. Start the context with the evidence-based selection
reason and request interpretation, rather than private chain-of-thought.
The acknowledgment names both Goal and Agent and distinguishes inbox delivery
from governed execution submission. Delivery alone leaves receiver execution
unverified; even a refused sender-side launch cannot prove that the receiver has
not independently started. Model-authored prose cannot override those facts.
This applies to the shared App, Goal and Lark handoff path; it changes neither
grants, dispatch, request identity nor the automatic result-return path.
The default shared Chat handoff acknowledgment is a short user-facing preview:
the exact recipient Agent, purpose, a bulleted result checklist (up to three
items), execution boundaries (up to two items), and the return requirement.
Each item has a visible text bound; omitted list items are counted. Selection
evidence, historical context and internal operation identities stay in the full
brief and diagnostic readback rather than flooding the ordinary conversation.
The complete normalized brief is delivered unchanged, including every context,
constraint and acceptance item. This is a presentation default change, not a
lossy receiver handoff or a new public-safe projection.

The acknowledgment distinguishes inbox delivery from governed execution
submission. Delivery alone leaves receiver execution unverified; even a refused
sender-side launch cannot prove that the receiver has not independently started.
Model-authored prose cannot override those facts. This applies to the shared App,
Goal and Lark handoff path; grants, dispatch, request identity and the automatic
result-return path keep their existing owners.

The packaged Chat [before](../../../docs/assets/handoff-ack/before.png) and
[after](../../../docs/assets/handoff-ack/after.png) views use the same synthetic
request and production context delivery, with a scripted model-protocol host.
They show presentation and receipt handling, not real-model routing or receiver
execution.
[compact preview](../../../docs/assets/handoff-ack/after.png) views use a
synthetic request and production context delivery, with a scripted model-protocol
host. They demonstrate presentation and receipt handling, not real-model routing
or receiver execution.

Registered workers can ask another worker of the **same Goal on the same host**
for help or independent review:
Expand Down
48 changes: 23 additions & 25 deletions loopx/capabilities/manager_context/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,40 +123,38 @@ def handoff_message(
Display limits do not change the full normalized brief in the inbox.
The sender's free-form message cannot establish delivery or completion.
"""
blocks = [f"已将原消息交给 {receipt['goal_id']} / {receipt['agent_id']}。"]
shortened = False
blocks = [f"**已转交给 `{receipt['agent_id']}`。**"]

def preview(value: str, limit: int) -> str:
nonlocal shortened
if len(value) <= limit:
return value
shortened = True
return value[:limit] + "…"
# One display item stays one line. This is not a semantic parser and
# never changes the complete brief delivered to the receiver.
text = " ".join(value.split())
return text if len(text) <= limit else text[:limit] + "…"

if brief:
blocks.extend(
[
"任务:" + preview(brief["purpose"], 180),
"交接依据:" + preview(brief["context"], 400),
]
)
for key, label in (("constraints", "约束"), ("acceptance", "验收")):
blocks.append(preview(brief["purpose"], 160))
for key, label, count, limit in (
("acceptance", "期待的结果", 3, 100),
("constraints", "执行边界", 2, 90),
):
items = brief[key]
if items:
shortened |= len(items) > 3
blocks.append(
label + ":" + ";".join(preview(item, 120) for item in items[:3])
)
blocks.append("回传:" + preview(brief["return_requirement"], 180))
if shortened:
blocks.append("以上为摘要,完整简报已投递。")
lines = [f"**{label}**", ""]
lines.extend("- " + preview(item, limit) for item in items[:count])
if len(items) > count:
lines.append(f"- 另有 {len(items) - count} 项,已随完整简报交接。")
blocks.append("\n".join(lines))
blocks.append("**回传要求**:" + preview(brief["return_requirement"], 120))
# Selection evidence, internal operation ids and historical context
# belong to the full brief/readback, not the default chat preview.
blocks.append("完整简报已投递;上面是摘要。")
if execution.get("submitted"):
state = "已提交受控执行;受理不代表完成。"
state = "已提交执行,尚未确认完成。"
elif execution.get("reason"):
state = "交接已保存,但本次未提交受控执行;接收方是否已开始处理尚未核实。"
state = "交接已保存,本次未提交受控执行;是否已开始处理尚未核实。"
else:
state = "已确认投递到收件箱;接收方是否已开始处理尚未核实。"
blocks.append(state + "接收方的处理结论将回到本次对话。")
state = "已送达;是否已开始处理尚未核实。"
blocks.append(state + "处理结论会回到本次对话。")
return "\n\n".join(blocks)


Expand Down
5 changes: 3 additions & 2 deletions tests/extensions/test_lark_reply_handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,16 @@ def test_short_reply_handoff_preserves_source_and_returns_once(
options = dict(route=route, text=request_text,
work_dir=tmp_path, objective="Inspect the draft.", runtime_controller=controller)
answer = answer_lark_goal_topic(**options)
assert "research / worker" in answer
assert "**已转交给 `worker`。**" in answer
assert "是否已开始处理尚未核实" in answer
if context_state == "near_record_limit":
assert "完整简报已投递" in answer
else:
assert handoff_target["brief"]["purpose"] in answer
assert handoff_target["brief"]["context"] in answer
assert handoff_target["brief"]["context"] not in answer
assert "Do not publish." in answer
entry, = pending(tmp_path, **target)["items"]
assert entry["brief"] == handoff_target["brief"]
forwarded = entry["message"]
if context_state in {"not_a_reply", "near_record_limit"}:
assert forwarded == request_text
Expand Down
2 changes: 1 addition & 1 deletion tests/test_chat_delegation_journey.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def start_turn(self, message, sink):
)
assert completed["status"] == "completed", completed
reply = completed["response"]["message"]
assert "community / writer" in reply
assert "**已转交给 `writer`。**" in reply
assert drafts[index][1] in reply
assert "是否已开始处理尚未核实" in reply
if index:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_manager_context_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def test_handoff_response_preserves_receipt_and_separate_execution_status(flow):
source_authorized=lambda: True, execution_allowed=lambda: True)
assert response["context_handoff_receipt"]["status"] == "delivered"
assert response["context_execution"]["status"] == "prepared"
assert "受理不代表完成" in response["message"]
assert "尚未确认完成" in response["message"]
assert request["brief"]["purpose"] in response["message"]
assert "Unverified model completion claim" not in response["message"]
assert response["proposals"] == [] and response["gate"] is None
Expand Down
2 changes: 1 addition & 1 deletion tests/test_manager_context_handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ def start_turn(self, message, sink):
assert created and completed["status"] == "completed", completed
response = completed["response"]
assert response["context_handoff_receipt"]["status"] == "delivered"
assert "已将原消息交给 research / worker" in response["message"]
assert "**已转交给 `worker`。**" in response["message"]
assert response["proposals"] == [] and response["gate"] is None
assert len(pending(root, "research", "worker")["items"]) == 1
assert registry.read_bytes() == original_registry
Expand Down
45 changes: 40 additions & 5 deletions tests/test_manager_handoff_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
)
def test_receipt_does_not_establish_receiver_execution(execution):
message = handoff_message(RECEIPT, execution)
assert "research / worker" in message
assert "`worker`" in message
assert "research / worker" not in message
assert "尚未启动" not in message
assert "是否已开始处理尚未核实" in message
assert "处理结论" in message and "本次对话" in message
Expand All @@ -35,17 +36,18 @@ def test_receipt_does_not_establish_receiver_execution(execution):
def test_ack_explains_existing_brief_without_changing_it():
original = {**BRIEF, "constraints": list(BRIEF["constraints"])}
message = handoff_message(RECEIPT, {"submitted": False}, brief=BRIEF)
for field in ("purpose", "context", "return_requirement"):
for field in ("purpose", "return_requirement"):
assert BRIEF[field] in message
assert BRIEF["constraints"][0] in message
assert BRIEF["acceptance"][0] in message
assert BRIEF["context"] not in message
assert BRIEF == original


def test_full_brief_is_not_dumped_into_ack():
long_brief = {**BRIEF, "context": "c" * 6000, "constraints": ["x" * 1000] * 12}
message = handoff_message(RECEIPT, {"submitted": False}, brief=long_brief)
assert len(message) < 2000
assert len(message) < 1000
assert "完整简报已投递" in message
assert long_brief["context"] == "c" * 6000
assert len(long_brief["constraints"]) == 12
Expand All @@ -55,6 +57,39 @@ def test_submission_is_separate_from_completion():
message = handoff_message(
RECEIPT, {"submitted": True, "status": "prepared"}, brief=BRIEF
)
assert "已提交受控执行" in message
assert "受理不代表完成" in message
assert "已提交执行" in message
assert "尚未确认完成" in message
assert "是否已开始处理尚未核实" not in message


def test_display_is_scannable_and_keeps_detail_out_of_the_preview():
detailed = {
**BRIEF,
"purpose": "Inspect the changed behavior.\nReturn checked findings.",
"context": "Internal selection record: request-" + "a" * 64,
"acceptance": ["First result", "Second result", "Third result", "Fourth result"],
"constraints": ["Keep scope", "Keep audience", "Preserve stop"],
}
message = handoff_message(RECEIPT, {"submitted": False}, brief=detailed)
assert "Inspect the changed behavior. Return checked findings." in message
assert "**期待的结果**\n\n- First result\n- Second result\n- Third result" in message
assert "**执行边界**\n\n- Keep scope\n- Keep audience" in message
assert "另有 1 项" in message
assert detailed["context"] not in message
assert detailed["acceptance"][-1] == "Fourth result"
assert detailed["constraints"][-1] == "Preserve stop"
assert "是否已开始处理尚未核实" in message


def test_each_preview_item_has_a_visible_bound():
huge = {
**BRIEF, "purpose": "p" * 2000, "context": "context" * 1000,
"acceptance": ["a" * 1000] * 6, "constraints": ["k" * 1000] * 6,
"return_requirement": "r" * 1000,
}
message = handoff_message(RECEIPT, {"submitted": False}, brief=huge)
assert len(message) < 1000
assert "p" * 161 not in message and "a" * 101 not in message
assert "k" * 91 not in message and "r" * 121 not in message
assert "…" in message and "另有 3 项" in message and "另有 4 项" in message
assert huge["return_requirement"] == "r" * 1000
Loading