mirror of
https://github.com/HKUDS/OpenSpace.git
synced 2026-09-06 08:16:07 +00:00
688 lines
23 KiB
Python
688 lines
23 KiB
Python
"""Deterministic quality signal detector."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from dataclasses import replace
|
|
from typing import Any, Iterable, Mapping, Sequence
|
|
|
|
from openspace.skill_engine.evidence import EvidenceScope, EvidenceStore, ResourceRef
|
|
|
|
from .linkers import (
|
|
EvidenceIndexes,
|
|
SkillContextCandidate,
|
|
build_evidence_indexes,
|
|
has_representative_tool_evidence,
|
|
latest_ref,
|
|
metadata_values,
|
|
ref_sort_key,
|
|
ref_tool_key,
|
|
related_tool_incidents,
|
|
related_tool_results,
|
|
resolve_skill_contexts,
|
|
tool_use_key,
|
|
)
|
|
from .types import (
|
|
ACTION_MANUAL_REVIEW,
|
|
ACTION_OBSERVE_ONLY,
|
|
ACTION_TRIGGER_REVIEW,
|
|
SIGNAL_AGGREGATE_WITHOUT_INCIDENT,
|
|
SIGNAL_TOOL_CALL_FAILED,
|
|
SIGNAL_TOOL_FAILURE_AFFECTS_SKILL,
|
|
STATUS_ACTIONABLE_PARTIAL,
|
|
STATUS_AGGREGATE_ONLY,
|
|
STATUS_AMBIGUOUS_SUBJECT,
|
|
STATUS_CONFLICTING,
|
|
STATUS_COMPLETE,
|
|
STATUS_EXTERNAL_ONLY,
|
|
STATUS_MISSING_SKILL_CONTEXT,
|
|
QualitySignal,
|
|
choose_dominant_signal,
|
|
stable_unique,
|
|
)
|
|
|
|
|
|
CHECKPOINT_TASK_SESSION_PERSISTED = "task_session_persisted"
|
|
|
|
_BASE_REF_TYPES = [
|
|
"tool_event",
|
|
"tool_result",
|
|
"tool_incident",
|
|
"tool_quality_record",
|
|
"skill_event",
|
|
"skill_record",
|
|
"skill_file",
|
|
"execution_analysis",
|
|
"transcript_message",
|
|
]
|
|
_SKILL_CONTEXT_REF_TYPES = ["skill_record", "skill_file"]
|
|
_TOOL_CONTEXT_REF_TYPES = ["tool_quality_record", "tool_incident"]
|
|
|
|
|
|
class QualitySignalDetector:
|
|
def __init__(self, evidence_store: EvidenceStore) -> None:
|
|
self.evidence_store = evidence_store
|
|
self.warnings: list[str] = []
|
|
|
|
def scan_checkpoint(
|
|
self,
|
|
*,
|
|
checkpoint_name: str,
|
|
scope: EvidenceScope,
|
|
manifest_watermark: int,
|
|
) -> list[QualitySignal]:
|
|
self.warnings = []
|
|
if checkpoint_name != CHECKPOINT_TASK_SESSION_PERSISTED:
|
|
return []
|
|
|
|
refs = self._load_refs(scope, manifest_watermark)
|
|
indexes = build_evidence_indexes(refs)
|
|
signals: list[QualitySignal] = []
|
|
|
|
for tool_event in indexes.failed_tool_events:
|
|
tool_call_signal = self._tool_call_failed_signal(
|
|
tool_event,
|
|
indexes,
|
|
manifest_watermark=manifest_watermark,
|
|
)
|
|
if tool_call_signal is not None:
|
|
signals.append(tool_call_signal)
|
|
skill_signal = self._tool_failure_affects_skill_signal(
|
|
tool_event,
|
|
indexes,
|
|
manifest_watermark=manifest_watermark,
|
|
)
|
|
if skill_signal is not None:
|
|
signals.append(skill_signal)
|
|
|
|
signals.extend(
|
|
self._aggregate_without_incident_signals(
|
|
indexes,
|
|
manifest_watermark=manifest_watermark,
|
|
)
|
|
)
|
|
return signals
|
|
|
|
def _load_refs(
|
|
self,
|
|
scope: EvidenceScope,
|
|
manifest_watermark: int,
|
|
) -> list[ResourceRef]:
|
|
refs = self.evidence_store.query_refs(
|
|
scope,
|
|
ref_types=list(_BASE_REF_TYPES),
|
|
watermark=manifest_watermark,
|
|
)
|
|
indexes = build_evidence_indexes(refs)
|
|
skill_ids = sorted(
|
|
{
|
|
*indexes.skill_events_by_skill_id,
|
|
*[item for item in scope.skill_ids if item],
|
|
}
|
|
)
|
|
tool_keys = sorted(
|
|
{
|
|
*[
|
|
ref_tool_key(ref)
|
|
for bucket in indexes.tool_events_by_use_id.values()
|
|
for ref in bucket
|
|
if ref_tool_key(ref)
|
|
],
|
|
*[item for item in scope.tool_keys if item],
|
|
}
|
|
)
|
|
|
|
extra_refs: list[ResourceRef] = []
|
|
if skill_ids:
|
|
extra_refs.extend(
|
|
self.evidence_store.query_refs(
|
|
EvidenceScope(skill_ids=tuple(skill_ids)),
|
|
ref_types=list(_SKILL_CONTEXT_REF_TYPES),
|
|
watermark=manifest_watermark,
|
|
)
|
|
)
|
|
if tool_keys:
|
|
extra_refs.extend(
|
|
ref
|
|
for ref in self.evidence_store.query_refs(
|
|
EvidenceScope(
|
|
session_id=scope.session_id,
|
|
task_id=scope.task_id,
|
|
source_task_ids=scope.source_task_ids,
|
|
agent_ids=scope.agent_ids,
|
|
tool_keys=tuple(tool_keys),
|
|
),
|
|
ref_types=list(_TOOL_CONTEXT_REF_TYPES),
|
|
watermark=manifest_watermark,
|
|
)
|
|
if _same_checkpoint_context(scope, ref)
|
|
)
|
|
|
|
by_ref_id = {ref.ref_id: ref for ref in [*refs, *extra_refs] if ref.ref_id}
|
|
return sorted(by_ref_id.values(), key=ref_sort_key)
|
|
|
|
def _tool_call_failed_signal(
|
|
self,
|
|
tool_event: ResourceRef,
|
|
indexes: EvidenceIndexes,
|
|
*,
|
|
manifest_watermark: int,
|
|
) -> QualitySignal | None:
|
|
tool_key = ref_tool_key(tool_event)
|
|
if not tool_key:
|
|
self.warnings.append(f"malformed_tool_event_missing_tool_key:{tool_event.ref_id}")
|
|
return None
|
|
results = related_tool_results(tool_event, indexes)
|
|
incidents = related_tool_incidents(tool_event, indexes)
|
|
raw_backrefs = stable_unique(
|
|
[tool_event.ref_id, *[ref.ref_id for ref in results], *[ref.ref_id for ref in incidents]]
|
|
)
|
|
return self._signal(
|
|
signal_type=SIGNAL_TOOL_CALL_FAILED,
|
|
subject_type="tool",
|
|
subject_id=tool_key,
|
|
actionability=ACTION_OBSERVE_ONLY,
|
|
evidence_status=STATUS_COMPLETE,
|
|
failure_signature=stable_failure_signature(
|
|
SIGNAL_TOOL_CALL_FAILED,
|
|
tool_event=tool_event,
|
|
result_refs=results,
|
|
),
|
|
raw_backrefs=raw_backrefs,
|
|
source_ref=tool_event,
|
|
tool_key=tool_key,
|
|
skill_id=None,
|
|
skill_version=None,
|
|
manifest_watermark=manifest_watermark,
|
|
metadata=_summary_metadata(
|
|
tool_event,
|
|
result_refs=results,
|
|
incident_refs=incidents,
|
|
linkage="tool_event_failed",
|
|
)
|
|
| {"policy_reason": "observe_tool_event_failed"},
|
|
)
|
|
|
|
def _tool_failure_affects_skill_signal(
|
|
self,
|
|
tool_event: ResourceRef,
|
|
indexes: EvidenceIndexes,
|
|
*,
|
|
manifest_watermark: int,
|
|
) -> QualitySignal | None:
|
|
tool_key = ref_tool_key(tool_event)
|
|
if not tool_key:
|
|
return None
|
|
|
|
all_results = related_tool_results(tool_event, indexes)
|
|
results = _complete_results(all_results)
|
|
incidents = related_tool_incidents(tool_event, indexes)
|
|
contexts = resolve_skill_contexts(tool_event, indexes)
|
|
supporting_refs = [*results, *incidents]
|
|
|
|
if not contexts:
|
|
return self._signal(
|
|
signal_type=SIGNAL_TOOL_FAILURE_AFFECTS_SKILL,
|
|
subject_type="tool",
|
|
subject_id=tool_key,
|
|
actionability=ACTION_OBSERVE_ONLY,
|
|
evidence_status=STATUS_MISSING_SKILL_CONTEXT,
|
|
failure_signature=stable_failure_signature(
|
|
SIGNAL_TOOL_FAILURE_AFFECTS_SKILL,
|
|
tool_event=tool_event,
|
|
result_refs=results,
|
|
),
|
|
raw_backrefs=stable_unique(
|
|
[tool_event.ref_id, *[ref.ref_id for ref in supporting_refs]]
|
|
),
|
|
source_ref=tool_event,
|
|
tool_key=tool_key,
|
|
skill_id=None,
|
|
skill_version=None,
|
|
manifest_watermark=manifest_watermark,
|
|
missing_refs=("skill_event", "skill_file"),
|
|
metadata=_summary_metadata(
|
|
tool_event,
|
|
result_refs=results,
|
|
incident_refs=incidents,
|
|
linkage="missing_skill_context",
|
|
)
|
|
| {"policy_reason": "missing_skill_context"},
|
|
)
|
|
|
|
if len(contexts) > 1:
|
|
raw_backrefs = [
|
|
tool_event.ref_id,
|
|
*[ref.ref_id for ref in supporting_refs],
|
|
*[context.skill_event.ref_id for context in contexts],
|
|
]
|
|
return self._signal(
|
|
signal_type=SIGNAL_TOOL_FAILURE_AFFECTS_SKILL,
|
|
subject_type="tool",
|
|
subject_id=tool_key,
|
|
actionability=ACTION_MANUAL_REVIEW,
|
|
evidence_status=STATUS_AMBIGUOUS_SUBJECT,
|
|
failure_signature=stable_failure_signature(
|
|
SIGNAL_TOOL_FAILURE_AFFECTS_SKILL,
|
|
tool_event=tool_event,
|
|
result_refs=results,
|
|
),
|
|
raw_backrefs=stable_unique(raw_backrefs),
|
|
source_ref=tool_event,
|
|
tool_key=tool_key,
|
|
skill_id=None,
|
|
skill_version=None,
|
|
manifest_watermark=manifest_watermark,
|
|
missing_refs=("unique_skill_context",),
|
|
metadata={
|
|
**_summary_metadata(
|
|
tool_event,
|
|
result_refs=results,
|
|
incident_refs=incidents,
|
|
linkage="ambiguous_skill_context",
|
|
),
|
|
"candidate_skill_ids": [context.skill_id for context in contexts],
|
|
"policy_reason": "ambiguous_skill_context",
|
|
},
|
|
)
|
|
|
|
context = contexts[0]
|
|
skill_version = _skill_version(context)
|
|
conflict_reasons = _conflicting_evidence_reasons(
|
|
result_refs=all_results,
|
|
incident_refs=incidents,
|
|
)
|
|
if context.skill_file is None:
|
|
status = STATUS_MISSING_SKILL_CONTEXT
|
|
actionability = ACTION_OBSERVE_ONLY
|
|
missing_refs = ("skill_file",)
|
|
policy_reason = "missing_skill_file"
|
|
elif conflict_reasons:
|
|
status = STATUS_CONFLICTING
|
|
actionability = ACTION_MANUAL_REVIEW
|
|
missing_refs = ("consistent_tool_evidence",)
|
|
policy_reason = "conflicting_tool_evidence"
|
|
elif _is_permission_or_user_denied(tool_event):
|
|
status = STATUS_EXTERNAL_ONLY
|
|
actionability = ACTION_OBSERVE_ONLY
|
|
missing_refs = ()
|
|
policy_reason = "permission_denied_without_skill_evidence"
|
|
elif supporting_refs:
|
|
status = STATUS_COMPLETE
|
|
actionability = ACTION_TRIGGER_REVIEW
|
|
missing_refs = ()
|
|
policy_reason = "complete_exact_failure_evidence"
|
|
else:
|
|
status = STATUS_ACTIONABLE_PARTIAL
|
|
actionability = ACTION_TRIGGER_REVIEW
|
|
missing_refs = ("tool_result", "tool_incident")
|
|
policy_reason = "actionable_partial_failed_tool_event"
|
|
|
|
raw_tool_refs = [*all_results, *incidents] if conflict_reasons else supporting_refs
|
|
raw_backrefs = [
|
|
tool_event.ref_id,
|
|
*[ref.ref_id for ref in raw_tool_refs],
|
|
context.skill_event.ref_id,
|
|
]
|
|
if context.skill_record is not None:
|
|
raw_backrefs.append(context.skill_record.ref_id)
|
|
if context.skill_file is not None:
|
|
raw_backrefs.append(context.skill_file.ref_id)
|
|
|
|
return self._signal(
|
|
signal_type=SIGNAL_TOOL_FAILURE_AFFECTS_SKILL,
|
|
subject_type="tool_skill_relation",
|
|
subject_id=f"{context.skill_id}:{tool_key}",
|
|
actionability=actionability,
|
|
evidence_status=status,
|
|
failure_signature=stable_failure_signature(
|
|
SIGNAL_TOOL_FAILURE_AFFECTS_SKILL,
|
|
tool_event=tool_event,
|
|
skill_id=context.skill_id,
|
|
result_refs=results,
|
|
),
|
|
raw_backrefs=stable_unique(raw_backrefs),
|
|
source_ref=tool_event,
|
|
tool_key=tool_key,
|
|
skill_id=context.skill_id,
|
|
skill_version=skill_version,
|
|
manifest_watermark=manifest_watermark,
|
|
missing_refs=missing_refs,
|
|
metadata={
|
|
**_summary_metadata(
|
|
tool_event,
|
|
result_refs=results,
|
|
incident_refs=incidents,
|
|
linkage=context.link_type,
|
|
),
|
|
"skill_context_ref": context.skill_event.ref_id,
|
|
"policy_reason": policy_reason,
|
|
**({"conflict_reasons": conflict_reasons} if conflict_reasons else {}),
|
|
},
|
|
)
|
|
|
|
def _aggregate_without_incident_signals(
|
|
self,
|
|
indexes: EvidenceIndexes,
|
|
*,
|
|
manifest_watermark: int,
|
|
) -> list[QualitySignal]:
|
|
signals: list[QualitySignal] = []
|
|
for tool_key, records in sorted(indexes.quality_records_by_tool_key.items()):
|
|
if has_representative_tool_evidence(tool_key, indexes):
|
|
continue
|
|
record = latest_ref(records)
|
|
if record is None:
|
|
continue
|
|
signals.append(
|
|
self._signal(
|
|
signal_type=SIGNAL_AGGREGATE_WITHOUT_INCIDENT,
|
|
subject_type="tool",
|
|
subject_id=tool_key,
|
|
actionability=ACTION_OBSERVE_ONLY,
|
|
evidence_status=STATUS_AGGREGATE_ONLY,
|
|
failure_signature=stable_failure_signature(
|
|
SIGNAL_AGGREGATE_WITHOUT_INCIDENT,
|
|
tool_key=tool_key,
|
|
),
|
|
raw_backrefs=(record.ref_id,),
|
|
source_ref=record,
|
|
tool_key=tool_key,
|
|
skill_id=None,
|
|
skill_version=None,
|
|
manifest_watermark=manifest_watermark,
|
|
metadata={
|
|
"summary": "aggregate quality record has no representative incident in scope",
|
|
"linkage": "aggregate_without_incident",
|
|
"policy_reason": "aggregate_without_incident",
|
|
"recent_success_rate": _safe_number(
|
|
record.metadata.get("recent_success_rate")
|
|
),
|
|
},
|
|
)
|
|
)
|
|
return signals
|
|
|
|
def _signal(
|
|
self,
|
|
*,
|
|
signal_type: str,
|
|
subject_type: str,
|
|
subject_id: str,
|
|
actionability: str,
|
|
evidence_status: str,
|
|
failure_signature: str,
|
|
raw_backrefs: Sequence[str],
|
|
source_ref: ResourceRef,
|
|
tool_key: str | None,
|
|
skill_id: str | None,
|
|
skill_version: str | None,
|
|
manifest_watermark: int,
|
|
missing_refs: Sequence[str] = (),
|
|
metadata: Mapping[str, Any] | None = None,
|
|
) -> QualitySignal:
|
|
merge_key = (
|
|
f"{subject_type}:{subject_id}:{signal_type}:"
|
|
f"{failure_signature}:{skill_version or 'no_skill_version'}"
|
|
)
|
|
return QualitySignal(
|
|
signal_id=_signal_id(merge_key, source_ref.ref_id),
|
|
signal_type=signal_type,
|
|
subject_type=subject_type,
|
|
subject_id=subject_id,
|
|
actionability=actionability,
|
|
evidence_status=evidence_status,
|
|
merge_key=merge_key,
|
|
failure_signature=failure_signature,
|
|
raw_backrefs=tuple(raw_backrefs),
|
|
session_id=source_ref.session_id,
|
|
task_id=source_ref.task_id,
|
|
parent_task_id=source_ref.parent_task_id,
|
|
agent_id=source_ref.agent_id,
|
|
tool_key=tool_key,
|
|
skill_id=skill_id,
|
|
skill_version=skill_version,
|
|
source_watermark=manifest_watermark,
|
|
missing_refs=tuple(stable_unique(missing_refs)),
|
|
metadata=dict(metadata or {}),
|
|
)
|
|
|
|
|
|
def stable_failure_signature(
|
|
signal_type: str,
|
|
*,
|
|
tool_event: ResourceRef | None = None,
|
|
tool_key: str | None = None,
|
|
skill_id: str | None = None,
|
|
result_refs: Sequence[ResourceRef] = (),
|
|
) -> str:
|
|
event_metadata = tool_event.metadata if tool_event is not None else {}
|
|
resolved_tool_key = tool_key or (ref_tool_key(tool_event) if tool_event else "")
|
|
payload = [
|
|
signal_type,
|
|
resolved_tool_key or "",
|
|
skill_id or "",
|
|
_normalized_error_type(event_metadata),
|
|
_metadata_text(event_metadata, "permission_status"),
|
|
_metadata_text(event_metadata, "exception_class", "exception_type"),
|
|
_normalized_result_shape(result_refs),
|
|
]
|
|
return _digest(payload)
|
|
|
|
|
|
def _summary_metadata(
|
|
tool_event: ResourceRef,
|
|
*,
|
|
result_refs: Sequence[ResourceRef],
|
|
incident_refs: Sequence[ResourceRef],
|
|
linkage: str,
|
|
) -> dict[str, Any]:
|
|
metadata = tool_event.metadata
|
|
tool_key = ref_tool_key(tool_event)
|
|
error_type = _normalized_error_type(metadata)
|
|
result_shape = _normalized_result_shape(result_refs)
|
|
summary_parts = [
|
|
f"tool={tool_key or 'unknown'}",
|
|
f"status={str(metadata.get('status') or 'unknown').lower()}",
|
|
]
|
|
if error_type:
|
|
summary_parts.append(f"error_type={error_type}")
|
|
return _without_none(
|
|
{
|
|
"summary": " ".join(summary_parts),
|
|
"linkage": linkage,
|
|
"tool_use_id": tool_use_key(tool_event).tool_use_id or None,
|
|
"error_type": error_type or None,
|
|
"exception_class": _metadata_text(
|
|
metadata,
|
|
"exception_class",
|
|
"exception_type",
|
|
)
|
|
or None,
|
|
"permission_status": _metadata_text(metadata, "permission_status") or None,
|
|
"result_shape": result_shape or None,
|
|
"result_ref_count": len(result_refs),
|
|
"incident_ref_count": len(incident_refs),
|
|
}
|
|
)
|
|
|
|
|
|
def _complete_results(refs: Sequence[ResourceRef]) -> list[ResourceRef]:
|
|
return [
|
|
ref
|
|
for ref in refs
|
|
if not bool(ref.metadata.get("missing"))
|
|
]
|
|
|
|
|
|
_SUCCESS_STATUSES_BY_REF_TYPE: Mapping[str, frozenset[str]] = {
|
|
"tool_event": frozenset({"ok", "passed", "success", "succeeded"}),
|
|
"tool_result": frozenset({"ok", "passed", "success", "succeeded"}),
|
|
"tool_incident": frozenset(),
|
|
}
|
|
|
|
|
|
def _conflicting_evidence_reasons(
|
|
*,
|
|
result_refs: Sequence[ResourceRef],
|
|
incident_refs: Sequence[ResourceRef],
|
|
) -> list[str]:
|
|
reasons: list[str] = []
|
|
for ref in sorted([*result_refs, *incident_refs], key=ref_sort_key):
|
|
if _is_successish_ref(ref):
|
|
reasons.append(f"{ref.ref_type}:{ref.ref_id}:success")
|
|
return reasons
|
|
|
|
|
|
def _is_successish_ref(ref: ResourceRef) -> bool:
|
|
metadata = ref.metadata
|
|
success = metadata.get("success")
|
|
if success is True or _truthy_text(success):
|
|
return True
|
|
status = str(metadata.get("status") or "").strip().lower()
|
|
if status in _SUCCESS_STATUSES_BY_REF_TYPE.get(ref.ref_type, frozenset()):
|
|
return True
|
|
is_error = metadata.get("is_error")
|
|
return is_error is False or _falsy_text(is_error)
|
|
|
|
|
|
def _is_permission_or_user_denied(ref: ResourceRef) -> bool:
|
|
metadata = ref.metadata
|
|
values = {
|
|
str(metadata.get(key) or "").strip().lower()
|
|
for key in (
|
|
"status",
|
|
"permission_status",
|
|
"error_type",
|
|
"error_bucket",
|
|
"exception_class",
|
|
"exception_type",
|
|
)
|
|
}
|
|
values.discard("")
|
|
if values.intersection({"blocked", "denied", "permission_denied", "rejected"}):
|
|
return True
|
|
text = " ".join(
|
|
str(metadata.get(key) or "").strip().lower()
|
|
for key in ("error_message", "message", "reason")
|
|
)
|
|
return "permission" in text and "denied" in text
|
|
|
|
|
|
def _truthy_text(value: Any) -> bool:
|
|
return isinstance(value, str) and value.strip().lower() in {"1", "true", "yes"}
|
|
|
|
|
|
def _falsy_text(value: Any) -> bool:
|
|
return isinstance(value, str) and value.strip().lower() in {"0", "false", "no"}
|
|
|
|
|
|
def _skill_version(context: SkillContextCandidate) -> str | None:
|
|
record = context.skill_record
|
|
if record is not None:
|
|
for key in ("skill_version", "version", "generation", "lineage_generation"):
|
|
value = record.metadata.get(key)
|
|
if value is not None and str(value) != "":
|
|
return str(value)
|
|
if context.skill_file is not None:
|
|
return str(context.skill_file.hash or context.skill_file.ref_id)
|
|
return None
|
|
|
|
|
|
def _normalized_error_type(metadata: Mapping[str, Any]) -> str:
|
|
return _metadata_text(
|
|
metadata,
|
|
"error_type",
|
|
"exception_type",
|
|
"failure_mode",
|
|
"error_bucket",
|
|
"error_code",
|
|
"exit_code",
|
|
)
|
|
|
|
|
|
def _normalized_result_shape(result_refs: Sequence[ResourceRef]) -> str:
|
|
shapes: list[str] = []
|
|
for ref in sorted(result_refs, key=ref_sort_key):
|
|
metadata = ref.metadata
|
|
source = _metadata_text(metadata, "persistence_source")
|
|
missing = "missing" if metadata.get("missing") else "present"
|
|
length = _metadata_text(metadata, "original_length", "persisted_output_size")
|
|
shapes.append(":".join(item for item in (source, missing, length) if item))
|
|
return "|".join(shapes)
|
|
|
|
|
|
def _metadata_text(metadata: Mapping[str, Any], *keys: str) -> str:
|
|
values = metadata_values(metadata, *keys)
|
|
if not values:
|
|
return ""
|
|
return sorted(values)[0]
|
|
|
|
|
|
def _without_none(values: Mapping[str, Any]) -> dict[str, Any]:
|
|
return {key: value for key, value in values.items() if value is not None}
|
|
|
|
|
|
def _signal_id(merge_key: str, source_ref_id: str) -> str:
|
|
return f"qsig_{_digest([merge_key, source_ref_id])}"
|
|
|
|
|
|
def _safe_number(value: Any) -> int | float | None:
|
|
if isinstance(value, (int, float)):
|
|
return value
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def _same_checkpoint_context(scope: EvidenceScope, ref: ResourceRef) -> bool:
|
|
if scope.session_id and ref.session_id != scope.session_id:
|
|
return False
|
|
|
|
task_ids = {
|
|
item
|
|
for item in (
|
|
scope.task_id,
|
|
*scope.source_task_ids,
|
|
)
|
|
if item
|
|
}
|
|
if task_ids:
|
|
ref_task_ids = {
|
|
str(item)
|
|
for item in (
|
|
ref.task_id,
|
|
ref.parent_task_id,
|
|
ref.metadata.get("task_id"),
|
|
ref.metadata.get("parent_task_id"),
|
|
)
|
|
if item
|
|
}
|
|
if not ref_task_ids.intersection(task_ids):
|
|
return False
|
|
|
|
if scope.agent_ids and ref.agent_id and ref.agent_id not in scope.agent_ids:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _digest(value: Any) -> str:
|
|
data = json.dumps(value, sort_keys=True, separators=(",", ":"), default=str)
|
|
return hashlib.sha256(data.encode("utf-8")).hexdigest()[:16]
|
|
|
|
|
|
def merge_signals_by_key(signals: Iterable[QualitySignal]) -> list[QualitySignal]:
|
|
merged: dict[str, QualitySignal] = {}
|
|
for signal in signals:
|
|
existing = merged.get(signal.merge_key)
|
|
if existing is None:
|
|
merged[signal.merge_key] = signal
|
|
continue
|
|
dominant = choose_dominant_signal(existing, signal)
|
|
merged[signal.merge_key] = replace(
|
|
dominant,
|
|
raw_backrefs=stable_unique((*existing.raw_backrefs, *signal.raw_backrefs)),
|
|
)
|
|
return list(merged.values())
|