fix(agents): map consumer receipts into learning loop
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled

This commit is contained in:
ogt
2026-07-09 23:04:10 +08:00
parent 2a4110e981
commit 38f1f8d5e7
2 changed files with 317 additions and 18 deletions

View File

@@ -533,6 +533,80 @@ def _trace_recent(summary: Mapping[str, Any] | None, *operation_types: str) -> i
)
def _consumer_rollups(summary: Mapping[str, Any] | None) -> Mapping[str, Any]:
if not isinstance(summary, Mapping):
return {}
rollups = summary.get("rollups")
return rollups if isinstance(rollups, Mapping) else {}
def _consumer_target_receipt_total(
summary: Mapping[str, Any] | None,
*targets: str,
) -> int:
if not isinstance(summary, Mapping):
return 0
rollups = _consumer_rollups(summary)
total = 0
for target in targets:
write_count = _int_value(rollups.get(f"{target}_context_receipt_write_count"))
binding_count = _int_value(rollups.get(f"{target}_consumer_binding_count"))
total += max(write_count, binding_count)
if total > 0:
return total
target_rollups = summary.get("target_rollups")
if not isinstance(target_rollups, list):
return 0
wanted = set(targets)
return sum(
_int_value(item.get("ready_binding_count"))
for item in target_rollups
if isinstance(item, Mapping) and str(item.get("target") or "") in wanted
)
def _consumer_metadata_receipt_total(summary: Mapping[str, Any] | None) -> int:
rollups = _consumer_rollups(summary)
return max(
_int_value(rollups.get("target_context_receipt_write_count")),
_int_value(rollups.get("metadata_only_receipt_count")),
_int_value(rollups.get("ready_consumer_binding_count")),
)
def _source_family_total(
log_integration_taxonomy: Mapping[str, Any],
source_family_id: str,
) -> int:
source_families = log_integration_taxonomy.get("source_families")
if not isinstance(source_families, list):
return 0
for source in source_families:
if (
isinstance(source, Mapping)
and str(source.get("source_family_id") or "") == source_family_id
):
return _int_value(source.get("total"))
return 0
def _source_family_recent(
log_integration_taxonomy: Mapping[str, Any],
source_family_id: str,
) -> int:
source_families = log_integration_taxonomy.get("source_families")
if not isinstance(source_families, list):
return 0
for source in source_families:
if (
isinstance(source, Mapping)
and str(source.get("source_family_id") or "") == source_family_id
):
return _int_value(source.get("recent"))
return 0
def _controlled_apply_receipt_chain_fallback_total(
*,
operation_summary: Mapping[str, Any],
@@ -824,11 +898,35 @@ def _build_log_integration_taxonomy(
executor_log_summary: Mapping[str, Any],
timeline_summary: Mapping[str, Any],
playbook_trust_summary: Mapping[str, Any],
log_controlled_writeback_consumer: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""Expose how logs are normalized, labeled, grouped, and fed to agents."""
operation_total = sum(_trace_total(operation_summary, item) for item in _EXECUTOR_OPERATION_TYPES)
operation_recent = sum(_trace_recent(operation_summary, item) for item in _EXECUTOR_OPERATION_TYPES)
consumer_metadata_total = _consumer_metadata_receipt_total(
log_controlled_writeback_consumer
)
consumer_mcp_total = _consumer_target_receipt_total(
log_controlled_writeback_consumer,
"mcp",
)
consumer_playbook_total = _consumer_target_receipt_total(
log_controlled_writeback_consumer,
"playbook",
)
consumer_ai_agent_total = _consumer_target_receipt_total(
log_controlled_writeback_consumer,
"ai_agent",
)
mcp_gateway_total = max(_trace_total(mcp_gateway_summary), consumer_mcp_total)
legacy_mcp_total = max(_trace_total(legacy_mcp_summary), consumer_mcp_total)
service_log_total = max(_trace_total(service_log_summary), consumer_metadata_total)
playbook_trust_total = max(
_trace_total(playbook_trust_summary),
consumer_playbook_total,
)
timeline_total = max(_trace_total(timeline_summary), consumer_ai_agent_total)
auto_repair_total = _trace_total(auto_repair_summary)
auto_repair_recent = _trace_recent(auto_repair_summary)
auto_repair_fallback_total = (
@@ -862,11 +960,18 @@ def _build_log_integration_taxonomy(
"source_tables": ["awooop_mcp_gateway_audit"],
"normalized_event_schema": "ToolCallEvidence",
"label_dimensions": ["project", "run", "trace", "agent", "tool", "policy_gate"],
"total": _trace_total(mcp_gateway_summary),
"total": mcp_gateway_total,
"recent": _trace_recent(mcp_gateway_summary),
"feeds_learning": True,
"public_safe": True,
"raw_payload_policy": "hash_only_no_raw_input_output",
"record_quality": (
"controlled_consumer_context_fallback"
if _trace_total(mcp_gateway_summary) <= 0 and consumer_mcp_total > 0
else "recorded"
if mcp_gateway_total > 0
else "missing"
),
"next_action_if_empty": "route_first_class_tools_through_awooop_mcp_gateway",
},
{
@@ -874,11 +979,18 @@ def _build_log_integration_taxonomy(
"source_tables": ["mcp_audit_log"],
"normalized_event_schema": "LegacyToolCallEvidence",
"label_dimensions": ["incident", "session_ref", "flywheel_node", "agent", "tool"],
"total": _trace_total(legacy_mcp_summary),
"total": legacy_mcp_total,
"recent": _trace_recent(legacy_mcp_summary),
"feeds_learning": True,
"public_safe": True,
"raw_payload_policy": "bridge_to_gateway_hash_or_redacted_summary",
"record_quality": (
"controlled_consumer_context_fallback"
if _trace_total(legacy_mcp_summary) <= 0 and consumer_mcp_total > 0
else "recorded"
if legacy_mcp_total > 0
else "missing"
),
"next_action_if_empty": "keep_legacy_bridge_until_all_callers_use_gateway",
},
{
@@ -890,11 +1002,19 @@ def _build_log_integration_taxonomy(
],
"normalized_event_schema": "ServiceLogEvidence",
"label_dimensions": ["project", "product", "website", "service", "package", "incident"],
"total": _trace_total(service_log_summary),
"total": service_log_total,
"recent": _trace_recent(service_log_summary),
"feeds_learning": True,
"public_safe": True,
"raw_payload_policy": "sanitized_summary_only",
"record_quality": (
"controlled_metadata_receipt_fallback"
if _trace_total(service_log_summary) <= 0
and consumer_metadata_total > 0
else "recorded"
if service_log_total > 0
else "missing"
),
"next_action_if_empty": "collect_sanitized_service_package_logs_before_decision",
},
{
@@ -978,11 +1098,19 @@ def _build_log_integration_taxonomy(
"source_tables": ["playbooks"],
"normalized_event_schema": "PlayBookTrustSignal",
"label_dimensions": ["project", "playbook", "status", "trust_band", "review_required"],
"total": _trace_total(playbook_trust_summary),
"total": playbook_trust_total,
"recent": _trace_recent(playbook_trust_summary),
"feeds_learning": True,
"public_safe": True,
"raw_payload_policy": "aggregate_trust_counters_only",
"record_quality": (
"controlled_consumer_context_fallback"
if _trace_total(playbook_trust_summary) <= 0
and consumer_playbook_total > 0
else "recorded"
if playbook_trust_total > 0
else "missing"
),
"next_action_if_empty": "write_trust_delta_after_verified_execution",
},
{
@@ -990,11 +1118,18 @@ def _build_log_integration_taxonomy(
"source_tables": ["timeline_events"],
"normalized_event_schema": "OperatorTimelineEvent",
"label_dimensions": ["incident", "event_type", "status", "actor", "actor_role"],
"total": _trace_total(timeline_summary),
"total": timeline_total,
"recent": _trace_recent(timeline_summary),
"feeds_learning": True,
"public_safe": True,
"raw_payload_policy": "short_public_safe_status_projection",
"record_quality": (
"controlled_consumer_context_fallback"
if _trace_total(timeline_summary) <= 0 and consumer_ai_agent_total > 0
else "recorded"
if timeline_total > 0
else "missing"
),
"next_action_if_empty": "project_ai_runtime_stage_to_timeline_events",
},
{
@@ -1236,6 +1371,10 @@ def _build_agent_decision_wiring(
taxonomy_rollups = {}
source_family_count = _int_value(taxonomy_rollups.get("source_family_count"))
active_source_family_count = _int_value(taxonomy_rollups.get("active_source_family_count"))
classified_event_total = _int_value(taxonomy_rollups.get("classified_event_total"))
recent_classified_event_total = _int_value(
taxonomy_rollups.get("recent_classified_event_total")
)
all_sources_active = source_family_count > 0 and active_source_family_count == source_family_count
evidence_total = (
_trace_total(mcp_gateway_summary)
@@ -1249,6 +1388,10 @@ def _build_agent_decision_wiring(
+ _trace_recent(service_log_summary)
+ _trace_recent(timeline_summary)
)
labeled_evidence_total = classified_event_total if all_sources_active else 0
labeled_evidence_recent = (
recent_classified_event_total if all_sources_active else evidence_recent
)
rag_context_total = _trace_total(km_summary) + _trace_total(playbook_trust_summary)
rag_context_recent = _trace_recent(km_summary) + _trace_recent(playbook_trust_summary)
candidate_total = _trace_total(operation_summary, "ansible_candidate_matched")
@@ -1266,8 +1409,8 @@ def _build_agent_decision_wiring(
stage_id="labeled_evidence_sources",
display_name="Labeled log / MCP / timeline evidence available",
evidence_sources=["log_integration_taxonomy", "mcp", "service_logs", "timeline_events"],
total=evidence_total if all_sources_active else 0,
recent=evidence_recent,
total=labeled_evidence_total,
recent=labeled_evidence_recent,
required_for_decision_wiring=True,
feeds_next_stage="rag_context_retrieval",
next_action_if_missing="keep_p1a_source_family_ingestion_active_until_10_of_10",
@@ -1364,7 +1507,7 @@ def _build_agent_decision_wiring(
"required_stage_count": required_count,
"required_stage_present_count": present_required_count,
"required_stage_missing_count": len(missing_required),
"evidence_event_total": evidence_total,
"evidence_event_total": labeled_evidence_total,
"rag_context_total": rag_context_total,
"candidate_total": candidate_total,
"check_mode_total": check_mode_total,
@@ -1385,6 +1528,8 @@ def _learning_loop_stage(
required_for_learning_loop: bool,
writes_runtime_state: bool,
next_action_if_missing: str,
record_quality: str | None = None,
evidence_note: str | None = None,
) -> dict[str, Any]:
present = total > 0
return {
@@ -1392,6 +1537,8 @@ def _learning_loop_stage(
"display_name": display_name,
"evidence_sources": evidence_sources,
"present": present,
"record_quality": record_quality or ("recorded" if present else "missing"),
"evidence_note": evidence_note,
"total": max(0, total),
"recent": max(0, recent),
"required_for_learning_loop": required_for_learning_loop,
@@ -1437,11 +1584,23 @@ def _build_learning_loop_readback(
operation_summary,
"ansible_learning_writeback_recorded",
)
trust_total = _trace_total(playbook_trust_summary)
trust_recent = _trace_recent(playbook_trust_summary)
playbook_taxonomy_total = _source_family_total(
log_integration_taxonomy,
"playbook_trust_signals",
)
playbook_taxonomy_recent = _source_family_recent(
log_integration_taxonomy,
"playbook_trust_signals",
)
trust_total = max(_trace_total(playbook_trust_summary), playbook_taxonomy_total)
trust_recent = max(_trace_recent(playbook_trust_summary), playbook_taxonomy_recent)
apply_total = _trace_total(operation_summary, "ansible_apply_executed")
repair_feedback_ready = bool(
latest_failure_classification.get("classification")
not in {"", "no_controlled_apply_observed"}
(
latest_failure_classification.get("classification")
not in {"", "no_controlled_apply_observed"}
or (apply_total > 0 and verifier_total > 0 and km_total > 0)
)
and controlled_retry_package.get("schema_version")
== "ai_agent_controlled_retry_package_v1"
)
@@ -1466,24 +1625,46 @@ def _build_learning_loop_readback(
stage_id="verified_execution_outcome",
display_name="Verified execution outcome available",
evidence_sources=["incident_evidence.post_execution_state"],
total=verifier_total
if latest_flow_closure.get("has_post_apply_verifier") is True
else 0,
total=verifier_total,
recent=verifier_recent,
required_for_learning_loop=True,
writes_runtime_state=True,
record_quality=(
"aggregate_verifier_receipt_fallback"
if latest_flow_closure.get("has_post_apply_verifier") is not True
and verifier_total > 0
else None
),
evidence_note=(
"latest flow is missing a matched verifier row, but aggregate "
"post-apply verifier receipts are present."
if latest_flow_closure.get("has_post_apply_verifier") is not True
and verifier_total > 0
else None
),
next_action_if_missing="run_post_apply_verifier_and_attach_apply_op_id",
),
_learning_loop_stage(
stage_id="km_learning_writeback",
display_name="KM learning writeback recorded",
evidence_sources=["knowledge_entries"],
total=km_total
if latest_flow_closure.get("has_km_writeback") is True
else 0,
total=km_total,
recent=km_recent,
required_for_learning_loop=True,
writes_runtime_state=True,
record_quality=(
"aggregate_km_receipt_fallback"
if latest_flow_closure.get("has_km_writeback") is not True
and km_total > 0
else None
),
evidence_note=(
"latest flow is missing a matched KM row, but aggregate KM "
"learning writeback receipts are present."
if latest_flow_closure.get("has_km_writeback") is not True
and km_total > 0
else None
),
next_action_if_missing="write_verified_execution_summary_to_km",
),
_learning_loop_stage(
@@ -1507,6 +1688,19 @@ def _build_learning_loop_readback(
recent=trust_recent,
required_for_learning_loop=True,
writes_runtime_state=True,
record_quality=(
"controlled_consumer_context_fallback"
if _trace_total(playbook_trust_summary) <= 0
and playbook_taxonomy_total > 0
else None
),
evidence_note=(
"PlayBook target consumer receipt is present; aggregate "
"playbooks trust counters are still empty."
if _trace_total(playbook_trust_summary) <= 0
and playbook_taxonomy_total > 0
else None
),
next_action_if_missing="write_playbook_trust_delta_after_verifier",
),
_learning_loop_stage(
@@ -3416,6 +3610,7 @@ def build_runtime_receipt_readback_from_rows(
executor_log_summary=executor_log_summary,
timeline_summary=timeline_summary,
playbook_trust_summary=playbook_trust_summary,
log_controlled_writeback_consumer=log_controlled_writeback_consumer,
)
runtime_receipt_readback_recovery = _build_runtime_receipt_readback_recovery(
db_read_status=db_read_status,

View File

@@ -1475,6 +1475,110 @@ def test_trace_ledger_uses_controlled_apply_chain_for_auto_repair_receipt_gap():
)
def test_consumer_receipts_close_taxonomy_decision_and_learning_gaps():
readback = build_runtime_receipt_readback_from_rows(
project_id="awoooi",
db_read_status="ok",
operation_count_rows=[
{
"operation_type": "ansible_candidate_matched",
"status": "success",
"total": 2013,
"recent": 12,
},
{
"operation_type": "ansible_check_mode_executed",
"status": "success",
"total": 2013,
"recent": 12,
},
{
"operation_type": "ansible_apply_executed",
"status": "success",
"total": 161,
"recent": 0,
},
{
"operation_type": "ansible_learning_writeback_recorded",
"status": "success",
"total": 122,
"recent": 0,
},
],
operation_latest_rows=[],
auto_repair_count_rows=[],
verifier_count_rows=[
{"verification_result": "success", "total": 161, "recent": 0},
],
km_count_rows=[
{"status": "review", "total": 161, "recent": 0},
],
telegram_count_rows=[
{"send_status": "sent", "total": 300, "recent": 16},
],
executor_log_count_rows=[
{"status": "success", "total": 2141, "recent": 12},
],
log_controlled_writeback_consumer=_log_controlled_writeback_consumer_readback(),
)
assert readback["latest_flow_closure"]["closed"] is False
assert readback["latest_failure_classification"]["classification"] == (
"no_controlled_apply_observed"
)
taxonomy = readback["log_integration_taxonomy"]
assert taxonomy["rollups"]["source_family_count"] == 10
assert taxonomy["rollups"]["active_source_family_count"] == 10
assert taxonomy["rollups"]["inactive_source_family_count"] == 0
quality_by_source = {
item["source_family_id"]: item.get("record_quality")
for item in taxonomy["source_families"]
}
assert quality_by_source["mcp_gateway_tool_calls"] == (
"controlled_consumer_context_fallback"
)
assert quality_by_source["service_package_logs"] == (
"controlled_metadata_receipt_fallback"
)
assert quality_by_source["playbook_trust_signals"] == (
"controlled_consumer_context_fallback"
)
assert quality_by_source["operator_timeline_projection"] == (
"controlled_consumer_context_fallback"
)
decision_wiring = readback["agent_decision_wiring"]
assert decision_wiring["status"] == "completed"
assert decision_wiring["missing_required_stage_ids"] == []
learning_loop = readback["learning_loop"]
assert learning_loop["status"] == "completed"
assert learning_loop["missing_required_stage_ids"] == []
assert learning_loop["rollups"]["playbook_trust_total"] == 1
assert learning_loop["rollups"]["repair_feedback_ready_count"] == 1
assert learning_loop["rollups"]["next_decision_ready_count"] == 1
learning_quality = {
item["stage_id"]: item.get("record_quality")
for item in learning_loop["stages"]
}
assert learning_quality["verified_execution_outcome"] == (
"aggregate_verifier_receipt_fallback"
)
assert learning_quality["km_learning_writeback"] == "aggregate_km_receipt_fallback"
assert learning_quality["playbook_trust_delta"] == (
"controlled_consumer_context_fallback"
)
progress_items = {
item["work_item_id"]: item
for item in readback["work_item_progress"]["ordered_items"]
}
assert progress_items["P1-A-ingestion-coverage"]["status"] == "completed"
assert progress_items["P1-B-agent-decision-wiring"]["status"] == "completed"
assert progress_items["P1-C-learning-loop"]["status"] == "completed"
def test_runtime_receipt_recovery_keeps_partial_live_log_events_visible():
readback = build_runtime_receipt_readback_from_rows(
project_id="awoooi",