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
51 changes: 48 additions & 3 deletions loopx/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
from typing import Any, Iterable, Mapping

from .todos import add_goal_todo
from .control_plane.work_items.governed_transition_proposal import (
STEWARD_TEAM_PLAN_PREVIEW_KIND,
)


CHAT_AGENT_RESPONSE_SCHEMA_VERSION = "loopx_chat_agent_response_v0"
Expand Down Expand Up @@ -178,12 +181,29 @@ def _compact_line(value: Any, *, limit: int) -> str:
return text[:limit].strip()


def _normalize_proposals(value: Any, *, protected_paths: Iterable[Path | str]) -> list[dict[str, str]]:
proposals: list[dict[str, str]] = []
def _normalize_proposals(
value: Any,
*,
protected_paths: Iterable[Path | str],
team_plan_context: Mapping[str, Any] | None = None,
) -> list[dict[str, Any]]:
proposals: list[dict[str, Any]] = []
if not isinstance(value, list):
return proposals
for raw in value[:5]:
if not isinstance(raw, dict) or raw.get("kind") != "todo":
if not isinstance(raw, dict):
continue
if raw.get("kind") == STEWARD_TEAM_PLAN_PREVIEW_KIND:
# A team plan is admitted here or not at all: without the host facts
# that say which Agents and action kinds exist, a preview cannot be
# validated, so it is never surfaced half-checked.
preview = _validated_team_plan_preview(raw, team_plan_context)
if preview is not None:
proposals.append(
{"kind": STEWARD_TEAM_PLAN_PREVIEW_KIND, "preview": preview}
)
continue
if raw.get("kind") != "todo":
continue
text = _compact_line(redact_local_paths(str(raw.get("text") or ""), protected_paths=protected_paths), limit=400)
if not text:
Expand All @@ -206,6 +226,29 @@ def _normalize_proposals(value: Any, *, protected_paths: Iterable[Path | str]) -
return proposals


def _validated_team_plan_preview(
raw: Mapping[str, Any], context: Mapping[str, Any] | None
) -> dict[str, Any] | None:
"""Return the validated preview, or ``None`` when it may not be surfaced."""

if not isinstance(context, Mapping):
return None
from .control_plane.work_items.governed_transition_proposal import (
validate_steward_team_plan_preview,
)

try:
return validate_steward_team_plan_preview(
raw,
registered_agent_ids=list(context.get("registered_agent_ids") or []),
supported_action_kinds=list(context.get("supported_action_kinds") or []),
)
except ValueError:
# A malformed preview is dropped exactly like any other proposal this
# normalizer cannot accept; the answer text still reaches the owner.
return None


def _normalize_protected_action(
value: Any,
*,
Expand Down Expand Up @@ -272,6 +315,7 @@ def normalize_agent_response(
payload: Mapping[str, Any],
*,
protected_paths: Iterable[Path | str] = (),
team_plan_context: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""Normalize one structured provider response to the public Chat contract."""

Expand All @@ -289,6 +333,7 @@ def normalize_agent_response(
"proposals": _normalize_proposals(
payload.get("proposals"),
protected_paths=protected,
team_plan_context=team_plan_context,
),
"protected_action": _normalize_protected_action(
payload.get("protected_action"),
Expand Down
2 changes: 1 addition & 1 deletion tests/test_manager_channel_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -836,8 +836,8 @@ def test_every_production_steward_caller_passes_the_machine_defaults() -> None:
assert len(call_sites) >= 14, call_sites
assert {path for path, _line, _name, _ok in call_sites} >= {
"loopx/chat_manager.py",
"loopx/chat_manager_context.py",
"loopx/chat_runtime.py",
"loopx/chat_server.py",
"loopx/extensions/lark/goal_topic_runtime.py",
}
undocumented = [
Expand Down
48 changes: 48 additions & 0 deletions tests/test_steward_team_plan_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,51 @@ def test_the_preview_kind_has_no_materializer() -> None:
"""Nothing may apply the preview while only the preview slice exists."""

assert STEWARD_TEAM_PLAN_PREVIEW_KIND not in _SETTLEMENT_PHASE_BY_PROPOSAL_KIND


def test_the_chat_normalizer_admits_a_preview_only_with_host_facts() -> None:
"""The production caller: one malformed or unproven preview never surfaces."""

from loopx.chat import normalize_agent_response

envelope = {
"message": "Here is the plan",
"proposals": [_plan()],
"context_handoff": None,
"protected_action": None,
"gate": None,
}
context = {
"registered_agent_ids": ["agent-alpha"],
"supported_action_kinds": ["implement"],
}

surfaced = normalize_agent_response(envelope, team_plan_context=context)
proposals = surfaced["proposals"]
assert [item["kind"] for item in proposals] == [
"steward_team_plan_preview"
]
assert proposals[0]["preview"]["applies"] is False
assert proposals[0]["preview"]["lanes"][0]["staffing"] == "ready"

# Without the host facts the preview cannot be validated, so it is not
# surfaced at all rather than admitted half-checked.
assert normalize_agent_response(envelope)["proposals"] == []

# A malformed preview is dropped like any other proposal the normalizer
# cannot accept, and the owner's answer text still arrives.
malformed = {**envelope, "proposals": [_plan(kind="not_a_plan")]}
dropped = normalize_agent_response(malformed, team_plan_context=context)
assert dropped["proposals"] == []
assert dropped["message"] == "Here is the plan"

# The plain Todo proposals keep their existing behaviour.
todo = {
**envelope,
"proposals": [
{"kind": "todo", "text": "Do one thing", "priority": "P2", "rationale": "why"}
],
}
assert normalize_agent_response(todo)["proposals"] == [
{"kind": "todo", "text": "Do one thing", "priority": "P2", "rationale": "why"}
]
Loading