mirror of
https://github.com/HKUDS/OpenSpace.git
synced 2026-08-28 05:15:00 +00:00
2095 lines
76 KiB
Python
2095 lines
76 KiB
Python
"""Mode-gated evolution engine orchestration.
|
|
|
|
The concrete packet, decision, admission, authoring, validation, candidate, and
|
|
commit collaborators are intentionally injected. This module owns the hard
|
|
mode boundary so no caller can accidentally bypass audit_only/fix_only/autonomous
|
|
semantics.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
import uuid
|
|
from dataclasses import dataclass, field, replace
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from typing import Any, Mapping
|
|
|
|
from openspace.skill_engine.patch import SKILL_FILENAME, collect_skill_snapshot
|
|
from openspace.skill_engine.registry import write_skill_id
|
|
from openspace.skill_engine.skill_utils import validate_skill_dir
|
|
from openspace.skill_engine.types import (
|
|
EvolutionType,
|
|
SkillCategory,
|
|
SkillLineage,
|
|
SkillOrigin,
|
|
SkillRecord,
|
|
SkillTrustState,
|
|
)
|
|
from .audit import EvolutionActionRecord
|
|
from .behavior_eval import (
|
|
SkillBehaviorEvalResult,
|
|
behavior_eval_feedback,
|
|
_replay_result_has_verified_executable_evidence,
|
|
_replay_task_result_failures,
|
|
)
|
|
from openspace.utils.logging import Logger
|
|
|
|
logger = Logger.get_logger(__name__)
|
|
|
|
_EVOLUTION_MODES = {"audit_only", "fix_only", "autonomous"}
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class EvolutionRunResult:
|
|
job_id: str
|
|
status: str
|
|
decisions: list[Any] = field(default_factory=list)
|
|
admissions: list[Any] = field(default_factory=list)
|
|
candidates: list[Any] = field(default_factory=list)
|
|
actions: list[Any] = field(default_factory=list)
|
|
behavior_evals: list[SkillBehaviorEvalResult] = field(default_factory=list)
|
|
evolved_skill_records: list[Any] = field(default_factory=list)
|
|
errors: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class EvolutionMutationOutcome:
|
|
action: Any | None = None
|
|
candidate: Any | None = None
|
|
behavior_evals: list[SkillBehaviorEvalResult] = field(default_factory=list)
|
|
blocked_reason: str | None = None
|
|
errors: list[str] = field(default_factory=list)
|
|
|
|
@property
|
|
def committed_action(self) -> Any | None:
|
|
if self.action is None:
|
|
return None
|
|
return self.action
|
|
|
|
|
|
class EvolutionEngine:
|
|
"""Process TriggerJobs through mode-gated evolution stages."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
packet_builder: Any | None = None,
|
|
decision_engine: Any | None = None,
|
|
admission_policy: Any | None = None,
|
|
candidate_store: Any | None = None,
|
|
authoring_backend: Any | None = None,
|
|
validator: Any | None = None,
|
|
behavior_evaluator: Any | None = None,
|
|
committer: Any | None = None,
|
|
evolution_mode: str = "autonomous",
|
|
behavior_eval_max_revisions: int = 2,
|
|
) -> None:
|
|
self.packet_builder = packet_builder
|
|
self.decision_engine = decision_engine
|
|
self.admission_policy = admission_policy
|
|
self.candidate_store = candidate_store
|
|
self.authoring_backend = authoring_backend
|
|
self.validator = validator
|
|
self.behavior_evaluator = behavior_evaluator
|
|
self.committer = committer
|
|
self.evolution_mode = _normalize_evolution_mode(evolution_mode)
|
|
self.behavior_eval_max_revisions = max(0, int(behavior_eval_max_revisions))
|
|
|
|
async def process_job(
|
|
self,
|
|
job: Any,
|
|
*,
|
|
evolution_mode: str | None = None,
|
|
) -> EvolutionRunResult:
|
|
mode = _normalize_evolution_mode(evolution_mode or self.evolution_mode)
|
|
job_id = str(getattr(job, "job_id", None) or getattr(job, "id", None) or "")
|
|
if not job_id:
|
|
job_id = "unknown"
|
|
job_status = str(getattr(job, "status", "") or "").strip().lower()
|
|
if job_status and job_status != "running":
|
|
return EvolutionRunResult(
|
|
job_id=job_id,
|
|
status="failed_unclaimed_job",
|
|
errors=[
|
|
"trigger job must be claimed/running before processing: "
|
|
f"{job_status}"
|
|
],
|
|
)
|
|
|
|
decisions: list[Any] = []
|
|
admissions: list[Any] = []
|
|
candidates: list[Any] = []
|
|
actions: list[Any] = []
|
|
behavior_evals: list[SkillBehaviorEvalResult] = []
|
|
evolved: list[Any] = []
|
|
errors: list[str] = []
|
|
packet: Any | None = None
|
|
|
|
try:
|
|
packet_result = await self._build_packet_result(job)
|
|
packet = _unwrap_packet_result(packet_result)
|
|
if packet is None:
|
|
decision = self._persist_packet_noop_decision(job, packet_result)
|
|
if decision is not None:
|
|
decisions.append(decision)
|
|
result = EvolutionRunResult(
|
|
job_id=job_id,
|
|
status="completed_noop",
|
|
decisions=decisions,
|
|
)
|
|
return result
|
|
|
|
decisions = list(await self._decide(packet, job))
|
|
if not decisions:
|
|
result = EvolutionRunResult(
|
|
job_id=job_id,
|
|
status="completed_noop",
|
|
decisions=[],
|
|
)
|
|
return result
|
|
|
|
for decision in decisions:
|
|
admission = await self._admit(decision, packet, job)
|
|
admissions.append(admission)
|
|
|
|
if mode == "audit_only":
|
|
continue
|
|
|
|
outcome = _admission_outcome(admission)
|
|
if outcome in {
|
|
"noop",
|
|
"reject",
|
|
"rejected",
|
|
"human_review",
|
|
"needs_human_review",
|
|
}:
|
|
continue
|
|
|
|
action_type = _decision_action_type(decision)
|
|
if mode == "fix_only" and action_type != EvolutionType.FIX.value:
|
|
candidate = await self._create_candidate(
|
|
decision,
|
|
admission,
|
|
packet,
|
|
job,
|
|
reason="fix_only_mode_non_fix",
|
|
)
|
|
if candidate is not None:
|
|
candidates.append(candidate)
|
|
continue
|
|
|
|
if outcome == "candidate":
|
|
candidate = await self._create_candidate(
|
|
decision,
|
|
admission,
|
|
packet,
|
|
job,
|
|
reason="admission_candidate",
|
|
)
|
|
if candidate is not None:
|
|
candidates.append(candidate)
|
|
continue
|
|
|
|
if mode == "fix_only" and outcome != "direct":
|
|
continue
|
|
|
|
committed_action = _committed_action_for_decision(self, decision)
|
|
if committed_action is not None:
|
|
actions.append(committed_action)
|
|
skill_record = _extract_skill_record(committed_action)
|
|
if skill_record is not None:
|
|
evolved.append(skill_record)
|
|
logger.info(
|
|
"Evolution decision %s already committed as action %s; "
|
|
"reusing durable result",
|
|
_decision_id(decision),
|
|
_action_id(committed_action),
|
|
)
|
|
continue
|
|
|
|
mutation = await self._author_validate_commit(
|
|
decision,
|
|
admission,
|
|
packet,
|
|
job,
|
|
)
|
|
if mutation.candidate is not None:
|
|
candidates.append(mutation.candidate)
|
|
behavior_evals.extend(mutation.behavior_evals)
|
|
if mutation.errors:
|
|
errors.extend(mutation.errors)
|
|
action = mutation.committed_action
|
|
if action is None:
|
|
if mutation.blocked_reason:
|
|
logger.info(
|
|
"Evolution direct action blocked before commit: %s",
|
|
mutation.blocked_reason,
|
|
)
|
|
else:
|
|
errors.append(
|
|
"direct action did not produce a committed action record"
|
|
)
|
|
continue
|
|
actions.append(action)
|
|
commit_status = _commit_status(action)
|
|
commit_succeeded = commit_status in {
|
|
"committed",
|
|
"committed_reconciled",
|
|
}
|
|
if commit_status and not commit_succeeded:
|
|
reason = (
|
|
getattr(action, "failure_reason", None)
|
|
or _mapping_get(action, "failure_reason")
|
|
or commit_status
|
|
)
|
|
errors.append(f"commit {commit_status}: {reason}")
|
|
skill_record = _extract_skill_record(action)
|
|
if skill_record is not None:
|
|
evolved.append(skill_record)
|
|
|
|
result = EvolutionRunResult(
|
|
job_id=job_id,
|
|
status="failed" if errors else "completed",
|
|
decisions=decisions,
|
|
admissions=admissions,
|
|
candidates=candidates,
|
|
actions=actions,
|
|
behavior_evals=behavior_evals,
|
|
evolved_skill_records=evolved,
|
|
errors=errors,
|
|
)
|
|
return result
|
|
except Exception as exc:
|
|
logger.debug("EvolutionEngine job failed: %s", job_id, exc_info=True)
|
|
errors.append(str(exc))
|
|
result = EvolutionRunResult(
|
|
job_id=job_id,
|
|
status="failed",
|
|
decisions=decisions,
|
|
admissions=admissions,
|
|
candidates=candidates,
|
|
actions=actions,
|
|
behavior_evals=behavior_evals,
|
|
evolved_skill_records=evolved,
|
|
errors=errors,
|
|
)
|
|
return result
|
|
|
|
async def _build_packet_result(self, job: Any) -> Any:
|
|
builder = self.packet_builder
|
|
if builder is None:
|
|
return getattr(job, "packet", None)
|
|
for name in ("build_trigger_packet", "build_packet", "build"):
|
|
method = getattr(builder, name, None)
|
|
if callable(method):
|
|
return await _maybe_await(method(job))
|
|
if callable(builder):
|
|
return await _maybe_await(builder(job))
|
|
return None
|
|
|
|
def _persist_packet_noop_decision(self, job: Any, packet_result: Any) -> Any | None:
|
|
store = _evidence_store_for(self)
|
|
if store is None:
|
|
return None
|
|
try:
|
|
from openspace.skill_engine.decision.types import DecisionRationale
|
|
|
|
status = (
|
|
getattr(packet_result, "status", None)
|
|
or _mapping_get(packet_result, "status")
|
|
or "packet_unavailable"
|
|
)
|
|
noop_reason = (
|
|
getattr(packet_result, "noop_reason", None)
|
|
or _mapping_get(packet_result, "noop_reason")
|
|
or str(status)
|
|
)
|
|
missing = _str_list(
|
|
getattr(packet_result, "missing_ref_types", None)
|
|
or _mapping_get(packet_result, "missing_ref_types")
|
|
)
|
|
decision = DecisionRationale(
|
|
decision_id=f"dec_{uuid.uuid4().hex}",
|
|
trigger_job_id=str(getattr(job, "job_id", "") or ""),
|
|
proposed_action="NOOP",
|
|
candidate_policy="never",
|
|
target_skill_ids=[],
|
|
reason_summary=f"Evidence packet unavailable: {noop_reason}",
|
|
reason_tags=[
|
|
"packet_unavailable",
|
|
f"packet_status:{status}",
|
|
*[f"missing_ref:{item}" for item in missing],
|
|
],
|
|
evidence_claims=[],
|
|
confidence=0.0,
|
|
risks=missing,
|
|
source_analysis_id=None,
|
|
noop_reason=str(noop_reason),
|
|
analyzed_by="evolution_engine",
|
|
created_at=datetime.now(timezone.utc).isoformat(),
|
|
)
|
|
persist = getattr(store, "persist_decision", None)
|
|
if callable(persist):
|
|
persist(decision, packet_id="")
|
|
return decision
|
|
except Exception:
|
|
logger.debug("Failed to persist packet NOOP decision", exc_info=True)
|
|
return None
|
|
|
|
async def _decide(self, packet: Any, job: Any) -> list[Any]:
|
|
engine = self.decision_engine
|
|
if engine is None:
|
|
return list(getattr(packet, "decisions", []) or [])
|
|
for name in ("decide", "process_packet"):
|
|
method = getattr(engine, name, None)
|
|
if callable(method):
|
|
result = await _maybe_await(method(packet))
|
|
return _as_list(result)
|
|
if callable(engine):
|
|
return _as_list(await _maybe_await(engine(packet, job)))
|
|
return []
|
|
|
|
async def _admit(self, decision: Any, packet: Any, job: Any) -> Any:
|
|
policy = self.admission_policy
|
|
if policy is None:
|
|
return getattr(decision, "admission", None) or {
|
|
"outcome": getattr(decision, "admission_outcome", "noop")
|
|
}
|
|
for name in ("admit", "evaluate"):
|
|
method = getattr(policy, name, None)
|
|
if callable(method):
|
|
if _accepts_positional_count(method, 3):
|
|
return await _maybe_await(method(decision, packet, job))
|
|
return await _maybe_await(method(decision, packet))
|
|
if callable(policy):
|
|
return await _maybe_await(policy(decision, packet, job))
|
|
return {"outcome": "noop"}
|
|
|
|
async def _create_candidate(
|
|
self,
|
|
decision: Any,
|
|
admission: Any,
|
|
packet: Any,
|
|
job: Any,
|
|
*,
|
|
reason: str,
|
|
) -> Any:
|
|
store = self.candidate_store
|
|
if store is None:
|
|
return {
|
|
"decision": decision,
|
|
"admission": admission,
|
|
"job": job,
|
|
"reason": reason,
|
|
}
|
|
for name in ("create_or_merge", "create"):
|
|
method = getattr(store, name, None)
|
|
if callable(method):
|
|
kwargs = {
|
|
"decision": decision,
|
|
"admission": admission,
|
|
"job": job,
|
|
"reason": reason,
|
|
}
|
|
if _accepts_keyword(method, "packet"):
|
|
kwargs["packet"] = packet
|
|
return await _maybe_await(method(**kwargs))
|
|
if callable(store):
|
|
if _accepts_positional_count(store, 5):
|
|
return await _maybe_await(
|
|
store(decision, admission, packet, job, reason)
|
|
)
|
|
return await _maybe_await(store(decision, admission, job, reason))
|
|
return None
|
|
|
|
async def _author_validate_commit(
|
|
self,
|
|
decision: Any,
|
|
admission: Any,
|
|
packet: Any,
|
|
job: Any,
|
|
) -> EvolutionMutationOutcome:
|
|
authoring = self.authoring_backend
|
|
if authoring is None:
|
|
logger.warning("Evolution authoring skipped: no authoring backend available")
|
|
return EvolutionMutationOutcome(errors=["missing_authoring_backend"])
|
|
method = getattr(authoring, "author_from_action_packet", None)
|
|
if not callable(method):
|
|
logger.warning(
|
|
"Evolution authoring backend does not expose "
|
|
"author_from_action_packet; skipping mutation"
|
|
)
|
|
return EvolutionMutationOutcome(errors=["invalid_authoring_backend"])
|
|
|
|
action_packet = await self._build_action_packet(decision, admission, packet)
|
|
if action_packet is None:
|
|
return EvolutionMutationOutcome(errors=["missing_action_packet"])
|
|
|
|
behavior_evals: list[SkillBehaviorEvalResult] = []
|
|
eval_feedback: Any | None = None
|
|
previous_authoring: Any | None = None
|
|
last_validation: Any | None = None
|
|
last_authoring: Any | None = None
|
|
for attempt in range(self.behavior_eval_max_revisions + 1):
|
|
authoring_result = await self._call_authoring_backend(
|
|
method,
|
|
action_packet,
|
|
eval_feedback=eval_feedback,
|
|
previous_authoring=previous_authoring,
|
|
)
|
|
last_authoring = authoring_result
|
|
if authoring_result is None:
|
|
return EvolutionMutationOutcome(
|
|
behavior_evals=behavior_evals,
|
|
errors=["authoring_returned_none"],
|
|
)
|
|
if not _authoring_staged(authoring_result):
|
|
errors = ["authoring_not_staged"]
|
|
failure_reason = _authoring_failure_reason(authoring_result)
|
|
if failure_reason:
|
|
errors.append(f"authoring_failure:{failure_reason}")
|
|
return EvolutionMutationOutcome(
|
|
behavior_evals=behavior_evals,
|
|
errors=errors,
|
|
)
|
|
|
|
validator = self.validator
|
|
validation_result = None
|
|
if validator is None:
|
|
logger.warning("Evolution validation skipped: no validator available")
|
|
return EvolutionMutationOutcome(
|
|
behavior_evals=behavior_evals,
|
|
errors=["missing_validator"],
|
|
)
|
|
validator_packet = await self._build_validator_packet(
|
|
authoring_result,
|
|
action_packet,
|
|
)
|
|
if validator_packet is None:
|
|
logger.warning("Evolution validation skipped: no validator packet available")
|
|
return EvolutionMutationOutcome(
|
|
behavior_evals=behavior_evals,
|
|
errors=["missing_validator_packet"],
|
|
)
|
|
validation_result = await self._call_validator(
|
|
validator,
|
|
authoring_result,
|
|
validator_packet,
|
|
decision,
|
|
admission,
|
|
job,
|
|
)
|
|
last_validation = validation_result
|
|
if not _validation_passed(validation_result):
|
|
candidate = await self._create_candidate(
|
|
decision,
|
|
admission,
|
|
packet,
|
|
job,
|
|
reason=_validation_candidate_reason(validation_result),
|
|
)
|
|
return EvolutionMutationOutcome(
|
|
candidate=candidate,
|
|
behavior_evals=behavior_evals,
|
|
blocked_reason="validation_failed",
|
|
)
|
|
|
|
behavior_result = await self._run_behavior_eval(
|
|
authoring_result,
|
|
validation_result,
|
|
decision,
|
|
admission,
|
|
action_packet,
|
|
)
|
|
if behavior_result is not None:
|
|
behavior_evals.append(behavior_result)
|
|
if not behavior_result.passed:
|
|
if attempt < self.behavior_eval_max_revisions:
|
|
eval_feedback = behavior_eval_feedback(behavior_result)
|
|
previous_authoring = authoring_result
|
|
continue
|
|
candidate = await self._create_candidate(
|
|
decision,
|
|
admission,
|
|
packet,
|
|
job,
|
|
reason=_behavior_eval_candidate_reason(behavior_result),
|
|
)
|
|
return EvolutionMutationOutcome(
|
|
candidate=candidate,
|
|
behavior_evals=behavior_evals,
|
|
blocked_reason=_behavior_eval_blocked_reason(behavior_result),
|
|
)
|
|
validation_result = _attach_behavior_eval_ref(
|
|
validation_result,
|
|
behavior_result,
|
|
)
|
|
else:
|
|
candidate = await self._create_candidate(
|
|
decision,
|
|
admission,
|
|
packet,
|
|
job,
|
|
reason="missing_behavior_eval",
|
|
)
|
|
return EvolutionMutationOutcome(
|
|
candidate=candidate,
|
|
behavior_evals=behavior_evals,
|
|
blocked_reason="missing_behavior_eval",
|
|
errors=["missing_behavior_eval"],
|
|
)
|
|
|
|
committer = self.committer
|
|
if committer is None:
|
|
logger.warning("Evolution commit skipped: no committer available")
|
|
return EvolutionMutationOutcome(
|
|
behavior_evals=behavior_evals,
|
|
errors=["missing_committer"],
|
|
)
|
|
for name in ("commit", "apply"):
|
|
commit_method = getattr(committer, name, None)
|
|
if callable(commit_method):
|
|
action = await _maybe_await(
|
|
commit_method(
|
|
authoring_result,
|
|
validation_result,
|
|
decision,
|
|
admission,
|
|
action_packet,
|
|
)
|
|
)
|
|
return EvolutionMutationOutcome(
|
|
action=action,
|
|
behavior_evals=behavior_evals,
|
|
)
|
|
if callable(committer):
|
|
action = await _maybe_await(
|
|
committer(
|
|
authoring_result,
|
|
validation_result,
|
|
decision,
|
|
admission,
|
|
action_packet,
|
|
)
|
|
)
|
|
return EvolutionMutationOutcome(
|
|
action=action,
|
|
behavior_evals=behavior_evals,
|
|
)
|
|
return EvolutionMutationOutcome(
|
|
action=authoring_result,
|
|
behavior_evals=behavior_evals,
|
|
)
|
|
return EvolutionMutationOutcome(
|
|
behavior_evals=behavior_evals,
|
|
blocked_reason="behavior_eval_revision_exhausted",
|
|
errors=[] if last_authoring is not None and last_validation is not None else [
|
|
"mutation_loop_exhausted_without_result"
|
|
],
|
|
)
|
|
|
|
async def _call_authoring_backend(
|
|
self,
|
|
method: Any,
|
|
action_packet: Any,
|
|
*,
|
|
eval_feedback: Any | None,
|
|
previous_authoring: Any | None,
|
|
) -> Any:
|
|
kwargs: dict[str, Any] = {}
|
|
if eval_feedback is not None and _accepts_keyword(method, "eval_feedback"):
|
|
kwargs["eval_feedback"] = eval_feedback
|
|
if previous_authoring is not None and _accepts_keyword(method, "previous_authoring"):
|
|
kwargs["previous_authoring"] = previous_authoring
|
|
if kwargs:
|
|
return await _maybe_await(method(action_packet, **kwargs))
|
|
return await _maybe_await(method(action_packet))
|
|
|
|
async def _run_behavior_eval(
|
|
self,
|
|
authoring_result: Any,
|
|
validation_result: Any,
|
|
decision: Any,
|
|
admission: Any,
|
|
action_packet: Any,
|
|
) -> SkillBehaviorEvalResult | None:
|
|
evaluator = self.behavior_evaluator
|
|
if evaluator is None:
|
|
logger.warning("Behavior eval skipped: no behavior evaluator available")
|
|
return None
|
|
for name in ("evaluate", "run"):
|
|
method = getattr(evaluator, name, None)
|
|
if callable(method):
|
|
return await _maybe_await(
|
|
method(
|
|
authoring_result,
|
|
validation_result,
|
|
decision,
|
|
admission,
|
|
action_packet,
|
|
)
|
|
)
|
|
if callable(evaluator):
|
|
return await _maybe_await(
|
|
evaluator(
|
|
authoring_result,
|
|
validation_result,
|
|
decision,
|
|
admission,
|
|
action_packet,
|
|
)
|
|
)
|
|
return None
|
|
|
|
async def _build_action_packet(
|
|
self,
|
|
decision: Any,
|
|
admission: Any,
|
|
packet: Any,
|
|
) -> Any | None:
|
|
builder = self.packet_builder
|
|
if builder is not None:
|
|
method = getattr(builder, "build_action_packet", None)
|
|
if callable(method):
|
|
action_scope = _decision_with_action_scope(decision, admission, packet)
|
|
result = await _maybe_await(method(action_scope))
|
|
return _unwrap_packet_result(result)
|
|
|
|
if str(getattr(packet, "packet_type", "") or "").lower() == "action":
|
|
return packet
|
|
logger.warning("Evolution authoring skipped: no action packet builder available")
|
|
return None
|
|
|
|
async def _build_validator_packet(
|
|
self,
|
|
authoring_result: Any,
|
|
action_packet: Any,
|
|
) -> Any | None:
|
|
builder = self.packet_builder
|
|
if builder is not None:
|
|
method = getattr(builder, "build_validator_packet", None)
|
|
if callable(method):
|
|
result = await _maybe_await(method(authoring_result))
|
|
return _unwrap_packet_result(result)
|
|
|
|
if str(getattr(action_packet, "packet_type", "") or "").lower() == "validator":
|
|
return action_packet
|
|
if builder is None:
|
|
return action_packet
|
|
return None
|
|
|
|
async def _call_validator(
|
|
self,
|
|
validator: Any,
|
|
authoring_result: Any,
|
|
validator_packet: Any,
|
|
decision: Any,
|
|
admission: Any,
|
|
job: Any,
|
|
) -> Any:
|
|
for name in ("validate_async", "validate", "run"):
|
|
method = getattr(validator, name, None)
|
|
if callable(method):
|
|
return await _invoke_validator(
|
|
method,
|
|
authoring_result,
|
|
validator_packet,
|
|
decision,
|
|
admission,
|
|
job,
|
|
)
|
|
if callable(validator):
|
|
return await _invoke_validator(
|
|
validator,
|
|
authoring_result,
|
|
validator_packet,
|
|
decision,
|
|
admission,
|
|
job,
|
|
)
|
|
return None
|
|
|
|
|
|
class EvolutionCommitter:
|
|
"""Commit approved staged edits to active skill storage with audit lineage."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
evidence_store: Any,
|
|
skill_store: Any,
|
|
registry: Any,
|
|
trigger_store: Any | None = None,
|
|
trigger_engine: Any | None = None,
|
|
backup_root: str | Path | None = None,
|
|
) -> None:
|
|
self.evidence_store = evidence_store
|
|
self.skill_store = skill_store
|
|
self.registry = registry
|
|
self.trigger_store = trigger_store or getattr(trigger_engine, "store", None)
|
|
self.trigger_engine = trigger_engine
|
|
if backup_root is not None:
|
|
self.backup_root = Path(backup_root).expanduser().resolve()
|
|
else:
|
|
db_path = getattr(evidence_store, "db_path", None)
|
|
if db_path is not None:
|
|
self.backup_root = Path(db_path).expanduser().resolve().parent / "evolution" / "backups"
|
|
else:
|
|
self.backup_root = Path.cwd() / ".openspace" / "evolution" / "backups"
|
|
|
|
async def commit(
|
|
self,
|
|
authoring: Any,
|
|
validation: Any,
|
|
decision: Any,
|
|
admission: Any,
|
|
action_packet: Any,
|
|
) -> EvolutionActionRecord:
|
|
action_type = _commit_action_type(decision, _attr(authoring, "staged_edit"))
|
|
self._check_preconditions(
|
|
authoring=authoring,
|
|
validation=validation,
|
|
decision=decision,
|
|
admission=admission,
|
|
action_packet=action_packet,
|
|
action_type=action_type,
|
|
)
|
|
staged = _attr(authoring, "staged_edit")
|
|
if staged is None:
|
|
raise ValueError("commit requires staged_edit")
|
|
|
|
target_dir = Path(str(_attr(staged, "target_dir") or "")).expanduser().resolve()
|
|
proposed_dir = _proposed_dir(staged, target_dir)
|
|
parent_skill_ids = _parent_skill_ids(staged, decision, action_type)
|
|
changed_files = _str_list(_attr(validation, "changed_files")) or _str_list(
|
|
_attr(staged, "changed_files")
|
|
)
|
|
evidence_refs = _str_list(_attr(validation, "provenance_refs")) or _str_list(
|
|
_attr(staged, "evidence_refs")
|
|
)
|
|
proposed_skill_id = self._resolve_commit_skill_id(
|
|
staged=staged,
|
|
action_type=action_type,
|
|
target_dir=target_dir,
|
|
parent_skill_ids=parent_skill_ids,
|
|
)
|
|
action_id = f"act_{uuid.uuid4().hex}"
|
|
backup_dir: Path | None = None
|
|
|
|
if action_type == "FIX":
|
|
backup_dir = self.backup_root / action_id / "before"
|
|
action = self.evidence_store.begin_action(
|
|
action_id=action_id,
|
|
decision_id=str(_attr(decision, "decision_id") or _attr(authoring, "decision_id") or ""),
|
|
trigger_job_id=str(
|
|
_attr(decision, "trigger_job_id")
|
|
or _attr(action_packet, "trigger_job_id")
|
|
or ""
|
|
),
|
|
authoring_id=str(_attr(authoring, "authoring_id") or ""),
|
|
validation_id=str(_attr(validation, "validation_id") or ""),
|
|
action_type=action_type,
|
|
skill_id=proposed_skill_id,
|
|
parent_skill_ids=parent_skill_ids,
|
|
changed_files=changed_files,
|
|
evidence_refs=evidence_refs,
|
|
staging_dir=str(_attr(staged, "staging_dir") or ""),
|
|
active_target_dir=str(target_dir),
|
|
backup_dir=str(backup_dir) if backup_dir is not None else None,
|
|
session_id=_packet_scope_value(action_packet, "session_id"),
|
|
task_id=_packet_scope_value(action_packet, "task_id"),
|
|
raw_backrefs=_commit_raw_backrefs(
|
|
authoring,
|
|
validation,
|
|
decision,
|
|
admission,
|
|
action_packet,
|
|
evidence_refs,
|
|
),
|
|
)
|
|
if backup_dir is not None:
|
|
backup_dir = Path(action.backup_dir or backup_dir)
|
|
|
|
phase = "begin"
|
|
store_written = False
|
|
new_record: SkillRecord | None = None
|
|
try:
|
|
phase = "backup"
|
|
if action_type == "FIX":
|
|
if backup_dir is None:
|
|
raise RuntimeError("FIX commit missing backup_dir")
|
|
_backup_target(target_dir, backup_dir)
|
|
|
|
phase = "active_copy"
|
|
_apply_proposed_dir(action_type, proposed_dir, target_dir)
|
|
|
|
phase = "disk_validation"
|
|
validation_error = validate_skill_dir(target_dir)
|
|
if validation_error:
|
|
raise RuntimeError(f"disk structural validation failed: {validation_error}")
|
|
|
|
phase = "skill_store"
|
|
new_record = self._build_skill_record(
|
|
action=action,
|
|
staged=staged,
|
|
authoring=authoring,
|
|
decision=decision,
|
|
action_packet=action_packet,
|
|
action_type=action_type,
|
|
target_dir=target_dir,
|
|
parent_skill_ids=parent_skill_ids,
|
|
evidence_refs=evidence_refs,
|
|
skill_id=proposed_skill_id,
|
|
)
|
|
if action_type in {"FIX", "DERIVED"}:
|
|
await _maybe_await(
|
|
self.skill_store.evolve_skill(new_record, parent_skill_ids)
|
|
)
|
|
else:
|
|
await _maybe_await(self.skill_store.save_record(new_record))
|
|
if action_type == "CAPTURED" and _source_validation_passed(admission):
|
|
record_trust = getattr(
|
|
self.skill_store,
|
|
"record_trust_observation",
|
|
None,
|
|
)
|
|
if callable(record_trust):
|
|
source_task_id = _packet_scope_value(action_packet, "task_id")
|
|
observation_id = (
|
|
f"task:{source_task_id}"
|
|
if source_task_id
|
|
else f"action:{action.action_id}"
|
|
)
|
|
try:
|
|
observed_record = await _maybe_await(
|
|
record_trust(
|
|
new_record.skill_id,
|
|
observation_id,
|
|
"success",
|
|
task_id=source_task_id or "",
|
|
session_id=(
|
|
_packet_scope_value(action_packet, "session_id") or ""
|
|
),
|
|
source="evolution_origin",
|
|
evidence_refs=evidence_refs,
|
|
)
|
|
)
|
|
if isinstance(observed_record, SkillRecord):
|
|
new_record = observed_record
|
|
except Exception:
|
|
logger.warning(
|
|
"Evolution trust origin record failed for %s",
|
|
new_record.skill_id,
|
|
exc_info=True,
|
|
)
|
|
store_written = True
|
|
|
|
phase = "skill_id_sidecar"
|
|
write_skill_id(target_dir, new_record.skill_id, raise_on_error=True)
|
|
|
|
phase = "local_category_tree"
|
|
materialized_dir = self._materialize_local_category_tree(
|
|
action_type=action_type,
|
|
target_dir=target_dir,
|
|
record=new_record,
|
|
decision=decision,
|
|
parent_skill_ids=parent_skill_ids,
|
|
)
|
|
if materialized_dir != target_dir:
|
|
target_dir = materialized_dir
|
|
new_record.path = str(target_dir / SKILL_FILENAME)
|
|
await _maybe_await(self.skill_store.save_record(new_record))
|
|
|
|
phase = "registry_refresh"
|
|
self._refresh_registry(action_type, target_dir, new_record, parent_skill_ids)
|
|
|
|
phase = "evidence_finalize"
|
|
finalized = self.evidence_store.finalize_action(
|
|
action.action_id,
|
|
status="committed",
|
|
skill_id=new_record.skill_id,
|
|
changed_files=changed_files,
|
|
backup_dir=str(backup_dir) if backup_dir is not None else None,
|
|
session_id=_packet_scope_value(action_packet, "session_id"),
|
|
task_id=_packet_scope_value(action_packet, "task_id"),
|
|
raw_backrefs=_commit_raw_backrefs(
|
|
authoring,
|
|
validation,
|
|
decision,
|
|
admission,
|
|
action_packet,
|
|
evidence_refs,
|
|
),
|
|
)
|
|
return _attach_skill_record(finalized, new_record)
|
|
except Exception as exc:
|
|
reason = f"{phase}: {exc}"
|
|
self._record_failure(action.action_id, phase, "failed", reason)
|
|
if store_written:
|
|
return _attach_skill_record(
|
|
_replace_action(
|
|
action,
|
|
skill_id=new_record.skill_id if new_record else proposed_skill_id,
|
|
failure_reason=reason,
|
|
),
|
|
new_record,
|
|
)
|
|
failed_status = "failed"
|
|
try:
|
|
_rollback_disk(action_type, target_dir, backup_dir)
|
|
except Exception as rollback_exc:
|
|
failed_status = "failed_needs_review"
|
|
reason = f"{reason}; rollback failed: {rollback_exc}"
|
|
self._record_failure(
|
|
action.action_id,
|
|
"rollback",
|
|
"failed_needs_review",
|
|
str(rollback_exc),
|
|
)
|
|
finalized = self.evidence_store.finalize_action(
|
|
action.action_id,
|
|
status=failed_status,
|
|
skill_id=proposed_skill_id,
|
|
changed_files=changed_files,
|
|
backup_dir=str(backup_dir) if backup_dir is not None else None,
|
|
failure_reason=reason,
|
|
session_id=_packet_scope_value(action_packet, "session_id"),
|
|
task_id=_packet_scope_value(action_packet, "task_id"),
|
|
raw_backrefs=_commit_raw_backrefs(
|
|
authoring,
|
|
validation,
|
|
decision,
|
|
admission,
|
|
action_packet,
|
|
evidence_refs,
|
|
),
|
|
)
|
|
return finalized
|
|
|
|
async def recover_committing_actions(
|
|
self,
|
|
*,
|
|
limit: int = 100,
|
|
) -> list[EvolutionActionRecord]:
|
|
from .recovery import EvolutionRecovery
|
|
|
|
recovered: list[EvolutionActionRecord] = []
|
|
actions = self.evidence_store.list_actions(status="committing", limit=limit)
|
|
recovery = EvolutionRecovery(
|
|
evidence_store=self.evidence_store,
|
|
skill_store=self.skill_store,
|
|
registry=self.registry,
|
|
)
|
|
for action in actions:
|
|
try:
|
|
recovery._reconcile_action(action) # pylint: disable=protected-access
|
|
finalized = self.evidence_store.load_action(action.action_id) or action
|
|
record = (
|
|
self.skill_store.load_record(finalized.skill_id)
|
|
if finalized.skill_id
|
|
else None
|
|
)
|
|
recovered.append(
|
|
_attach_skill_record(finalized, record)
|
|
if record is not None
|
|
else finalized
|
|
)
|
|
except Exception as exc:
|
|
reason = f"recovery: {exc}"
|
|
self._record_failure(
|
|
action.action_id,
|
|
"recovery",
|
|
"failed_retryable",
|
|
reason,
|
|
)
|
|
recovered.append(_replace_action(action, failure_reason=reason))
|
|
return recovered
|
|
|
|
def _check_preconditions(
|
|
self,
|
|
*,
|
|
authoring: Any,
|
|
validation: Any,
|
|
decision: Any | None = None,
|
|
admission: Any,
|
|
action_packet: Any | None = None,
|
|
action_type: str,
|
|
) -> None:
|
|
if str(_attr(authoring, "status") or "").strip().lower() != "staged":
|
|
raise ValueError("authoring result is not staged")
|
|
if str(_attr(validation, "outcome") or "").strip().lower() != "approve":
|
|
raise ValueError("validation result is not approved")
|
|
if str(_attr(admission, "outcome") or "").strip().lower() != "direct":
|
|
raise ValueError("admission result is not direct")
|
|
if action_type not in {"FIX", "DERIVED", "CAPTURED"}:
|
|
raise ValueError(f"unsupported commit action type: {action_type or '(missing)'}")
|
|
_require_approved_behavior_eval(
|
|
self.evidence_store,
|
|
authoring=authoring,
|
|
validation=validation,
|
|
decision=decision,
|
|
action_packet=action_packet,
|
|
action_type=action_type,
|
|
)
|
|
|
|
def _build_skill_record(
|
|
self,
|
|
*,
|
|
action: EvolutionActionRecord,
|
|
staged: Any,
|
|
authoring: Any,
|
|
decision: Any,
|
|
action_packet: Any,
|
|
action_type: str,
|
|
target_dir: Path,
|
|
parent_skill_ids: list[str],
|
|
evidence_refs: list[str],
|
|
skill_id: str,
|
|
) -> SkillRecord:
|
|
parent_records = _load_parent_records(self.skill_store, parent_skill_ids)
|
|
if action_type == "FIX" and len(parent_records) != 1:
|
|
raise RuntimeError("FIX commit requires exactly one SkillStore parent")
|
|
if action_type == "DERIVED" and not parent_records:
|
|
raise RuntimeError("DERIVED commit requires at least one SkillStore parent")
|
|
|
|
first_parent = parent_records[0] if parent_records else None
|
|
proposed_name = str(_attr(staged, "proposed_name") or target_dir.name)
|
|
proposed_description = str(
|
|
_attr(staged, "proposed_description")
|
|
or getattr(first_parent, "description", "")
|
|
or proposed_name
|
|
)
|
|
snapshot = _mapping_str_str(_attr(staged, "content_snapshot"))
|
|
if not snapshot:
|
|
snapshot = collect_skill_snapshot(target_dir)
|
|
content_diff = str(_attr(staged, "content_diff") or "")
|
|
source_task_id = (
|
|
_packet_scope_value(action_packet, "task_id")
|
|
or _none_or_str(_attr(decision, "source_task_id"))
|
|
or _none_or_str(_attr(decision, "source_analysis_id"))
|
|
)
|
|
change_summary = _change_summary(staged, decision, action)
|
|
provenance_refs = _dedupe_strs(evidence_refs)
|
|
content_hash = _content_snapshot_hash(snapshot)
|
|
revision_metadata = _revision_metadata(
|
|
evidence_store=self.evidence_store,
|
|
evidence_refs=provenance_refs,
|
|
revision_id=skill_id,
|
|
parent_revision_ids=parent_skill_ids,
|
|
content_hash=content_hash,
|
|
)
|
|
|
|
if action_type == "FIX":
|
|
assert first_parent is not None
|
|
generation = first_parent.lineage.generation + 1
|
|
origin = SkillOrigin.FIXED
|
|
category = first_parent.category
|
|
tags = list(first_parent.tags)
|
|
visibility = first_parent.visibility
|
|
creator_id = first_parent.creator_id
|
|
tool_dependencies = _str_list(_attr(staged, "tool_dependencies")) or list(
|
|
first_parent.tool_dependencies
|
|
)
|
|
critical_tools = _str_list(_attr(staged, "critical_tools")) or list(
|
|
first_parent.critical_tools
|
|
)
|
|
path = first_parent.path or str(target_dir / SKILL_FILENAME)
|
|
elif action_type == "DERIVED":
|
|
assert first_parent is not None
|
|
generation = max(record.lineage.generation for record in parent_records) + 1
|
|
origin = SkillOrigin.DERIVED
|
|
category = _decision_category(decision) or first_parent.category
|
|
tags = sorted({tag for record in parent_records for tag in record.tags})
|
|
visibility = first_parent.visibility
|
|
creator_id = first_parent.creator_id
|
|
tool_dependencies = _str_list(_attr(staged, "tool_dependencies")) or sorted(
|
|
{tool for record in parent_records for tool in record.tool_dependencies}
|
|
)
|
|
critical_tools = _str_list(_attr(staged, "critical_tools")) or sorted(
|
|
{tool for record in parent_records for tool in record.critical_tools}
|
|
)
|
|
path = str(target_dir / SKILL_FILENAME)
|
|
else:
|
|
generation = 0
|
|
origin = SkillOrigin.CAPTURED
|
|
category = _decision_category(decision) or SkillCategory.WORKFLOW
|
|
tags = []
|
|
visibility = getattr(first_parent, "visibility", None)
|
|
if visibility is None:
|
|
from openspace.skill_engine.types import SkillVisibility
|
|
|
|
visibility = SkillVisibility.PRIVATE
|
|
creator_id = getattr(first_parent, "creator_id", "")
|
|
tool_dependencies = _str_list(_attr(staged, "tool_dependencies"))
|
|
critical_tools = _str_list(_attr(staged, "critical_tools"))
|
|
path = str(target_dir / SKILL_FILENAME)
|
|
|
|
return SkillRecord(
|
|
skill_id=skill_id,
|
|
name=proposed_name,
|
|
description=proposed_description,
|
|
path=path,
|
|
is_active=True,
|
|
enabled=True,
|
|
trust_state=SkillTrustState.PROVISIONAL,
|
|
category=category,
|
|
tags=tags,
|
|
visibility=visibility,
|
|
creator_id=creator_id,
|
|
lineage=SkillLineage(
|
|
origin=origin,
|
|
revision_id=skill_id,
|
|
generation=generation,
|
|
parent_skill_ids=list(parent_skill_ids),
|
|
parent_revision_ids=list(parent_skill_ids),
|
|
source_task_id=source_task_id,
|
|
change_summary=change_summary,
|
|
content_hash=content_hash,
|
|
content_diff=content_diff,
|
|
content_snapshot=snapshot,
|
|
evolution_action_id=action.action_id,
|
|
provenance_refs=provenance_refs,
|
|
revision_metadata=revision_metadata,
|
|
created_by=str(_attr(authoring, "model") or ""),
|
|
),
|
|
tool_dependencies=tool_dependencies,
|
|
critical_tools=critical_tools,
|
|
)
|
|
|
|
def _resolve_commit_skill_id(
|
|
self,
|
|
*,
|
|
staged: Any,
|
|
action_type: str,
|
|
target_dir: Path,
|
|
parent_skill_ids: list[str],
|
|
) -> str:
|
|
proposed_skill_id = _none_or_str(_attr(staged, "proposed_skill_id"))
|
|
if proposed_skill_id:
|
|
return proposed_skill_id
|
|
parent_records = _load_parent_records(self.skill_store, parent_skill_ids)
|
|
first_parent = parent_records[0] if parent_records else None
|
|
proposed_name = str(_attr(staged, "proposed_name") or target_dir.name)
|
|
return _new_skill_id(proposed_name, action_type, first_parent)
|
|
|
|
def _materialize_local_category_tree(
|
|
self,
|
|
*,
|
|
action_type: str,
|
|
target_dir: Path,
|
|
record: SkillRecord,
|
|
decision: Any,
|
|
parent_skill_ids: list[str],
|
|
) -> Path:
|
|
try:
|
|
from openspace.cloud.local_mapping import CloudLocalMappingStore
|
|
from openspace.cloud.skill_classification import (
|
|
build_local_category_path,
|
|
classify_skill_dir,
|
|
initialize_local_skill_taxonomy,
|
|
materialize_skill_category_tree,
|
|
persist_skill_classification,
|
|
)
|
|
|
|
db_path = getattr(self.skill_store, "db_path", None)
|
|
if db_path is None and getattr(self.skill_store, "base", None) is not None:
|
|
db_path = getattr(self.skill_store.base, "db_path", None)
|
|
mapping_store = CloudLocalMappingStore(db_path)
|
|
try:
|
|
parent_records = _load_parent_records(self.skill_store, parent_skill_ids)
|
|
if parent_records:
|
|
initialize_local_skill_taxonomy(
|
|
mapping_store=mapping_store,
|
|
skills=parent_records,
|
|
)
|
|
parent_classification = None
|
|
parent_cloud_path = ""
|
|
for parent_id in parent_skill_ids:
|
|
parent_classification = mapping_store.get_skill_local_classification(parent_id)
|
|
parent_binding = mapping_store.get_skill_cloud_binding_by_local(parent_id)
|
|
if parent_binding is not None and not parent_cloud_path:
|
|
parent_cloud_path = (
|
|
parent_binding.current_package_path
|
|
or parent_binding.package_path_at_pull
|
|
or ""
|
|
)
|
|
if parent_classification is not None:
|
|
break
|
|
|
|
decision_path = _decision_local_category_path(decision)
|
|
inherited_path = ""
|
|
if (
|
|
parent_classification is not None
|
|
and parent_classification.local_category_path
|
|
and action_type in {"FIX", "DERIVED"}
|
|
):
|
|
inherited_path = parent_classification.local_category_path
|
|
selected_path = decision_path or inherited_path
|
|
classification = classify_skill_dir(
|
|
target_dir,
|
|
local_skill_id=record.skill_id,
|
|
cloud_package_path=parent_cloud_path or None,
|
|
local_category=record.category.value,
|
|
local_category_path=selected_path,
|
|
origin=_classification_origin(action_type),
|
|
)
|
|
category = record.category.value
|
|
local_category_path = build_local_category_path(
|
|
category,
|
|
local_category_path=selected_path,
|
|
cloud_package_path=parent_cloud_path or None,
|
|
local_path=str(target_dir),
|
|
name=record.name,
|
|
)
|
|
if not decision_path and parent_classification is not None:
|
|
if action_type == "FIX":
|
|
category = parent_classification.category
|
|
local_category_path = parent_classification.local_category_path
|
|
elif action_type == "DERIVED" and inherited_path:
|
|
local_category_path = parent_classification.local_category_path
|
|
|
|
classification = replace(
|
|
classification,
|
|
category=category,
|
|
local_category_path=local_category_path,
|
|
evidence={
|
|
**dict(classification.evidence or {}),
|
|
"origin": _classification_origin(action_type),
|
|
"evolution_action_type": action_type,
|
|
"parent_skill_ids": list(parent_skill_ids),
|
|
},
|
|
)
|
|
saved = persist_skill_classification(mapping_store, classification)
|
|
return materialize_skill_category_tree(
|
|
target_dir,
|
|
saved,
|
|
skills_root=target_dir.parent,
|
|
)
|
|
finally:
|
|
mapping_store.close()
|
|
except Exception as exc:
|
|
logger.debug("local category tree materialization skipped: %s", exc)
|
|
return target_dir
|
|
|
|
def _refresh_registry(
|
|
self,
|
|
action_type: str,
|
|
target_dir: Path,
|
|
record: SkillRecord,
|
|
parent_skill_ids: list[str],
|
|
) -> None:
|
|
meta = self.registry.load_skill_from_dir(target_dir)
|
|
if meta is None:
|
|
raise RuntimeError(f"registry could not load skill from {target_dir}")
|
|
if action_type == "FIX":
|
|
old_skill_id = parent_skill_ids[0] if parent_skill_ids else record.skill_id
|
|
self.registry.update_skill(old_skill_id, meta)
|
|
else:
|
|
self.registry.add_skill(meta)
|
|
|
|
|
|
def _record_failure(
|
|
self,
|
|
action_id: str,
|
|
phase: str,
|
|
status: str,
|
|
reason: str,
|
|
) -> None:
|
|
recorder = getattr(self.evidence_store, "record_action_failure", None)
|
|
if not callable(recorder):
|
|
return
|
|
try:
|
|
recorder(action_id, phase=phase, status=status, error=reason)
|
|
except Exception:
|
|
logger.debug("Failed to record evolution action failure", exc_info=True)
|
|
|
|
|
|
def _commit_action_type(decision: Any, staged: Any = None) -> str:
|
|
raw = (
|
|
_attr(decision, "proposed_action")
|
|
or _attr(decision, "action_type")
|
|
or _attr(decision, "evolution_type")
|
|
or _attr(staged, "action_type")
|
|
or ""
|
|
)
|
|
return str(getattr(raw, "value", raw) or "").strip().upper()
|
|
|
|
|
|
def _classification_origin(action_type: str) -> str:
|
|
return {
|
|
"FIX": "fix",
|
|
"DERIVED": "derive",
|
|
"CAPTURED": "capture",
|
|
}.get(action_type, "imported")
|
|
|
|
|
|
def _attr(value: Any, name: str, default: Any = None) -> Any:
|
|
if value is None:
|
|
return default
|
|
if isinstance(value, Mapping):
|
|
return value.get(name, default)
|
|
return getattr(value, name, default)
|
|
|
|
|
|
def _proposed_dir(staged: Any, target_dir: Path) -> Path:
|
|
staging_dir = Path(str(_attr(staged, "staging_dir") or "")).expanduser().resolve()
|
|
proposed_root = staging_dir / "proposed"
|
|
preferred = proposed_root / target_dir.name
|
|
if preferred.is_dir():
|
|
return preferred
|
|
proposed_name = str(_attr(staged, "proposed_name") or "")
|
|
if proposed_name and (proposed_root / proposed_name).is_dir():
|
|
return proposed_root / proposed_name
|
|
children = [path for path in proposed_root.iterdir()] if proposed_root.is_dir() else []
|
|
dirs = [path for path in children if path.is_dir()]
|
|
if len(dirs) == 1:
|
|
return dirs[0]
|
|
raise RuntimeError(f"Cannot resolve proposed staging dir under {proposed_root}")
|
|
|
|
|
|
def _parent_skill_ids(staged: Any, decision: Any, action_type: str) -> list[str]:
|
|
ids = _str_list(_attr(staged, "parent_skill_ids"))
|
|
if not ids:
|
|
ids = _str_list(_attr(staged, "target_skill_ids"))
|
|
if not ids:
|
|
ids = _str_list(_attr(decision, "target_skill_ids")) or _str_list(
|
|
_attr(decision, "target_skills")
|
|
)
|
|
return [] if action_type == "CAPTURED" else _dedupe_strs(ids)
|
|
|
|
|
|
def _backup_target(target_dir: Path, backup_dir: Path) -> None:
|
|
if not target_dir.is_dir():
|
|
raise RuntimeError(f"active target dir not found: {target_dir}")
|
|
if backup_dir.exists():
|
|
shutil.rmtree(backup_dir)
|
|
backup_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copytree(target_dir, backup_dir)
|
|
|
|
|
|
def _apply_proposed_dir(action_type: str, proposed_dir: Path, target_dir: Path) -> None:
|
|
if not proposed_dir.is_dir():
|
|
raise RuntimeError(f"proposed staging dir not found: {proposed_dir}")
|
|
if action_type == "FIX":
|
|
if target_dir.exists():
|
|
shutil.rmtree(target_dir)
|
|
shutil.copytree(proposed_dir, target_dir)
|
|
return
|
|
if target_dir.exists():
|
|
raise RuntimeError(f"active target already exists: {target_dir}")
|
|
target_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copytree(proposed_dir, target_dir)
|
|
|
|
|
|
def _rollback_disk(
|
|
action_type: str,
|
|
target_dir: Path,
|
|
backup_dir: Path | None,
|
|
) -> None:
|
|
if action_type == "FIX":
|
|
if backup_dir is None or not backup_dir.is_dir():
|
|
raise RuntimeError("missing backup for FIX rollback")
|
|
if target_dir.exists():
|
|
shutil.rmtree(target_dir)
|
|
shutil.copytree(backup_dir, target_dir)
|
|
return
|
|
if target_dir.exists():
|
|
shutil.rmtree(target_dir)
|
|
|
|
|
|
def _load_parent_records(skill_store: Any, parent_skill_ids: list[str]) -> list[SkillRecord]:
|
|
records: list[SkillRecord] = []
|
|
for skill_id in parent_skill_ids:
|
|
record = skill_store.load_record(skill_id)
|
|
if record is None:
|
|
raise RuntimeError(f"missing parent SkillRecord: {skill_id}")
|
|
records.append(record)
|
|
return records
|
|
|
|
|
|
def _new_skill_id(
|
|
proposed_name: str,
|
|
action_type: str,
|
|
parent: SkillRecord | None,
|
|
) -> str:
|
|
if action_type == "FIX" and parent is not None:
|
|
generation = parent.lineage.generation + 1
|
|
return f"{proposed_name}__v{generation}_{uuid.uuid4().hex[:8]}"
|
|
return f"{proposed_name}__v0_{uuid.uuid4().hex[:8]}"
|
|
|
|
|
|
def _mapping_str_str(value: Any) -> dict[str, str]:
|
|
if not isinstance(value, Mapping):
|
|
return {}
|
|
return {str(key): str(item) for key, item in value.items()}
|
|
|
|
|
|
def _packet_scope_value(packet: Any, key: str) -> str | None:
|
|
scope = _attr(packet, "scope")
|
|
return _none_or_str(_attr(scope, key))
|
|
|
|
|
|
def _commit_raw_backrefs(
|
|
authoring: Any,
|
|
validation: Any,
|
|
decision: Any,
|
|
admission: Any,
|
|
action_packet: Any,
|
|
evidence_refs: list[str],
|
|
) -> list[str]:
|
|
refs = [
|
|
f"authoring:{_attr(authoring, 'authoring_id')}" if _attr(authoring, "authoring_id") else "",
|
|
f"validation:{_attr(validation, 'validation_id')}" if _attr(validation, "validation_id") else "",
|
|
f"decision:{_attr(decision, 'decision_id')}" if _attr(decision, "decision_id") else "",
|
|
f"admission:{_attr(admission, 'admission_id')}" if _attr(admission, "admission_id") else "",
|
|
f"packet:{_attr(action_packet, 'packet_id')}" if _attr(action_packet, "packet_id") else "",
|
|
*evidence_refs,
|
|
]
|
|
return _dedupe_strs(refs)
|
|
|
|
|
|
def _change_summary(staged: Any, decision: Any, action: EvolutionActionRecord) -> str:
|
|
apply_metadata = _attr(staged, "apply_metadata")
|
|
if isinstance(apply_metadata, Mapping):
|
|
summary = str(apply_metadata.get("change_summary") or "").strip()
|
|
else:
|
|
summary = ""
|
|
if not summary:
|
|
summary = str(_attr(decision, "reason_summary") or "").strip()
|
|
audit = f"evolution_action={action.action_id}; decision={action.decision_id}"
|
|
return f"{summary}\n\nAudit: {audit}" if summary else f"Audit: {audit}"
|
|
|
|
|
|
def _content_snapshot_hash(snapshot: Mapping[str, str]) -> str:
|
|
payload = json.dumps(
|
|
{str(key): str(value) for key, value in sorted(snapshot.items())},
|
|
sort_keys=True,
|
|
ensure_ascii=False,
|
|
)
|
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _revision_metadata(
|
|
*,
|
|
evidence_store: Any,
|
|
evidence_refs: list[str],
|
|
revision_id: str,
|
|
parent_revision_ids: list[str],
|
|
content_hash: str,
|
|
) -> dict[str, Any]:
|
|
metadata: dict[str, Any] = {
|
|
"revision_id": revision_id,
|
|
"parent_revision_ids": list(parent_revision_ids),
|
|
"content_hash": content_hash,
|
|
"behavior_eval_refs": [
|
|
ref for ref in evidence_refs if str(ref).startswith("behavior_eval:")
|
|
],
|
|
}
|
|
loader = getattr(evidence_store, "load_behavior_eval", None)
|
|
if not callable(loader):
|
|
return metadata
|
|
evals: list[dict[str, Any]] = []
|
|
for ref in metadata["behavior_eval_refs"]:
|
|
eval_id = str(ref).split(":", 1)[1]
|
|
try:
|
|
result = loader(eval_id)
|
|
except Exception:
|
|
logger.debug("Failed to load behavior eval for revision metadata", exc_info=True)
|
|
continue
|
|
if result is None:
|
|
continue
|
|
replay = getattr(result, "replay_eval", None)
|
|
evals.append(
|
|
{
|
|
"eval_id": getattr(result, "eval_id", eval_id),
|
|
"outcome": getattr(result, "outcome", ""),
|
|
"replay_run_id": getattr(replay, "replay_run_id", ""),
|
|
"sandbox_run_id": getattr(replay, "sandbox_run_id", ""),
|
|
"judge_result_id": getattr(replay, "judge_result_id", ""),
|
|
"baseline_revision_set": list(
|
|
getattr(replay, "baseline_revision_set", []) or []
|
|
),
|
|
"candidate_revision_set": list(
|
|
getattr(replay, "candidate_revision_set", []) or []
|
|
),
|
|
"baseline_score": getattr(replay, "baseline_score", None),
|
|
"candidate_score": getattr(replay, "candidate_score", None),
|
|
"artifact_refs": list(getattr(replay, "artifact_refs", []) or []),
|
|
}
|
|
)
|
|
if evals:
|
|
metadata["behavior_evals"] = evals
|
|
metadata["latest_behavior_eval"] = evals[-1]
|
|
return metadata
|
|
|
|
|
|
def _decision_category(decision: Any) -> SkillCategory | None:
|
|
value = _attr(decision, "category")
|
|
if not value:
|
|
return None
|
|
try:
|
|
return value if isinstance(value, SkillCategory) else SkillCategory(str(value))
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _decision_local_category_path(decision: Any) -> str:
|
|
return str(_attr(decision, "local_category_path") or "").strip()
|
|
|
|
|
|
def _attach_skill_record(
|
|
action: EvolutionActionRecord,
|
|
record: SkillRecord | None,
|
|
) -> EvolutionActionRecord:
|
|
if record is None:
|
|
return action
|
|
from dataclasses import replace
|
|
|
|
return replace(action, skill_record=record)
|
|
|
|
|
|
def _replace_action(action: EvolutionActionRecord, **changes: Any) -> EvolutionActionRecord:
|
|
from dataclasses import replace
|
|
|
|
return replace(action, **changes)
|
|
|
|
|
|
def _dedupe_strs(values: list[Any]) -> list[str]:
|
|
return [
|
|
text
|
|
for text in dict.fromkeys(str(item) for item in values if item is not None)
|
|
if text
|
|
]
|
|
|
|
|
|
def _none_or_str(value: Any) -> str | None:
|
|
if value is None:
|
|
return None
|
|
text = str(value)
|
|
return text if text else None
|
|
|
|
|
|
def _strict_bool(value: Any) -> bool | None:
|
|
if isinstance(value, bool):
|
|
return value
|
|
if isinstance(value, int) and value in {0, 1}:
|
|
return bool(value)
|
|
if isinstance(value, str):
|
|
text = value.strip().lower()
|
|
if text in {"true", "1", "yes", "y"}:
|
|
return True
|
|
if text in {"false", "0", "no", "n"}:
|
|
return False
|
|
return None
|
|
|
|
|
|
def _str_list(value: Any) -> list[str]:
|
|
if value is None:
|
|
return []
|
|
if isinstance(value, str):
|
|
return [value] if value else []
|
|
if isinstance(value, (list, tuple, set)):
|
|
return [str(item) for item in value if str(item)]
|
|
return []
|
|
|
|
|
|
def _normalize_evolution_mode(value: str) -> str:
|
|
mode = str(value or "autonomous").strip().lower()
|
|
if mode not in _EVOLUTION_MODES:
|
|
raise ValueError(
|
|
"evolution_mode must be one of: audit_only, fix_only, autonomous"
|
|
)
|
|
return mode
|
|
|
|
|
|
def _decision_action_type(decision: Any) -> str:
|
|
raw = (
|
|
getattr(decision, "proposed_action", None)
|
|
or getattr(decision, "action_type", None)
|
|
or getattr(decision, "evolution_type", None)
|
|
or _mapping_get(decision, "proposed_action")
|
|
or _mapping_get(decision, "action_type")
|
|
or _mapping_get(decision, "evolution_type")
|
|
or ""
|
|
)
|
|
if isinstance(raw, EvolutionType):
|
|
return raw.value
|
|
return str(raw).strip().lower()
|
|
|
|
|
|
def _admission_outcome(admission: Any) -> str:
|
|
raw = getattr(admission, "outcome", None) or _mapping_get(admission, "outcome")
|
|
return str(raw or "noop").strip().lower()
|
|
|
|
|
|
def _validation_passed(validation: Any) -> bool:
|
|
if validation is None:
|
|
return False
|
|
if isinstance(validation, bool):
|
|
return validation
|
|
outcome = getattr(validation, "outcome", None) or _mapping_get(validation, "outcome")
|
|
if outcome is not None:
|
|
return str(outcome).strip().lower() == "approve"
|
|
raw = (
|
|
getattr(validation, "passed", None)
|
|
if getattr(validation, "passed", None) is not None
|
|
else _mapping_get(validation, "passed")
|
|
)
|
|
if raw is None:
|
|
raw = getattr(validation, "status", None) or _mapping_get(validation, "status")
|
|
return str(raw).strip().lower() in {"passed", "success", "ok"}
|
|
return _strict_bool(raw) is True
|
|
|
|
|
|
def _validation_candidate_reason(validation: Any) -> str:
|
|
deterministic = _str_list(
|
|
getattr(validation, "deterministic_failures", None)
|
|
or _mapping_get(validation, "deterministic_failures")
|
|
)
|
|
semantic = _str_list(
|
|
getattr(validation, "semantic_warnings", None)
|
|
or _mapping_get(validation, "semantic_warnings")
|
|
)
|
|
if semantic and not deterministic:
|
|
return "semantic_validation_failed"
|
|
return "validation_failed"
|
|
|
|
|
|
def _attach_behavior_eval_ref(
|
|
validation: Any,
|
|
behavior_result: SkillBehaviorEvalResult,
|
|
) -> Any:
|
|
if validation is None:
|
|
return validation
|
|
ref_id = behavior_result.ref_id
|
|
current = _str_list(
|
|
getattr(validation, "provenance_refs", None)
|
|
or _mapping_get(validation, "provenance_refs")
|
|
)
|
|
refs = _dedupe_strs([*current, ref_id])
|
|
try:
|
|
return replace(validation, provenance_refs=refs)
|
|
except Exception:
|
|
if isinstance(validation, dict):
|
|
updated = dict(validation)
|
|
updated["provenance_refs"] = refs
|
|
return updated
|
|
if hasattr(validation, "provenance_refs"):
|
|
try:
|
|
setattr(validation, "provenance_refs", refs)
|
|
return validation
|
|
except Exception:
|
|
logger.debug(
|
|
"Failed to attach behavior eval ref to mutable validation object",
|
|
exc_info=True,
|
|
)
|
|
return validation
|
|
|
|
|
|
def _has_behavior_eval_ref(validation: Any, staged: Any = None) -> bool:
|
|
del staged
|
|
return bool(_validation_behavior_eval_refs(validation))
|
|
|
|
|
|
def _require_approved_behavior_eval(
|
|
evidence_store: Any,
|
|
*,
|
|
authoring: Any,
|
|
validation: Any,
|
|
decision: Any | None,
|
|
action_packet: Any | None,
|
|
action_type: str,
|
|
) -> SkillBehaviorEvalResult:
|
|
refs = _validation_behavior_eval_refs(validation)
|
|
if not refs:
|
|
raise ValueError("commit requires approved behavior eval provenance ref")
|
|
loader = getattr(evidence_store, "load_behavior_eval", None)
|
|
if not callable(loader):
|
|
raise ValueError("commit requires behavior eval evidence store lookup")
|
|
|
|
checked_failures: list[str] = []
|
|
for ref in refs:
|
|
eval_id = _behavior_eval_id_from_ref(ref)
|
|
if not eval_id:
|
|
continue
|
|
try:
|
|
result = loader(eval_id)
|
|
except Exception as exc:
|
|
checked_failures.append(f"{ref}:load_failed:{str(exc)[:120]}")
|
|
logger.debug("Failed to load behavior eval ref %s", ref, exc_info=True)
|
|
continue
|
|
failures = _behavior_eval_commit_gate_failures(
|
|
result,
|
|
evidence_store=evidence_store,
|
|
authoring=authoring,
|
|
validation=validation,
|
|
decision=decision,
|
|
action_packet=action_packet,
|
|
action_type=action_type,
|
|
)
|
|
if not failures:
|
|
return result
|
|
checked_failures.append(f"{ref}:{','.join(failures[:4])}")
|
|
|
|
detail = f" ({'; '.join(checked_failures[:3])})" if checked_failures else ""
|
|
raise ValueError(f"commit requires approved behavior eval provenance ref{detail}")
|
|
|
|
|
|
def _validation_behavior_eval_refs(validation: Any) -> list[str]:
|
|
refs = _str_list(
|
|
getattr(validation, "provenance_refs", None)
|
|
or _mapping_get(validation, "provenance_refs")
|
|
)
|
|
return [
|
|
ref
|
|
for ref in refs
|
|
if _behavior_eval_id_from_ref(ref)
|
|
]
|
|
|
|
|
|
def _behavior_eval_id_from_ref(ref: Any) -> str:
|
|
text = str(ref or "").strip()
|
|
if not text.startswith("behavior_eval:"):
|
|
return ""
|
|
eval_id = text.split(":", 1)[1].strip()
|
|
return eval_id if eval_id else ""
|
|
|
|
|
|
def _behavior_eval_commit_gate_failures(
|
|
result: Any,
|
|
*,
|
|
evidence_store: Any | None = None,
|
|
authoring: Any,
|
|
validation: Any,
|
|
decision: Any | None,
|
|
action_packet: Any | None,
|
|
action_type: str,
|
|
) -> list[str]:
|
|
if result is None:
|
|
return ["missing_behavior_eval_result"]
|
|
failures: list[str] = []
|
|
if str(_attr(result, "outcome") or "").strip().lower() != "approve":
|
|
failures.append("behavior_eval_not_approved")
|
|
behavior_failures = _str_list(_attr(result, "failures"))
|
|
if behavior_failures:
|
|
failures.extend(f"behavior_eval_failure:{item}" for item in behavior_failures)
|
|
optional_replay_allowed = _behavior_eval_allows_optional_replay(result)
|
|
replay = _attr(result, "replay_eval")
|
|
if not replay:
|
|
if optional_replay_allowed:
|
|
pass
|
|
else:
|
|
failures.append("missing_replay_eval")
|
|
elif optional_replay_allowed:
|
|
replay_mapping = _replay_eval_gate_mapping(replay)
|
|
failures.extend(_replay_task_result_failures(replay_mapping))
|
|
else:
|
|
if _strict_bool(_attr(replay, "attempted")) is not True:
|
|
failures.append("replay_eval_not_attempted")
|
|
if _strict_bool(_attr(replay, "passed")) is not True:
|
|
failures.append("replay_eval_not_passed")
|
|
replay_mapping = _replay_eval_gate_mapping(replay)
|
|
if not _replay_result_has_verified_executable_evidence(
|
|
replay_mapping,
|
|
evidence_store,
|
|
):
|
|
failures.append("missing_executable_eval_evidence")
|
|
failures.extend(_replay_task_result_failures(replay_mapping))
|
|
|
|
expected = {
|
|
"authoring_id": str(_attr(authoring, "authoring_id") or ""),
|
|
"validation_id": str(_attr(validation, "validation_id") or ""),
|
|
"decision_id": str(
|
|
_attr(decision, "decision_id")
|
|
or _attr(authoring, "decision_id")
|
|
or ""
|
|
),
|
|
"packet_id": str(_attr(action_packet, "packet_id") or ""),
|
|
"action_type": str(action_type or "").strip().upper(),
|
|
}
|
|
for field_name, expected_value in expected.items():
|
|
if not expected_value:
|
|
failures.append(f"missing_behavior_eval_binding:{field_name}")
|
|
continue
|
|
actual = str(_attr(result, field_name) or "").strip()
|
|
if field_name == "action_type":
|
|
actual = actual.upper()
|
|
if actual != expected_value:
|
|
failures.append(f"behavior_eval_binding_mismatch:{field_name}")
|
|
return _dedupe_strs(failures)
|
|
|
|
|
|
def _behavior_eval_allows_optional_replay(result: Any) -> bool:
|
|
warnings = _str_list(_attr(result, "warnings"))
|
|
if any(str(item).startswith("optional_replay_eval_") for item in warnings):
|
|
return True
|
|
replay = _attr(result, "replay_eval")
|
|
if not replay:
|
|
return False
|
|
replay_warnings = _str_list(_attr(replay, "warnings"))
|
|
return any(
|
|
str(item).startswith("optional_replay_eval_") for item in replay_warnings
|
|
)
|
|
|
|
|
|
def _replay_eval_gate_mapping(replay: Any) -> dict[str, Any]:
|
|
if hasattr(replay, "to_dict") and callable(replay.to_dict):
|
|
data = replay.to_dict()
|
|
if isinstance(data, Mapping):
|
|
return dict(data)
|
|
if isinstance(replay, Mapping):
|
|
return dict(replay)
|
|
result: dict[str, Any] = {}
|
|
for key in (
|
|
"attempted",
|
|
"passed",
|
|
"runner",
|
|
"replay_run_id",
|
|
"sandbox_run_id",
|
|
"judge_result_id",
|
|
"baseline_revision_set",
|
|
"candidate_revision_set",
|
|
"baseline_score",
|
|
"candidate_score",
|
|
"artifact_refs",
|
|
"details",
|
|
"failures",
|
|
"warnings",
|
|
):
|
|
value = _attr(replay, key)
|
|
if value is not None:
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def _behavior_eval_blocked_reason(result: SkillBehaviorEvalResult) -> str:
|
|
if result.failures:
|
|
return f"behavior_eval_failed:{result.failures[0]}"
|
|
return f"behavior_eval_{result.outcome}"
|
|
|
|
|
|
def _behavior_eval_candidate_reason(result: SkillBehaviorEvalResult) -> str:
|
|
if result.failures:
|
|
return f"behavior_eval_failed:{result.failures[0]}"
|
|
return "behavior_eval_failed"
|
|
|
|
|
|
def _authoring_staged(authoring_result: Any) -> bool:
|
|
status = getattr(authoring_result, "status", None) or _mapping_get(
|
|
authoring_result, "status"
|
|
)
|
|
if status is None:
|
|
return False
|
|
return str(status).strip().lower() == "staged"
|
|
|
|
|
|
def _authoring_failure_reason(authoring_result: Any) -> str | None:
|
|
for key in ("failure_reason", "error", "message"):
|
|
raw = getattr(authoring_result, key, None) or _mapping_get(authoring_result, key)
|
|
if raw:
|
|
return str(raw).strip()[:500]
|
|
return None
|
|
|
|
|
|
def _extract_skill_record(action: Any) -> Any | None:
|
|
return (
|
|
getattr(action, "skill_record", None)
|
|
or getattr(action, "record", None)
|
|
or _mapping_get(action, "skill_record")
|
|
or _mapping_get(action, "record")
|
|
)
|
|
|
|
|
|
def _commit_status(action: Any) -> str:
|
|
raw = getattr(action, "commit_status", None) or _mapping_get(action, "commit_status")
|
|
return str(raw or "").strip().lower()
|
|
|
|
|
|
def _action_id(action: Any) -> str:
|
|
raw = getattr(action, "action_id", None) or _mapping_get(action, "action_id")
|
|
return str(raw or "").strip()
|
|
|
|
|
|
def _decision_id(decision: Any) -> str:
|
|
raw = getattr(decision, "decision_id", None) or _mapping_get(
|
|
decision,
|
|
"decision_id",
|
|
)
|
|
return str(raw or "").strip()
|
|
|
|
|
|
def _mapping_get(value: Any, key: str) -> Any:
|
|
return value.get(key) if isinstance(value, dict) else None
|
|
|
|
|
|
def _source_validation_passed(admission: Any) -> bool:
|
|
value = getattr(admission, "source_validation_passed", None)
|
|
if value is None:
|
|
value = _mapping_get(admission, "source_validation_passed")
|
|
return bool(value)
|
|
|
|
|
|
def _decision_with_action_scope(
|
|
decision: Any,
|
|
admission: Any,
|
|
packet: Any,
|
|
) -> Any:
|
|
decision_id = getattr(decision, "decision_id", None) or _mapping_get(
|
|
decision, "decision_id"
|
|
)
|
|
admission_id = getattr(admission, "admission_id", None) or _mapping_get(
|
|
admission, "admission_id"
|
|
)
|
|
trigger_job_id = getattr(decision, "trigger_job_id", None) or _mapping_get(
|
|
decision, "trigger_job_id"
|
|
)
|
|
return SimpleNamespace(
|
|
decision_id=decision_id,
|
|
admission_id=admission_id,
|
|
trigger_job_id=trigger_job_id,
|
|
packet=packet,
|
|
packet_id=getattr(packet, "packet_id", None) or _mapping_get(packet, "packet_id"),
|
|
)
|
|
|
|
|
|
def _accepts_positional_count(func: Any, count: int) -> bool:
|
|
try:
|
|
parameters = inspect.signature(func).parameters.values()
|
|
except (TypeError, ValueError):
|
|
return True
|
|
positional = 0
|
|
for parameter in parameters:
|
|
if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
|
|
return True
|
|
if parameter.kind in {
|
|
inspect.Parameter.POSITIONAL_ONLY,
|
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
}:
|
|
positional += 1
|
|
return positional >= count
|
|
|
|
|
|
def _accepts_keyword(func: Any, name: str) -> bool:
|
|
try:
|
|
parameters = inspect.signature(func).parameters.values()
|
|
except (TypeError, ValueError):
|
|
return True
|
|
for parameter in parameters:
|
|
if parameter.kind is inspect.Parameter.VAR_KEYWORD:
|
|
return True
|
|
if parameter.name == name:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _as_list(value: Any) -> list[Any]:
|
|
if value is None:
|
|
return []
|
|
decisions = getattr(value, "decisions", None)
|
|
if decisions is not None:
|
|
return list(decisions or [])
|
|
if isinstance(value, dict) and "decisions" in value:
|
|
return list(value.get("decisions") or [])
|
|
if isinstance(value, list):
|
|
return value
|
|
if isinstance(value, tuple):
|
|
return list(value)
|
|
return [value]
|
|
|
|
|
|
def _evidence_store_for(engine: EvolutionEngine) -> Any | None:
|
|
for owner in (
|
|
getattr(engine, "packet_builder", None),
|
|
getattr(engine, "decision_engine", None),
|
|
getattr(engine, "admission_policy", None),
|
|
getattr(engine, "candidate_store", None),
|
|
getattr(engine, "validator", None),
|
|
getattr(engine, "committer", None),
|
|
):
|
|
store = getattr(owner, "evidence_store", None)
|
|
if store is not None:
|
|
return store
|
|
return None
|
|
|
|
|
|
def _committed_action_for_decision(
|
|
engine: EvolutionEngine,
|
|
decision: Any,
|
|
) -> Any | None:
|
|
decision_id = _decision_id(decision)
|
|
if not decision_id:
|
|
return None
|
|
evidence_store = _evidence_store_for(engine)
|
|
loader = getattr(
|
|
evidence_store,
|
|
"load_committed_action_for_decision",
|
|
None,
|
|
)
|
|
if not callable(loader):
|
|
return None
|
|
action = loader(decision_id)
|
|
if action is None or _commit_status(action) not in {
|
|
"committed",
|
|
"committed_reconciled",
|
|
}:
|
|
return None
|
|
|
|
skill_id = str(
|
|
getattr(action, "skill_id", None)
|
|
or _mapping_get(action, "skill_id")
|
|
or ""
|
|
)
|
|
committer = getattr(engine, "committer", None)
|
|
skill_store = getattr(committer, "skill_store", None)
|
|
load_record = getattr(skill_store, "load_record", None)
|
|
if skill_id and callable(load_record):
|
|
record = load_record(skill_id)
|
|
if record is not None:
|
|
return _attach_skill_record(action, record)
|
|
return action
|
|
|
|
|
|
def _unwrap_packet_result(value: Any) -> Any:
|
|
if value is None:
|
|
return None
|
|
status = getattr(value, "status", None) or _mapping_get(value, "status")
|
|
packet = getattr(value, "packet", None) or _mapping_get(value, "packet")
|
|
if status is not None and packet is not None:
|
|
return packet if str(status).lower() == "ok" else None
|
|
if status is not None and packet is None:
|
|
return None
|
|
return value
|
|
|
|
|
|
async def _maybe_await(value: Any) -> Any:
|
|
if inspect.isawaitable(value):
|
|
return await value
|
|
return value
|
|
|
|
|
|
async def _invoke_validator(
|
|
method: Any,
|
|
authoring_result: Any,
|
|
validator_packet: Any,
|
|
decision: Any,
|
|
admission: Any,
|
|
job: Any,
|
|
) -> Any:
|
|
if _accepts_positional_count(method, 5):
|
|
return await _maybe_await(
|
|
method(authoring_result, validator_packet, decision, admission, job)
|
|
)
|
|
if _accepts_positional_count(method, 4):
|
|
return await _maybe_await(
|
|
method(authoring_result, validator_packet, decision, admission)
|
|
)
|
|
if _accepts_positional_count(method, 3):
|
|
return await _maybe_await(method(authoring_result, decision, admission))
|
|
if _accepts_positional_count(method, 2):
|
|
return await _maybe_await(method(authoring_result, validator_packet))
|
|
return await _maybe_await(method(authoring_result))
|