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
5 changes: 4 additions & 1 deletion loopx/capabilities/manager_context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,10 @@ content. Updating it in a release must not reject an otherwise identical saved
request. Replay retains the original entry bytes, guidance, assessment and
return history; message, brief and recipient identity conflicts still fail.

The default shared Chat handoff acknowledgment is a short user-facing preview:
The shared Chat handoff acknowledgment preserves the current audience-processed
answer, including its Markdown, and appends receipt-backed delivery and execution
status. It does not repeat the full brief or historical context after a substantive
answer. A missing or blank answer uses the 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
Expand Down
25 changes: 19 additions & 6 deletions loopx/capabilities/manager_context/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

from pathlib import Path
from collections.abc import Callable
from typing import Any
from typing import Any, TYPE_CHECKING

if TYPE_CHECKING:
from ...chat_store import ChatSessionStore

from ...agent_registry import load_goal_from_registry
from ...collaboration_mcp import Delegations
Expand Down Expand Up @@ -167,11 +170,21 @@ def handoff_response(
response: dict[str, Any],
source_authorized: Callable[[], bool],
execution_allowed: Callable[[], bool],
source_store=None,
source_store: "ChatSessionStore | None" = None,
) -> dict[str, Any]:
"""Present context delivery and separately admitted execution on one path."""
"""Append receipt-backed status to the existing audience-processed answer.

This presentation adapter does not reinterpret model prose as execution
evidence or replace the answer already shown by the Chat stream.
"""
from . import deliver

answer = response.get("message")
answer = answer if isinstance(answer, str) and answer.strip() else ""

def present(notice: str) -> str:
return answer + "\n\n" + notice if answer else notice

