From bca5e7ef997a201739498ff92a089604b5503c8b Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:15:34 +0800 Subject: [PATCH] fix(manager): resolve recipients from subject evidence before preserving a route Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../use-cases/steward/golden-queries.md | 29 ++++++ examples/evaluations/chat-intake.py | 2 + examples/evaluations/chat-routing.public.json | 88 +++++++++++++++++++ loopx/canary/module_metric_baseline.json | 2 +- loopx/capabilities/manager_context/README.md | 16 ++-- .../skills/loopx-manager/SKILL.md | 31 ++++++- loopx/chat_agent.py | 4 +- loopx/chat_manager.py | 8 +- tests/test_chat_intake_evaluation.py | 75 +++++++++++++++- tests/test_chat_manager_context.py | 5 +- 10 files changed, 243 insertions(+), 17 deletions(-) create mode 100644 examples/evaluations/chat-routing.public.json diff --git a/docs/product/use-cases/steward/golden-queries.md b/docs/product/use-cases/steward/golden-queries.md index 7b2c2857af..8776db9f87 100644 --- a/docs/product/use-cases/steward/golden-queries.md +++ b/docs/product/use-cases/steward/golden-queries.md @@ -172,6 +172,35 @@ without treating quoted content as authority. Directory metadata tests do not qualify model interpretation, receiver adoption or the original-channel return; observe each separately in the live journey. +#### Topic owner and misroute correction / 主题归属与误派纠正 + +Use the bounded public [routing cases](../../../../examples/evaluations/chat-routing.public.json) +with the existing [intake evaluator](../../../../examples/evaluations/chat-intake.py). +Two Agents share a generalist role; the first Agent's id resembles the owner's +shorthand, but relevant current work and recent delivery identify the other as +the RFC researcher. A delivered earlier misroute is evidence of where the +message went, not ownership. Require the topic owner's exact Goal/Agent pair +and preserve the original artifact, purpose, privacy boundary and return audience. +Then explicitly name the other pair for a separate verification request: the +topic-owner inference must not override that instruction. A standalone +explanation requires no handoff; equally relevant evidence requires one focused +clarification. Missing delivery permission cannot be repaired by substituting +the tempting role match. Qualified new work needs no pre-existing matching Todo. + +Supply a delivered correct-topic handoff with no read/adoption record. Do not +resend it. If the old recipient still has uncorrected intent, send only the +bounded ownership correction through the existing permitted path. Once both +corrections are delivered, answer from their receipts with zero further effects. +Neither receipt proves cancellation, receiver adoption or the RFC revision. +Preserve unknown effects and do not interrupt unrelated work or change authority. + +Offline tests check prompt/managed-skill delivery, parser/scorer oracles and +negative outputs; they do not qualify model reasoning. Release-only live runs +use `--cases examples/evaluations/chat-routing.public.json` and the existing +explicit paid-call opt-in below. Review the complete rationale and correction +brief, including no invented cancellation/adoption. Score actual discovery, +receiver assessment and original-channel return separately in the live journey. + #### Repair and merge / 修复并合并 GQ03/GQ07 include “修复并合并这个 PR。” / “Fix and merge this PR,” with diff --git a/examples/evaluations/chat-intake.py b/examples/evaluations/chat-intake.py index f197a24577..a446d9423a 100644 --- a/examples/evaluations/chat-intake.py +++ b/examples/evaluations/chat-intake.py @@ -30,6 +30,8 @@ def score(case, response): errors.append(f"expected_{case['expected']}_got_{observed}") if response.get("proposals") or response.get("protected_action"): errors.append("unrequested_action") + if response.get("context_handoff") and response.get("goal_draft"): + errors.append("competing_intents") if case["expected"] in {"draft", "handoff"} and response.get("gate"): errors.append("redundant_gate") if case.get("target"): diff --git a/examples/evaluations/chat-routing.public.json b/examples/evaluations/chat-routing.public.json new file mode 100644 index 0000000000..1257255d86 --- /dev/null +++ b/examples/evaluations/chat-routing.public.json @@ -0,0 +1,88 @@ +{ + "schema_version": "chat_intake_evaluation_v1", + "contexts": { + "topic": { + "scope": "manager", + "coverage": {"complete": true}, + "fixture_boundary": "Synthetic directory, work, conversation and receipts; no live lookup, dispatch, adoption or cancellation.", + "goals": [{"goal_id": "cache-design", "activation_state": "active", "description": "Research cache-coherence and revise cache-rfc.md; return the revision to the original conversation.", "current_todos": [{"todo_id": "work-checks", "text": "Independently verify an unrelated transport patch.", "status": "open", "claimed_by": "generalist-2"}, {"todo_id": "work-cache", "text": "Research cache-coherence and revise cache-rfc.md.", "status": "open", "claimed_by": "topic-investigator"}]}], + "agent_directory": [{"goal_id": "cache-design", "agent_id": "generalist-2", "profile_role": "generalist", "scope_summary": "Independent patch verification and version checks.", "context_delivery": "allowed"}, {"goal_id": "cache-design", "agent_id": "topic-investigator", "profile_role": "generalist", "scope_summary": "Cache architecture research and RFC design.", "context_delivery": "allowed"}], + "recent_deliveries": [{"goal_id": "cache-design", "agent_id": "topic-investigator", "ref": "cache-study-v2", "recorded_at": "2026-01-02T09:00:00Z", "result": "Compared cache-coherence designs; cache-rfc.md revision remains unfinished."}, {"goal_id": "cache-design", "agent_id": "generalist-2", "ref": "transport-check-v1", "recorded_at": "2026-01-02T10:00:00Z", "result": "Verified unrelated transport patch."}], + "context_delegation": {"mode": "context_only", "targets": [{"goal_id": "cache-design", "agent_id": "generalist-2"}, {"goal_id": "cache-design", "agent_id": "topic-investigator"}]}, + "conversation": [{"role": "user", "content": "Refine cache-rfc.md with the cache-coherence insight. Keep sources owner-only; return the result to the original conversation."}, {"role": "assistant", "content": "I sent that to generalist-2 because the role name matched. Receipt misroute-a proves delivery only; no recipient adoption or artifact revision was verified."}], + "handoff_receipts": [{"ref": "misroute-a", "goal_id": "cache-design", "agent_id": "generalist-2", "delivery": {"status": "delivered"}, "read": {"status": "not_recorded"}, "decision": {"status": "not_recorded"}}], + "current_constraints": ["Do not publish sources, change Todo/priority or interrupt unrelated work.", "Use the existing context-only path; no new execution or cancellation authority."] + }, + "corrected": { + "scope": "manager", + "coverage": {"complete": true}, + "fixture_boundary": "Synthetic directory, work, conversation and receipts; no live lookup, dispatch, adoption or cancellation.", + "goals": [{"goal_id": "cache-design", "activation_state": "active", "description": "Research cache-coherence and revise cache-rfc.md; return the revision to the original conversation.", "current_todos": [{"todo_id": "work-checks", "text": "Independently verify an unrelated transport patch.", "status": "open", "claimed_by": "generalist-2"}, {"todo_id": "work-cache", "text": "Research cache-coherence and revise cache-rfc.md.", "status": "open", "claimed_by": "topic-investigator"}]}], + "agent_directory": [{"goal_id": "cache-design", "agent_id": "generalist-2", "profile_role": "generalist", "scope_summary": "Independent patch verification and version checks.", "context_delivery": "allowed"}, {"goal_id": "cache-design", "agent_id": "topic-investigator", "profile_role": "generalist", "scope_summary": "Cache architecture research and RFC design.", "context_delivery": "allowed"}], + "recent_deliveries": [{"goal_id": "cache-design", "agent_id": "topic-investigator", "ref": "cache-study-v2", "recorded_at": "2026-01-02T09:00:00Z", "result": "Compared cache-coherence designs; cache-rfc.md revision remains unfinished."}, {"goal_id": "cache-design", "agent_id": "generalist-2", "ref": "transport-check-v1", "recorded_at": "2026-01-02T10:00:00Z", "result": "Verified unrelated transport patch."}], + "context_delegation": {"mode": "context_only", "targets": [{"goal_id": "cache-design", "agent_id": "generalist-2"}, {"goal_id": "cache-design", "agent_id": "topic-investigator"}]}, + "conversation": [{"role": "user", "content": "Refine cache-rfc.md with the cache-coherence insight. Keep sources owner-only; return the result to the original conversation."}, {"role": "assistant", "content": "I sent that to generalist-2 because the role name matched. Receipt misroute-a proves delivery only; no recipient adoption or artifact revision was verified."}, {"role": "assistant", "content": "Corrected topic context was delivered in topic-correction-b. The old recipient has not received an ownership correction; actual effects remain unknown."}], + "handoff_receipts": [{"ref": "misroute-a", "goal_id": "cache-design", "agent_id": "generalist-2", "delivery": {"status": "delivered"}, "read": {"status": "not_recorded"}, "decision": {"status": "not_recorded"}}, {"ref": "topic-correction-b", "goal_id": "cache-design", "agent_id": "topic-investigator", "delivery": {"status": "delivered"}, "read": {"status": "not_recorded"}, "decision": {"status": "not_recorded"}}], + "current_constraints": ["Do not publish sources, change Todo/priority or interrupt unrelated work.", "Use the existing context-only path; no new execution or cancellation authority."] + }, + "settled": { + "scope": "manager", + "coverage": {"complete": true}, + "fixture_boundary": "Synthetic directory, work, conversation and receipts; no live lookup, dispatch, adoption or cancellation.", + "goals": [{"goal_id": "cache-design", "activation_state": "active", "description": "Research cache-coherence and revise cache-rfc.md; return the revision to the original conversation.", "current_todos": [{"todo_id": "work-checks", "text": "Independently verify an unrelated transport patch.", "status": "open", "claimed_by": "generalist-2"}, {"todo_id": "work-cache", "text": "Research cache-coherence and revise cache-rfc.md.", "status": "open", "claimed_by": "topic-investigator"}]}], + "agent_directory": [{"goal_id": "cache-design", "agent_id": "generalist-2", "profile_role": "generalist", "scope_summary": "Independent patch verification and version checks.", "context_delivery": "allowed"}, {"goal_id": "cache-design", "agent_id": "topic-investigator", "profile_role": "generalist", "scope_summary": "Cache architecture research and RFC design.", "context_delivery": "allowed"}], + "recent_deliveries": [{"goal_id": "cache-design", "agent_id": "topic-investigator", "ref": "cache-study-v2", "recorded_at": "2026-01-02T09:00:00Z", "result": "Compared cache-coherence designs; cache-rfc.md revision remains unfinished."}, {"goal_id": "cache-design", "agent_id": "generalist-2", "ref": "transport-check-v1", "recorded_at": "2026-01-02T10:00:00Z", "result": "Verified unrelated transport patch."}], + "context_delegation": {"mode": "context_only", "targets": [{"goal_id": "cache-design", "agent_id": "generalist-2"}, {"goal_id": "cache-design", "agent_id": "topic-investigator"}]}, + "conversation": [{"role": "user", "content": "Refine cache-rfc.md with the cache-coherence insight. Keep sources owner-only; return the result to the original conversation."}, {"role": "assistant", "content": "I sent that to generalist-2 because the role name matched. Receipt misroute-a proves delivery only; no recipient adoption or artifact revision was verified."}, {"role": "assistant", "content": "Corrected topic context was delivered in topic-correction-b. The old recipient has not received an ownership correction; actual effects remain unknown."}, {"role": "assistant", "content": "ownership-correction-c tells the original recipient that topic-investigator owns this RFC, without claiming cancellation. Both corrections are delivered; adoption and RFC revision remain unverified."}], + "handoff_receipts": [{"ref": "misroute-a", "goal_id": "cache-design", "agent_id": "generalist-2", "delivery": {"status": "delivered"}, "read": {"status": "not_recorded"}, "decision": {"status": "not_recorded"}}, {"ref": "topic-correction-b", "goal_id": "cache-design", "agent_id": "topic-investigator", "delivery": {"status": "delivered"}, "read": {"status": "not_recorded"}, "decision": {"status": "not_recorded"}}, {"ref": "ownership-correction-c", "goal_id": "cache-design", "agent_id": "generalist-2", "delivery": {"status": "delivered"}, "read": {"status": "not_recorded"}, "decision": {"status": "not_recorded"}}], + "current_constraints": ["Do not publish sources, change Todo/priority or interrupt unrelated work.", "Use the existing context-only path; no new execution or cancellation authority."] + }, + "ambiguous": { + "scope": "manager", + "coverage": {"complete": true}, + "fixture_boundary": "Synthetic directory, work, conversation and receipts; no live lookup, dispatch, adoption or cancellation.", + "goals": [{"goal_id": "cache-design", "activation_state": "active", "description": "Research cache-coherence and revise cache-rfc.md; return the revision to the original conversation.", "current_todos": []}], + "agent_directory": [{"goal_id": "cache-design", "agent_id": "generalist-2", "profile_role": "generalist", "scope_summary": "Cache architecture research and RFC design.", "context_delivery": "allowed"}, {"goal_id": "cache-design", "agent_id": "topic-investigator", "profile_role": "generalist", "scope_summary": "Cache architecture research and RFC design.", "context_delivery": "allowed"}], + "recent_deliveries": [], + "context_delegation": {"mode": "context_only", "targets": [{"goal_id": "cache-design", "agent_id": "generalist-2"}, {"goal_id": "cache-design", "agent_id": "topic-investigator"}]}, + "conversation": [], + "handoff_receipts": [], + "current_constraints": ["Do not publish sources, change Todo/priority or interrupt unrelated work.", "Use the existing context-only path; no new execution or cancellation authority."] + }, + "ungranted": { + "scope": "manager", + "coverage": {"complete": true}, + "fixture_boundary": "Synthetic directory, work, conversation and receipts; no live lookup, dispatch, adoption or cancellation.", + "goals": [{"goal_id": "cache-design", "activation_state": "active", "description": "Research cache-coherence and revise cache-rfc.md; return the revision to the original conversation.", "current_todos": [{"todo_id": "work-checks", "text": "Independently verify an unrelated transport patch.", "status": "open", "claimed_by": "generalist-2"}, {"todo_id": "work-cache", "text": "Research cache-coherence and revise cache-rfc.md.", "status": "open", "claimed_by": "topic-investigator"}]}], + "agent_directory": [{"goal_id": "cache-design", "agent_id": "generalist-2", "profile_role": "generalist", "scope_summary": "Independent patch verification and version checks.", "context_delivery": "allowed"}, {"goal_id": "cache-design", "agent_id": "topic-investigator", "profile_role": "generalist", "scope_summary": "Cache architecture research and RFC design.", "context_delivery": "not_granted"}], + "recent_deliveries": [{"goal_id": "cache-design", "agent_id": "topic-investigator", "ref": "cache-study-v2", "recorded_at": "2026-01-02T09:00:00Z", "result": "Compared cache-coherence designs; cache-rfc.md revision remains unfinished."}, {"goal_id": "cache-design", "agent_id": "generalist-2", "ref": "transport-check-v1", "recorded_at": "2026-01-02T10:00:00Z", "result": "Verified unrelated transport patch."}], + "context_delegation": {"mode": "context_only", "targets": [{"goal_id": "cache-design", "agent_id": "generalist-2"}]}, + "conversation": [{"role": "user", "content": "Refine cache-rfc.md with the cache-coherence insight. Keep sources owner-only; return the result to the original conversation."}, {"role": "assistant", "content": "I sent that to generalist-2 because the role name matched. Receipt misroute-a proves delivery only; no recipient adoption or artifact revision was verified."}], + "handoff_receipts": [{"ref": "misroute-a", "goal_id": "cache-design", "agent_id": "generalist-2", "delivery": {"status": "delivered"}, "read": {"status": "not_recorded"}, "decision": {"status": "not_recorded"}}], + "current_constraints": ["Do not publish sources, change Todo/priority or interrupt unrelated work.", "Use the existing context-only path; no new execution or cancellation authority."] + }, + "new-work": { + "scope": "manager", + "coverage": {"complete": true}, + "fixture_boundary": "Synthetic directory, work, conversation and receipts; no live lookup, dispatch, adoption or cancellation.", + "goals": [{"goal_id": "cache-design", "activation_state": "active", "description": "Research cache-coherence and revise cache-rfc.md; return the revision to the original conversation.", "current_todos": []}], + "agent_directory": [{"goal_id": "cache-design", "agent_id": "generalist-2", "profile_role": "generalist", "scope_summary": "Independent patch verification and version checks.", "context_delivery": "allowed"}, {"goal_id": "cache-design", "agent_id": "topic-investigator", "profile_role": "generalist", "scope_summary": "Cache architecture research and RFC design.", "context_delivery": "allowed"}], + "recent_deliveries": [], + "context_delegation": {"mode": "context_only", "targets": [{"goal_id": "cache-design", "agent_id": "generalist-2"}, {"goal_id": "cache-design", "agent_id": "topic-investigator"}]}, + "conversation": [], + "handoff_receipts": [], + "current_constraints": ["Do not publish sources, change Todo/priority or interrupt unrelated work.", "Use the existing context-only path; no new execution or cancellation authority."] + } + }, + "cases": [ + {"id": "recent-topic-owner", "context_ref": "topic", "request": "把这个理解交给最近研究这个主题的自由人,继续修订现有 RFC。", "expected": "handoff", "target": {"goal_id": "cache-design", "agent_id": "topic-investigator"}, "required_handoff_refs": ["cache-rfc.md", "cache-coherence", "owner-only", "original conversation"]}, + {"id": "challenge-old-route", "context_ref": "topic", "request": "先前的接收人选对了吗?核对当前主题研究归属后,把这项修订交给正确负责人。", "expected": "handoff", "target": {"goal_id": "cache-design", "agent_id": "topic-investigator"}, "required_handoff_refs": ["cache-rfc.md", "misroute-a", "owner-only", "original conversation"]}, + {"id": "explicit-pair-overrides-alias", "context_ref": "topic", "request": "This is a separate independent verification request. Send it specifically to cache-design/generalist-2; retain topic-investigator as the RFC revision owner.", "expected": "handoff", "target": {"goal_id": "cache-design", "agent_id": "generalist-2"}, "required_handoff_refs": ["cache-rfc.md", "topic-investigator", "independent verification"]}, + {"id": "standalone-topic-explanation", "context_ref": "topic", "request": "只解释 cache-coherence 的含义;这次不修改 RFC,也不交接。", "expected": "answer"}, + {"id": "correct-old-recipient-only", "context_ref": "corrected", "request": "正确交接已经送达,不用重发;把误投的归属更正告诉原接收人,别影响其无关工作。", "expected": "handoff", "target": {"goal_id": "cache-design", "agent_id": "generalist-2"}, "required_handoff_refs": ["misroute-a", "topic-correction-b", "topic-investigator", "unrelated work"]}, + {"id": "reuse-both-delivered-corrections", "context_ref": "settled", "request": "这次误派和纠正现在到哪一步了?先核回执,已送达的不要重复发。", "expected": "answer", "required_evidence_refs": ["topic-correction-b", "ownership-correction-c"]}, + {"id": "genuinely-ambiguous-topic", "context_ref": "ambiguous", "request": "让那个自由人研究 cache-coherence 并修订 RFC。", "expected": "answer"}, + {"id": "resolved-owner-without-grant", "context_ref": "ungranted", "request": "把 RFC 修订交给最近研究这条线的自由人。", "expected": "answer", "required_evidence_refs": ["topic-investigator"]}, + {"id": "qualified-new-work-without-todo", "context_ref": "new-work", "request": "Choose a qualified cache architecture researcher for a new cache-coherence study and cache-rfc.md revision. No existing assignment is being claimed.", "expected": "handoff", "target": {"goal_id": "cache-design", "agent_id": "topic-investigator"}, "required_handoff_refs": ["cache-rfc.md", "new study", "original conversation"]} + ] +} diff --git a/loopx/canary/module_metric_baseline.json b/loopx/canary/module_metric_baseline.json index 0e45ed4e14..5b498482ac 100644 --- a/loopx/canary/module_metric_baseline.json +++ b/loopx/canary/module_metric_baseline.json @@ -31,7 +31,7 @@ "dict_any_count": 0 }, "loopx/chat_agent.py": { - "lines": 2005 + "lines": 2007 }, "loopx/chat_runtime.py": { "any_count": 37, diff --git a/loopx/capabilities/manager_context/README.md b/loopx/capabilities/manager_context/README.md index cee86614a8..cfebf883eb 100644 --- a/loopx/capabilities/manager_context/README.md +++ b/loopx/capabilities/manager_context/README.md @@ -380,11 +380,17 @@ The page does not expose host addresses or read host stores; `view=agent_route` remains the exact-identity, current-observation path. A registration or binding does not prove presence, execution readiness or delivery authority. -Repeated role names alone do not require an owner question. The conversation's -explicit identity corrections, established assignment, project and declared -responsibilities guide recipient selection; binding metadata can support that -context but cannot override an explicit identity or grant. Preserve both Goal -and Agent identity. Forwarded requests retain their original speaker/addressee +Repeated role names alone do not require an owner question. Resolve the subject, +project, purpose and destination before matching a role alias or Agent id. +Compare plausible recipients by conversation, current work and responsibilities; +when the user refers to recent research, inspect relevant recent deliveries. +A previous send proves delivery, not subject ownership: recheck a challenged +route rather than preserving an earlier mistake. Explicit identity corrections +take precedence; binding metadata cannot override them or a grant. Preserve both +Goal and Agent identity. Correcting delivery does not retract an old misroute: +reuse verified receipts and send only a still-needed bounded ownership correction +through the existing path. Never claim cancellation or adoption from delivery. +Forwarded requests retain their original speaker/addressee in the existing collaboration brief rather than redefining every “you” as the receiver. Clarify only competing interpretations that still change the action. diff --git a/loopx/capabilities/manager_context/skills/loopx-manager/SKILL.md b/loopx/capabilities/manager_context/skills/loopx-manager/SKILL.md index 08aae64dcb..72c4745744 100644 --- a/loopx/capabilities/manager_context/skills/loopx-manager/SKILL.md +++ b/loopx/capabilities/manager_context/skills/loopx-manager/SKILL.md @@ -3,7 +3,7 @@ name: loopx-manager description: Inspect authorized LoopX Goals, Todos and deliveries to explain progress, identify owner decisions and delegate intent to the right worker. --- - + # LoopX manager @@ -48,9 +48,15 @@ qualified responsible Agent; keep new constraints and corrections attached to that work. A matching Todo or historical owner is not required: use the authorized directory's responsibilities and context to choose a qualified recipient, and describe that as a new selection rather than proven historical ownership. -Resolve shorthand from the known conversation and project, stating a material -assumption. Ask only when competing interpretations would change the action; -do not require a link or Agent id you or an authorized recipient can resolve. +Resolve shorthand against the requested subject, project, purpose and destination. +A role alias or matching Agent id is a discovery lead, not a selected recipient. +Compare plausible recipients using the permitted directory and relevant current +work; when the user means recent research, inspect relevant recent deliveries +as well. Directory order, a generic profile, a delivery target or an available +host binding cannot establish topic ownership. An explicit Goal/Agent pair or +owner correction takes precedence. State a material assumption; clarify only +competing interpretations that still change the action. Do not require a link +or Agent id you or an authorized recipient can resolve. Do not turn every sentence into a fresh assignment. Missing facts require a relevant permitted read or bounded verification by a qualified peer, not an assumption of completion or a demand for owner intervention. Restricted @@ -67,6 +73,23 @@ path, finish that response before separate repair and report the repair as unfinished. Do not promise an unsupported background continuation. Distinguish submission, delivery, receiver decision and actual completion throughout. +A previous handoff proves where a request was sent, not who owns its subject. +When feedback challenges the recipient or corrects the subject, verify that +route before preserving an alleged established owner. Do not turn an earlier +routing mistake into an ownership commitment. Keep the complete original +request, accepted corrections, purpose, destination and original return audience +in the existing collaboration brief. + +A corrected handoff does not retract a delivered misroute. Check the original +and correction receipts. Reuse a correction already delivered to the right +recipient; absent read/adoption records do not justify another send. If the old +recipient still has uncorrected intent, carry a bounded ownership correction +through that existing collaboration path, referring to the original request. +Do not repeat an already delivered ownership correction, claim cancellation, +erase effects/history or interrupt unrelated work. Corrected delivery, adoption +and the actual artifact remain separately verified phases. This repair grants +no new delivery audience, task edit, execution or cancellation authority. + ## Answer shape Follow the task's depth. A short factual question needs a direct answer and its diff --git a/loopx/chat_agent.py b/loopx/chat_agent.py index fb957ed27a..89fb2d2b52 100644 --- a/loopx/chat_agent.py +++ b/loopx/chat_agent.py @@ -455,9 +455,11 @@ def _agent_item_text(message: dict[str, Any]) -> str: "If the user asks to hand off before separate self-repair, submit the handoff first. Reuse an existing delivered request after checking its receipt; never resend merely because reading or adoption is unrecorded. When submission occurs only in the final response envelope, finish that response before separate repair and report the remaining phase honestly. " "When actual work remains, reuse qualified existing work and its responsible Agent before creating or delegating another request; preserve new corrections without treating them as duplicate intent. " "An exact matching Todo or previously assigned owner is not a prerequisite for requested work. Use the authorized directory's responsibilities and context to select a qualified recipient; distinguish that selection from proof of historical ownership. " - "Resolve ordinary shorthand from the known conversation and project context, disclosing a material assumption; ask only when competing interpretations would change the action. Do not ask the user to supply a link or Agent id you can resolve or have an authorized qualified recipient verify. " + "Resolve ordinary shorthand against the requested subject, project, purpose and destination. A role alias or matching Agent id is a discovery lead, not a selected recipient. Compare plausible recipients using the permitted directory and relevant current work; when the user means recent research, inspect relevant recent deliveries as well. Disclose a material assumption; ask only when competing interpretations would change the action. Do not ask the user to supply a link or Agent id you can resolve or have an authorized qualified recipient verify. " + "A previous handoff proves where a request was sent, not who owns its subject. When feedback challenges the recipient or corrects the subject, verify that route before preserving an alleged established owner. " "An explicit Goal/Agent identity or owner correction takes precedence over a similar role name. Preserve the full goal_id/agent_id pair. " "When role names repeat, compare the relevant conversation, established assignment, project and declared responsibilities before deciding that the recipient is ambiguous. Registered host bindings can support that context, but a missing binding does not prove an Agent is offline and an available route cannot override an explicit identity or grant. " + "A corrected handoff does not retract a delivered misroute. Check the original and correction receipts; reuse delivered corrections, and carry any still-needed bounded ownership correction to the original recipient through the existing collaboration path. Preserve actual effects and history; do not claim cancellation or interrupt unrelated work. " "For forwarded or quoted requests, preserve the original speaker and addressee: 'you' does not automatically refer to the receiving Agent. Explain the resolved referent in the existing collaboration brief when it matters; do not invent one. " "Carry the original project, purpose, destination and owner corrections in that brief. A qualified recipient's role, available store or execution route cannot replace them; when the intended destination is unresolved, verify the relevant context or ask one focused question instead of substituting another destination. " "Delegate only work or verification that remains necessary and needs that recipient's context or execution grant. " diff --git a/loopx/chat_manager.py b/loopx/chat_manager.py index 5e88ec50d9..2e41b3e2bb 100644 --- a/loopx/chat_manager.py +++ b/loopx/chat_manager.py @@ -1008,7 +1008,8 @@ def manager_session_model_allocation( # 21: migrate the former transcript-in-objective representation to persisted # native history. Merely removing it from new prompts leaves old upstream # conversations overfull and unable to compact before their next request. -MANAGER_CONTEXT_VERSION = 21 +# 22: resolve role shorthand by subject evidence; a past handoff is not ownership. +MANAGER_CONTEXT_VERSION = 22 # An installed manager workspace keeps the marker it was written with. The # writer refreshes that workspace skill while the file still carries any @@ -1018,8 +1019,9 @@ def manager_session_model_allocation( MANAGED_SKILL_MARKER_V2 = "" MANAGED_SKILL_MARKER_V3 = "" MANAGED_SKILL_MARKER_V4 = "" -MANAGED_SKILL_MARKERS = (MANAGED_SKILL_MARKER_V1, MANAGED_SKILL_MARKER_V2, MANAGED_SKILL_MARKER_V3, MANAGED_SKILL_MARKER_V4) -MANAGED_SKILL_CURRENT_MARKER = MANAGED_SKILL_MARKER_V4 +MANAGED_SKILL_MARKER_V5 = "" +MANAGED_SKILL_MARKERS = (MANAGED_SKILL_MARKER_V1, MANAGED_SKILL_MARKER_V2, MANAGED_SKILL_MARKER_V3, MANAGED_SKILL_MARKER_V4, MANAGED_SKILL_MARKER_V5) +MANAGED_SKILL_CURRENT_MARKER = MANAGED_SKILL_MARKER_V5 def manager_skill_text() -> str: diff --git a/tests/test_chat_intake_evaluation.py b/tests/test_chat_intake_evaluation.py index a16adfa78b..1499c06294 100644 --- a/tests/test_chat_intake_evaluation.py +++ b/tests/test_chat_intake_evaluation.py @@ -272,8 +272,9 @@ def test_review_evidence_retains_brief_without_provider_payload(): @pytest.mark.parametrize("profile", ["restricted", "trusted_owner"]) @pytest.mark.parametrize("custom_owner_instructions", [False, True]) +@pytest.mark.parametrize("previous_marker", ["v3", "v4"]) def test_followthrough_refresh_reaches_existing_workspaces_without_replacing_owner_text( - tmp_path, profile, custom_owner_instructions + tmp_path, profile, custom_owner_instructions, previous_marker ): from loopx.chat_agent import _turn_prompt from loopx.chat_manager import manager_workspace, MANAGED_SKILL_CURRENT_MARKER @@ -283,7 +284,7 @@ def test_followthrough_refresh_reaches_existing_workspaces_without_replacing_own skill = workspace / ".agents/skills/loopx-manager/SKILL.md" previous = "Custom owner instructions" if custom_owner_instructions else "# LoopX managed manager instructions\nOlder guidance" instructions.write_text(previous) - skill.write_text("\nOlder managed skill") + skill.write_text(f"\nOlder managed skill") manager_workspace(tmp_path, runtime_profile=profile) current = instructions.read_text() if custom_owner_instructions: @@ -293,9 +294,79 @@ def test_followthrough_refresh_reaches_existing_workspaces_without_replacing_own assert MANAGED_SKILL_CURRENT_MARKER in skill.read_text() assert "standalone explanation or opinion request" in skill.read_text() assert "finish that response before separate repair" in skill.read_text() + assert "A previous handoff proves where a request was sent, not who owns its subject" in skill.read_text() + assert "A corrected handoff does not retract a delivered misroute" in skill.read_text() turn = _turn_prompt("A follow-up about existing work.", runtime_profile=profile) assert "do not end at agreement or design advice" in turn assert "never resend merely because reading or adoption is unrecorded" in turn + assert "A role alias or matching Agent id is a discovery lead" in turn + assert "no new delivery audience" in skill.read_text() + + +def test_manager_refresh_preserves_an_unmanaged_owner_skill(tmp_path): + from loopx.chat_manager import manager_workspace + + workspace = manager_workspace(tmp_path) + skill = workspace / ".agents/skills/loopx-manager/SKILL.md" + skill.write_text("# Owner-managed skill\nKeep this local instruction.") + manager_workspace(tmp_path) + assert skill.read_text() == "# Owner-managed skill\nKeep this local instruction." + + +ROUTING_SUITE = json.loads((ROOT / "examples/evaluations/chat-routing.public.json").read_text()) + + +@pytest.mark.parametrize("case", ROUTING_SUITE["cases"], ids=lambda case: case["id"]) +def test_routing_oracles_reject_alias_misroutes_duplicate_sends_and_added_authority(case): + """Frozen decisions exercise the production parser and release scorer, not inference.""" + response = {"message": "Evidence: " + " ".join(case.get("required_evidence_refs", []))} + if case["expected"] == "handoff": + response["context_handoff"] = {**case["target"], "brief": { + "schema_version": "collaboration_brief_v0", "purpose": "Assess the scoped request", + "context": " ".join(case["required_handoff_refs"]), "constraints": ["No new authority."], + "inputs": [], "acceptance": ["Return the actual disposition."], + "return_requirement": "Return a scoped result.", + }} + wire = evaluation.CHAT_REVIEW_OPEN_TAG + json.dumps(response) + evaluation.CHAT_REVIEW_CLOSE_TAG + parsed = evaluation.parse_agent_response(wire) + assert evaluation.score(case, parsed)["passed"] + context = ROUTING_SUITE["contexts"][case["context_ref"]] + for recipient in context["agent_directory"]: + wrong = {**parsed, "context_handoff": { + "goal_id": recipient["goal_id"], "agent_id": recipient["agent_id"], + "brief": (response.get("context_handoff") or {}).get("brief"), + }} + if case.get("target") != {k: recipient[k] for k in ("goal_id", "agent_id")}: + assert not evaluation.score(case, wrong)["passed"] + for effect in ({"proposals": [{"kind": "todo"}]}, {"goal_draft": {"objective": "Duplicate work"}}, + {"protected_action": {"operation": "deploy", "target": "unrequested"}}): + assert not evaluation.score(case, {**parsed, **effect})["passed"] + if case["expected"] == "handoff": + assert not evaluation.score(case, {**parsed, "gate": {"kind": "confirmation"}})["passed"] + for omitted in case["required_handoff_refs"]: + brief = {**response["context_handoff"]["brief"], "context": " ".join( + ref for ref in case["required_handoff_refs"] if ref != omitted)} + assert not evaluation.score(case, {**parsed, "context_handoff": { + **case["target"], "brief": brief}})["passed"] + + +def test_routing_fixture_separates_topic_identity_delivery_and_permission(): + contexts = ROUTING_SUITE["contexts"] + topic = contexts["topic"] + assert topic["agent_directory"][0]["agent_id"] == "generalist-2" + assert topic["agent_directory"][1]["agent_id"] == "topic-investigator" + assert len({row["profile_role"] for row in topic["agent_directory"]}) == 1 + assert topic["handoff_receipts"][0]["agent_id"] == "generalist-2" + assert topic["recent_deliveries"][0]["agent_id"] == "topic-investigator" + for receipt in contexts["settled"]["handoff_receipts"]: + assert receipt["delivery"]["status"] == "delivered" + assert receipt["read"]["status"] == receipt["decision"]["status"] == "not_recorded" + assert not contexts["new-work"]["goals"][0]["current_todos"] + assert not contexts["ambiguous"]["recent_deliveries"] + assert len({row["scope_summary"] for row in contexts["ambiguous"]["agent_directory"]}) == 1 + denied = contexts["ungranted"] + assert denied["agent_directory"][1]["context_delivery"] == "not_granted" + assert denied["context_delegation"]["targets"] == [topic["context_delegation"]["targets"][0]] def test_followthrough_oracles_distinguish_context_not_question_wording(): diff --git a/tests/test_chat_manager_context.py b/tests/test_chat_manager_context.py index b85d1ce35b..03964ca5c9 100644 --- a/tests/test_chat_manager_context.py +++ b/tests/test_chat_manager_context.py @@ -1145,8 +1145,9 @@ def start(**kwargs): } +@pytest.mark.parametrize("previous_context_version", [None, 21]) def test_legacy_manager_migrates_without_project_and_refreshes_each_turn( - monkeypatch, tmp_path + monkeypatch, tmp_path, previous_context_version ): store = ChatSessionStore(tmp_path / "runtime" / "chat") runtime = ChatRuntimeController( @@ -1176,6 +1177,8 @@ def start(**kwargs): upstream_mode="chat", codex_home=str(runtime.codex_home), ) + if previous_context_version is not None: + store.update_session(legacy["session_id"], manager_context_version=previous_context_version) session, resumed = runtime.open_session( goal_id="different-project", agent_id="codex",