try:
if not source_authorized():
raise ValueError("manager connection authority is no longer available")
Expand All @@ -182,8 +195,8 @@ def handoff_response(
execution_allowed=execution_allowed)
return {**response, "proposals": [], "gate": None,
"context_handoff_receipt": receipt, "context_execution": execution,
"message": handoff_message(receipt, execution,
brief=response["context_handoff"].get("brief"))}
"message": present(handoff_message(receipt, execution,
brief=None if answer else response["context_handoff"].get("brief")))}
except (OSError, ValueError):
return {**response, "proposals": [], "gate": None,
"message": "材料尚未转交:目标绑定、来源授权或持久收件回读未通过。需要修复交接链路;没有改动任务或优先级。"}
"message": present("材料尚未转交:目标绑定、来源授权或持久收件回读未通过。需要修复交接链路;没有改动任务或优先级。")}
10 changes: 9 additions & 1 deletion tests/test_chat_project_coordination.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def test_project_chat_nested_cli_handoff_returns_once_after_restart(project, mon
before = registry.read_bytes(), (repo / "ACTIVE_GOAL_STATE.md").read_bytes()
store = ChatSessionStore(root)
controller = ChatRuntimeController(store=store, codex_bin="codex", registry_path=registry)
answer = "**Source comparison**\n\nThe periods differ.\n\n- Keep the correction.\n- Do not infer growth."

class ModelFixture:
upstream_thread_id = "fixture-project-model"
Expand All @@ -79,7 +80,8 @@ def close_session(self): pass
def start_turn(self, message, sink):
assert "OTHER_PROJECT_PRIVATE" not in message
assert '"scope": "owner_goal"' in message and '"agent_id": "coordinator"' in message
return {"schema_version": "loopx_chat_agent_response_v0", "message": "Forwarding the correction", "proposals": [], "gate": None,
sink("answer.delta", {"text": answer})
return {"schema_version": "loopx_chat_agent_response_v0", "message": answer, "proposals": [], "gate": None,
"context_handoff": {"goal_id": "research", "agent_id": "coordinator"}}

monkeypatch.setattr(controller, "capabilities", lambda: [
Expand All @@ -100,6 +102,12 @@ def cli(agent, action, *args):
message="Ask coordinator to check the corrected source with reviewer.", work_dir=repo, objective="Research delivery")
done = controller.wait_for_turn(session_id=session["session_id"], turn_id=turn["turn_id"], timeout_sec=20)
assert done["status"] == "completed", done
assert done["response"]["message"].startswith(answer + "\n\n")
assert done["response"]["message"].count(answer) == 1
streamed = [event["payload"]["text"] for event in store.events_after(session["session_id"], turn["turn_id"], None)
if event["kind"] == "answer.delta"]
assert "".join(streamed) == answer
assert ChatSessionStore(root).load_turn(session["session_id"], turn["turn_id"])["response"] == done["response"]
receipt = done["response"]["context_handoff_receipt"]
request_id = receipt["request_id"]
assert request_id in json.dumps(cli("coordinator", "read"))
Expand Down
64 changes: 61 additions & 3 deletions tests/test_manager_context_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def test_model_receives_authorized_task_choices_after_manager_context_compaction
assert "execution_binding_id" not in _turn_prompt(turn["message"], context_summary=json.dumps(unavailable))


def test_handoff_response_preserves_receipt_and_separate_execution_status(flow):
def test_handoff_response_preserves_answer_and_separate_execution_status(flow):
root, registry, (_, session, turn, request, _, _), started = flow
response = execution.handoff_response(root, registry, session=session, turn=turn,
response={"context_handoff": request, "message": "Unverified model completion claim.",
Expand All @@ -129,12 +129,70 @@ def test_handoff_response_preserves_receipt_and_separate_execution_status(flow):
assert response["context_handoff_receipt"]["status"] == "delivered"
assert response["context_execution"]["status"] == "prepared"
assert "尚未确认完成" in response["message"]
assert request["brief"]["purpose"] in response["message"]
assert "Unverified model completion claim" not in response["message"]
assert response["message"].startswith("Unverified model completion claim.\n\n")
assert request["brief"]["purpose"] not in response["message"]
# Preserving prose does not turn a model claim into execution evidence.
assert response["context_execution"]["status"] != "accepted"
assert response["proposals"] == [] and response["gate"] is None
assert len(started) == 1


@pytest.mark.parametrize("message", [None, "", " \n\t"])
def test_empty_handoff_answer_retains_compact_brief_preview(flow, message):
root, registry, (_, session, turn, request, _, _), _ = flow
response = execution.handoff_response(root, registry, session=session, turn=turn,
response={"context_handoff": request, "message": message},
source_authorized=lambda: True, execution_allowed=lambda: True)
assert response["message"].startswith("**已转交给 `worker`。**")
assert request["brief"]["purpose"] in response["message"]


@pytest.mark.parametrize("refusal", ["revoke", "delivery", "stop"])
def test_handoff_refusal_preserves_answer_without_inventing_execution(flow, monkeypatch, refusal):
root, registry, (_, session, turn, request, _, _), started = flow
original = normalize_agent_response({"context_handoff": request,
"message": f"结论:保留现有数据。\n\n- 已核对 {root / 'private' / 'source.txt'}。\n- 恢复仍须独立验收。"},
protected_paths=[root])
answer = original["message"]
assert str(root) not in answer
if refusal == "delivery":
from loopx.capabilities import manager_context
def unavailable(*args, **kwargs):
raise OSError("delivery unavailable")
monkeypatch.setattr(manager_context, "deliver", unavailable)
response = execution.handoff_response(root, registry, session=session, turn=turn,
response=original,
source_authorized=lambda: refusal != "revoke",
execution_allowed=lambda: refusal != "stop")
assert response["message"].startswith(answer + "\n\n")
assert response["message"].count(answer) == 1
assert not started
if refusal == "stop":
assert response["context_handoff_receipt"]["status"] == "delivered"
assert response["context_execution"]["submitted"] is False
assert "本次未提交受控执行" in response["message"]
else:
assert "尚未转交" in response["message"]
assert "context_handoff_receipt" not in response


def test_replayed_handoff_keeps_one_answer_and_one_original_operation(flow):
root, registry, (_, session, turn, request, _, _), started = flow
answer = "**检查结果**\n\n```text\nretain\n```\n\n原约束仍适用。"
original = {"context_handoff": request, "message": answer}
results = [execution.handoff_response(root, registry, session=session, turn=turn,
response=original, source_authorized=lambda: True, execution_allowed=lambda: True)
for _ in range(2)]
assert original["message"] == answer
assert len(started) == 1
assert results[0]["context_execution"]["operation_id"] == results[1]["context_execution"]["operation_id"]
assert results[1]["context_execution"]["replayed"]
for result in results:
assert result["message"].startswith(answer + "\n\n")
assert result["message"].count(answer) == 1
assert request["brief"]["context"] not in result["message"]


def test_handoff_scope_revocation_stops_before_inbox_delivery(flow, monkeypatch):
root, registry, (_, session, turn, request, _, _), started = flow
from loopx.capabilities import manager_context
Expand Down
Loading