nexo-brain 7.9.13 → 7.9.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cognitive/_core.py +6 -4
- package/src/db/_protocol.py +10 -1
- package/src/db/_schema.py +9 -0
- package/src/doctor/providers/runtime.py +22 -4
- package/src/local_model_manifest.json +113 -0
- package/src/local_models.py +247 -0
- package/src/migrate_embeddings.py +6 -6
- package/src/model_warmup.py +20 -23
- package/src/paths.py +9 -0
- package/src/plugins/cortex.py +267 -34
- package/src/plugins/protocol.py +125 -47
- package/src/resonance_map.py +2 -0
- package/src/scripts/nexo-daily-self-audit.py +44 -0
- package/src/scripts/nexo-learning-housekeep.py +2 -2
- package/templates/core-prompts/cortex-decision-critic.md +24 -0
package/src/plugins/cortex.py
CHANGED
|
@@ -23,6 +23,7 @@ from datetime import datetime, timedelta
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
|
|
25
25
|
from db import VALID_IMPACT_LEVELS, VALID_TASK_TYPES, validate_impact_level, validate_task_type
|
|
26
|
+
from db._semantic_similarity import hybrid_similarity_score
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def _get_db():
|
|
@@ -89,6 +90,10 @@ STOP_WORDS = {
|
|
|
89
90
|
}
|
|
90
91
|
HISTORICAL_OUTCOME_MIN_RESOLVED = 2
|
|
91
92
|
HISTORICAL_OUTCOME_LOOKBACK = 12
|
|
93
|
+
SEMANTIC_HISTORY_LOOKBACK = 24
|
|
94
|
+
SEMANTIC_HISTORY_MATCH_THRESHOLD = 0.58
|
|
95
|
+
CRITIQUE_TOP_CANDIDATES = 3
|
|
96
|
+
CRITIQUE_MAX_MARGIN = 0.45
|
|
92
97
|
|
|
93
98
|
|
|
94
99
|
def _term_hits(text: str, terms: set[str]) -> int:
|
|
@@ -279,50 +284,80 @@ def _constraint_penalty(text: str, constraints: list[str]) -> tuple[float, list[
|
|
|
279
284
|
|
|
280
285
|
def _history_signal(text: str, *, area: str = "", goal: str = "") -> dict:
|
|
281
286
|
conn = _get_db()
|
|
282
|
-
|
|
283
|
-
if not
|
|
287
|
+
query_text = " ".join(part for part in [text, area, goal] if part).strip()
|
|
288
|
+
if not query_text:
|
|
284
289
|
return {"positive": 0.0, "negative": 0.0, "matched_decisions": 0, "matched_outcomes": 0}
|
|
285
290
|
|
|
286
|
-
|
|
287
|
-
|
|
291
|
+
def _keyword_extractor(value: str) -> list[str]:
|
|
292
|
+
return _tokenize(value, limit=8)
|
|
293
|
+
|
|
294
|
+
decision_positive = 0.0
|
|
295
|
+
decision_negative = 0.0
|
|
288
296
|
matched_decisions = 0
|
|
289
|
-
|
|
297
|
+
if conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='decisions'").fetchone():
|
|
290
298
|
rows = conn.execute(
|
|
291
|
-
"""SELECT
|
|
292
|
-
|
|
293
|
-
ORDER BY created_at DESC LIMIT
|
|
294
|
-
|
|
299
|
+
"""SELECT decision, alternatives, based_on, outcome
|
|
300
|
+
FROM decisions
|
|
301
|
+
ORDER BY created_at DESC LIMIT ?""",
|
|
302
|
+
(SEMANTIC_HISTORY_LOOKBACK,),
|
|
295
303
|
).fetchall()
|
|
296
304
|
for row in rows:
|
|
305
|
+
candidate_text = " ".join(
|
|
306
|
+
str(row[key] or "")
|
|
307
|
+
for key in ("decision", "alternatives", "based_on")
|
|
308
|
+
).strip()
|
|
309
|
+
similarity = hybrid_similarity_score(
|
|
310
|
+
query_text,
|
|
311
|
+
candidate_text,
|
|
312
|
+
keyword_extractor=_keyword_extractor,
|
|
313
|
+
strong_semantic_threshold=0.82,
|
|
314
|
+
moderate_semantic_threshold=0.74,
|
|
315
|
+
moderate_keyword_floor=0.12,
|
|
316
|
+
)
|
|
317
|
+
if similarity < SEMANTIC_HISTORY_MATCH_THRESHOLD:
|
|
318
|
+
continue
|
|
297
319
|
matched_decisions += 1
|
|
298
320
|
outcome = (row["outcome"] or "").lower()
|
|
299
321
|
if _contains_any(outcome, NEGATIVE_OUTCOME_TERMS):
|
|
300
|
-
decision_negative += 1
|
|
322
|
+
decision_negative += min(1.0, similarity)
|
|
301
323
|
elif _contains_any(outcome, POSITIVE_OUTCOME_TERMS):
|
|
302
|
-
decision_positive += 1
|
|
324
|
+
decision_positive += min(1.0, similarity)
|
|
303
325
|
|
|
304
|
-
outcome_positive = 0
|
|
305
|
-
outcome_negative = 0
|
|
326
|
+
outcome_positive = 0.0
|
|
327
|
+
outcome_negative = 0.0
|
|
306
328
|
matched_outcomes = 0
|
|
307
329
|
if conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='outcomes'").fetchone():
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
330
|
+
rows = conn.execute(
|
|
331
|
+
"""SELECT description, expected_result, action_type, status
|
|
332
|
+
FROM outcomes
|
|
333
|
+
ORDER BY created_at DESC LIMIT ?""",
|
|
334
|
+
(SEMANTIC_HISTORY_LOOKBACK,),
|
|
335
|
+
).fetchall()
|
|
336
|
+
for row in rows:
|
|
337
|
+
candidate_text = " ".join(
|
|
338
|
+
str(row[key] or "")
|
|
339
|
+
for key in ("description", "expected_result", "action_type")
|
|
340
|
+
).strip()
|
|
341
|
+
similarity = hybrid_similarity_score(
|
|
342
|
+
query_text,
|
|
343
|
+
candidate_text,
|
|
344
|
+
keyword_extractor=_keyword_extractor,
|
|
345
|
+
strong_semantic_threshold=0.82,
|
|
346
|
+
moderate_semantic_threshold=0.74,
|
|
347
|
+
moderate_keyword_floor=0.12,
|
|
348
|
+
)
|
|
349
|
+
if similarity < SEMANTIC_HISTORY_MATCH_THRESHOLD:
|
|
350
|
+
continue
|
|
351
|
+
matched_outcomes += 1
|
|
352
|
+
status = (row["status"] or "").lower()
|
|
353
|
+
if status == "met":
|
|
354
|
+
outcome_positive += min(1.0, similarity)
|
|
355
|
+
elif status in {"missed", "expired"}:
|
|
356
|
+
outcome_negative += min(1.0, similarity)
|
|
322
357
|
|
|
323
358
|
return {
|
|
324
|
-
"positive": min(2.5, (decision_positive * 0.
|
|
325
|
-
"negative": min(3.0, (decision_negative *
|
|
359
|
+
"positive": round(min(2.5, (decision_positive * 0.9) + (outcome_positive * 1.0)), 2),
|
|
360
|
+
"negative": round(min(3.0, (decision_negative * 1.1) + (outcome_negative * 1.2)), 2),
|
|
326
361
|
"matched_decisions": matched_decisions,
|
|
327
362
|
"matched_outcomes": matched_outcomes,
|
|
328
363
|
}
|
|
@@ -702,6 +737,172 @@ def _format_decision_summary(recommended: dict, alternatives_scored: list[dict])
|
|
|
702
737
|
return f"Recomendada por el mejor balance entre impacto, éxito, riesgo y huella somática; {notes}."
|
|
703
738
|
|
|
704
739
|
|
|
740
|
+
def _parse_json_object_response(raw: str) -> dict:
|
|
741
|
+
text = (raw or "").strip()
|
|
742
|
+
if not text:
|
|
743
|
+
return {}
|
|
744
|
+
try:
|
|
745
|
+
parsed = json.loads(text)
|
|
746
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
747
|
+
except json.JSONDecodeError:
|
|
748
|
+
match = re.search(r"\{.*\}", text, re.DOTALL)
|
|
749
|
+
if not match:
|
|
750
|
+
return {}
|
|
751
|
+
try:
|
|
752
|
+
parsed = json.loads(match.group(0))
|
|
753
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
754
|
+
except json.JSONDecodeError:
|
|
755
|
+
return {}
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _critique_tier(
|
|
759
|
+
*,
|
|
760
|
+
impact_level: str,
|
|
761
|
+
scored: list[dict],
|
|
762
|
+
constraints: list[str],
|
|
763
|
+
evidence_refs: list[str],
|
|
764
|
+
) -> str:
|
|
765
|
+
if impact_level != "critical":
|
|
766
|
+
return "alto"
|
|
767
|
+
gap = 99.0
|
|
768
|
+
if len(scored) > 1:
|
|
769
|
+
gap = scored[0]["total_score"] - scored[1]["total_score"]
|
|
770
|
+
if gap <= CRITIQUE_MAX_MARGIN or len(constraints) >= 3 or len(evidence_refs) <= 1:
|
|
771
|
+
return "maximo"
|
|
772
|
+
return "alto"
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _run_llm_critique(
|
|
776
|
+
*,
|
|
777
|
+
goal: str,
|
|
778
|
+
task_type: str,
|
|
779
|
+
impact_level: str,
|
|
780
|
+
area: str,
|
|
781
|
+
context_hint: str,
|
|
782
|
+
constraints: list[str],
|
|
783
|
+
evidence_refs: list[str],
|
|
784
|
+
goal_profile: dict,
|
|
785
|
+
scored: list[dict],
|
|
786
|
+
) -> dict:
|
|
787
|
+
if impact_level not in {"high", "critical"} or len(scored) < 2:
|
|
788
|
+
return {"active": False}
|
|
789
|
+
|
|
790
|
+
try:
|
|
791
|
+
from call_model_raw import call_model_raw, ClassifierUnavailableError
|
|
792
|
+
from core_prompts import render_core_prompt
|
|
793
|
+
from operator_language import append_operator_language_contract
|
|
794
|
+
except Exception as exc:
|
|
795
|
+
return {"active": True, "ok": False, "error": f"critic_unavailable:{exc}"}
|
|
796
|
+
|
|
797
|
+
tier = _critique_tier(
|
|
798
|
+
impact_level=impact_level,
|
|
799
|
+
scored=scored,
|
|
800
|
+
constraints=constraints,
|
|
801
|
+
evidence_refs=evidence_refs,
|
|
802
|
+
)
|
|
803
|
+
payload = {
|
|
804
|
+
"goal": goal,
|
|
805
|
+
"task_type": task_type,
|
|
806
|
+
"impact_level": impact_level,
|
|
807
|
+
"area": area,
|
|
808
|
+
"context_hint": context_hint,
|
|
809
|
+
"constraints": constraints,
|
|
810
|
+
"evidence_refs": evidence_refs,
|
|
811
|
+
"goal_profile": {
|
|
812
|
+
"profile_id": goal_profile.get("profile_id", ""),
|
|
813
|
+
"profile_name": goal_profile.get("profile_name", ""),
|
|
814
|
+
"goal_labels": goal_profile.get("goal_labels", []),
|
|
815
|
+
"weights": goal_profile.get("weights", {}),
|
|
816
|
+
},
|
|
817
|
+
"heuristic_recommendation": scored[0]["name"],
|
|
818
|
+
"candidates": [
|
|
819
|
+
{
|
|
820
|
+
"name": item["name"],
|
|
821
|
+
"impact": item["impact"],
|
|
822
|
+
"success_probability": item["success_probability"],
|
|
823
|
+
"risk_level": item["risk_level"],
|
|
824
|
+
"somatic_penalty": item["somatic_penalty"],
|
|
825
|
+
"total_score": item["total_score"],
|
|
826
|
+
"notes": item.get("notes") or [],
|
|
827
|
+
"historical_signal": item.get("historical_signal") or {},
|
|
828
|
+
"pattern_learning_signal": item.get("pattern_learning_signal") or {},
|
|
829
|
+
}
|
|
830
|
+
for item in scored[:CRITIQUE_TOP_CANDIDATES]
|
|
831
|
+
],
|
|
832
|
+
}
|
|
833
|
+
prompt = render_core_prompt(
|
|
834
|
+
"cortex-decision-critic",
|
|
835
|
+
payload_json=json.dumps(payload, ensure_ascii=False, indent=2),
|
|
836
|
+
)
|
|
837
|
+
prompt = append_operator_language_contract(prompt)
|
|
838
|
+
try:
|
|
839
|
+
raw = call_model_raw(
|
|
840
|
+
prompt,
|
|
841
|
+
caller="cortex_decision_critic",
|
|
842
|
+
tier=tier,
|
|
843
|
+
system=render_core_prompt("json-object-only"),
|
|
844
|
+
max_tokens=500,
|
|
845
|
+
temperature=0.0,
|
|
846
|
+
stop_sequences=[],
|
|
847
|
+
timeout=20.0,
|
|
848
|
+
)
|
|
849
|
+
except ClassifierUnavailableError as exc:
|
|
850
|
+
return {"active": True, "ok": False, "tier": tier, "error": str(exc)}
|
|
851
|
+
|
|
852
|
+
parsed = _parse_json_object_response(raw)
|
|
853
|
+
candidate_names = [item["name"] for item in scored]
|
|
854
|
+
recommended_choice = str(parsed.get("recommended_choice") or "").strip()
|
|
855
|
+
if recommended_choice not in candidate_names:
|
|
856
|
+
return {
|
|
857
|
+
"active": True,
|
|
858
|
+
"ok": False,
|
|
859
|
+
"tier": tier,
|
|
860
|
+
"error": "invalid_recommended_choice",
|
|
861
|
+
"raw_response": raw[:1200],
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
ranking = parsed.get("confirmed_ranking")
|
|
865
|
+
clean_ranking: list[str] = []
|
|
866
|
+
if isinstance(ranking, list):
|
|
867
|
+
for item in ranking:
|
|
868
|
+
name = str(item or "").strip()
|
|
869
|
+
if name in candidate_names and name not in clean_ranking:
|
|
870
|
+
clean_ranking.append(name)
|
|
871
|
+
for name in candidate_names:
|
|
872
|
+
if name not in clean_ranking:
|
|
873
|
+
clean_ranking.append(name)
|
|
874
|
+
|
|
875
|
+
try:
|
|
876
|
+
confidence = float(parsed.get("confidence"))
|
|
877
|
+
except (TypeError, ValueError):
|
|
878
|
+
confidence = 0.0
|
|
879
|
+
confidence = max(0.0, min(1.0, confidence))
|
|
880
|
+
risk_flags = parsed.get("risk_flags")
|
|
881
|
+
if not isinstance(risk_flags, list):
|
|
882
|
+
risk_flags = []
|
|
883
|
+
reasoning_summary = str(parsed.get("reasoning_summary") or "").strip()
|
|
884
|
+
disagreement = bool(parsed.get("disagreement_with_heuristic"))
|
|
885
|
+
return {
|
|
886
|
+
"active": True,
|
|
887
|
+
"ok": True,
|
|
888
|
+
"tier": tier,
|
|
889
|
+
"recommended_choice": recommended_choice,
|
|
890
|
+
"confirmed_ranking": clean_ranking,
|
|
891
|
+
"confidence": round(confidence, 3),
|
|
892
|
+
"risk_flags": [str(item).strip() for item in risk_flags if str(item).strip()][:5],
|
|
893
|
+
"reasoning_summary": reasoning_summary,
|
|
894
|
+
"disagreement_with_heuristic": disagreement or (recommended_choice != scored[0]["name"]),
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def _reorder_scores_by_names(scored: list[dict], ranking: list[str]) -> list[dict]:
|
|
899
|
+
order = {name: idx for idx, name in enumerate(ranking)}
|
|
900
|
+
return sorted(
|
|
901
|
+
scored,
|
|
902
|
+
key=lambda item: (order.get(item["name"], len(order)), -item["total_score"]),
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
|
|
705
906
|
def handle_cortex_check(
|
|
706
907
|
goal: str,
|
|
707
908
|
task_type: str = "answer",
|
|
@@ -858,6 +1059,7 @@ def handle_cortex_decide(
|
|
|
858
1059
|
linked_outcome_id: int = 0,
|
|
859
1060
|
goal_profile_id: str = "",
|
|
860
1061
|
goal_id: str = "",
|
|
1062
|
+
auto_create_outcome: bool = False,
|
|
861
1063
|
) -> str:
|
|
862
1064
|
"""Evaluate concrete alternatives for a high-impact task using the existing Cortex."""
|
|
863
1065
|
clean_goal = (goal or "").strip()
|
|
@@ -927,16 +1129,39 @@ def handle_cortex_decide(
|
|
|
927
1129
|
for item in parsed_alternatives
|
|
928
1130
|
]
|
|
929
1131
|
scored.sort(key=lambda item: item["total_score"], reverse=True)
|
|
930
|
-
|
|
931
|
-
|
|
1132
|
+
heuristic_recommended = scored[0]
|
|
1133
|
+
heuristic_reasoning = _format_decision_summary(heuristic_recommended, scored)
|
|
1134
|
+
critique = _run_llm_critique(
|
|
1135
|
+
goal=clean_goal,
|
|
1136
|
+
task_type=clean_type,
|
|
1137
|
+
impact_level=clean_level,
|
|
1138
|
+
area=area.strip(),
|
|
1139
|
+
context_hint=context_hint.strip(),
|
|
1140
|
+
constraints=parsed_constraints,
|
|
1141
|
+
evidence_refs=parsed_evidence,
|
|
1142
|
+
goal_profile=resolved_goal_profile,
|
|
1143
|
+
scored=scored,
|
|
1144
|
+
)
|
|
1145
|
+
decision_mode = "heuristic"
|
|
1146
|
+
if critique.get("ok"):
|
|
1147
|
+
scored = _reorder_scores_by_names(scored, critique.get("confirmed_ranking") or [])
|
|
1148
|
+
recommended = next(
|
|
1149
|
+
(item for item in scored if item["name"] == critique["recommended_choice"]),
|
|
1150
|
+
heuristic_recommended,
|
|
1151
|
+
)
|
|
1152
|
+
reasoning = (critique.get("reasoning_summary") or "").strip() or heuristic_reasoning
|
|
1153
|
+
decision_mode = "heuristic_plus_llm"
|
|
1154
|
+
else:
|
|
1155
|
+
recommended = heuristic_recommended
|
|
1156
|
+
reasoning = heuristic_reasoning
|
|
932
1157
|
resolved_outcome_id = _resolve_linked_outcome_id(
|
|
933
1158
|
linked_outcome_id=linked_outcome_id,
|
|
934
1159
|
task_id=task_id,
|
|
935
1160
|
)
|
|
936
1161
|
|
|
937
|
-
#
|
|
938
|
-
#
|
|
939
|
-
if resolved_outcome_id is None and clean_goal and task_id:
|
|
1162
|
+
# Outcome auto-creation is opt-in so analytics can distinguish
|
|
1163
|
+
# persisted decisions from explicitly tracked outcomes.
|
|
1164
|
+
if auto_create_outcome and resolved_outcome_id is None and clean_goal and task_id:
|
|
940
1165
|
try:
|
|
941
1166
|
from db import create_outcome
|
|
942
1167
|
|
|
@@ -974,6 +1199,10 @@ def handle_cortex_decide(
|
|
|
974
1199
|
goal_profile_id=resolved_goal_profile.get("profile_id", ""),
|
|
975
1200
|
goal_profile_labels=resolved_goal_profile.get("goal_labels", []),
|
|
976
1201
|
goal_profile_weights=resolved_goal_profile.get("weights", {}),
|
|
1202
|
+
heuristic_choice=heuristic_recommended["name"],
|
|
1203
|
+
heuristic_reasoning=heuristic_reasoning,
|
|
1204
|
+
critique_payload=critique,
|
|
1205
|
+
decision_mode=decision_mode,
|
|
977
1206
|
selected_choice=recommended["name"],
|
|
978
1207
|
selection_reason=reasoning,
|
|
979
1208
|
selection_source="recommended",
|
|
@@ -997,6 +1226,10 @@ def handle_cortex_decide(
|
|
|
997
1226
|
"impact_level": clean_level,
|
|
998
1227
|
"recommendation": recommended["name"],
|
|
999
1228
|
"reasoning": reasoning,
|
|
1229
|
+
"heuristic_recommendation": heuristic_recommended["name"],
|
|
1230
|
+
"heuristic_reasoning": heuristic_reasoning,
|
|
1231
|
+
"decision_mode": decision_mode,
|
|
1232
|
+
"critique": critique,
|
|
1000
1233
|
"selected_choice": record.get("selected_choice"),
|
|
1001
1234
|
"selection_source": record.get("selection_source"),
|
|
1002
1235
|
"linked_outcome_id": record.get("linked_outcome_id"),
|
package/src/plugins/protocol.py
CHANGED
|
@@ -898,6 +898,46 @@ def _auto_capture_learning(task: dict, task_id: str, effective_files: list[str],
|
|
|
898
898
|
)
|
|
899
899
|
|
|
900
900
|
|
|
901
|
+
def _append_debt_ref(debts: list[dict], debt: dict, *, debt_type: str, severity: str):
|
|
902
|
+
debt_id = debt.get("id")
|
|
903
|
+
if debt_id and any(item.get("id") == debt_id for item in debts):
|
|
904
|
+
return
|
|
905
|
+
debts.append(
|
|
906
|
+
{
|
|
907
|
+
"id": debt_id,
|
|
908
|
+
"debt_type": debt_type,
|
|
909
|
+
"severity": severity,
|
|
910
|
+
}
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def _ensure_open_debt(
|
|
915
|
+
session_id: str,
|
|
916
|
+
task_id: str,
|
|
917
|
+
debt_type: str,
|
|
918
|
+
*,
|
|
919
|
+
severity: str,
|
|
920
|
+
evidence: str,
|
|
921
|
+
debts: list[dict],
|
|
922
|
+
) -> dict:
|
|
923
|
+
existing = list_protocol_debts(
|
|
924
|
+
status="open",
|
|
925
|
+
task_id=task_id,
|
|
926
|
+
session_id="" if task_id else session_id,
|
|
927
|
+
debt_type=debt_type,
|
|
928
|
+
limit=1,
|
|
929
|
+
)
|
|
930
|
+
debt = existing[0] if existing else create_protocol_debt(
|
|
931
|
+
session_id,
|
|
932
|
+
debt_type,
|
|
933
|
+
severity=severity,
|
|
934
|
+
task_id=task_id,
|
|
935
|
+
evidence=evidence,
|
|
936
|
+
)
|
|
937
|
+
_append_debt_ref(debts, debt, debt_type=debt_type, severity=severity)
|
|
938
|
+
return debt
|
|
939
|
+
|
|
940
|
+
|
|
901
941
|
def _record_debt(session_id: str, task_id: str, debt_type: str, *, severity: str, evidence: str, debts: list[dict]):
|
|
902
942
|
debt = create_protocol_debt(
|
|
903
943
|
session_id,
|
|
@@ -906,13 +946,7 @@ def _record_debt(session_id: str, task_id: str, debt_type: str, *, severity: str
|
|
|
906
946
|
task_id=task_id,
|
|
907
947
|
evidence=evidence,
|
|
908
948
|
)
|
|
909
|
-
debts
|
|
910
|
-
{
|
|
911
|
-
"id": debt.get("id"),
|
|
912
|
-
"debt_type": debt_type,
|
|
913
|
-
"severity": severity,
|
|
914
|
-
}
|
|
915
|
-
)
|
|
949
|
+
_append_debt_ref(debts, debt, debt_type=debt_type, severity=severity)
|
|
916
950
|
|
|
917
951
|
|
|
918
952
|
def handle_confidence_check(
|
|
@@ -1336,10 +1370,10 @@ def handle_task_close(
|
|
|
1336
1370
|
high_stakes=bool(task.get("response_high_stakes")),
|
|
1337
1371
|
)
|
|
1338
1372
|
|
|
1339
|
-
# ── Evidence enforcement: reject 'done' without proof
|
|
1340
|
-
#
|
|
1341
|
-
#
|
|
1342
|
-
#
|
|
1373
|
+
# ── Evidence enforcement: reject 'done' without proof ──
|
|
1374
|
+
# G1 hardening: "done" is no longer allowed to degrade into a debt-only
|
|
1375
|
+
# close when verify evidence is missing. Keep the task open, open/dedupe
|
|
1376
|
+
# the debt, and force the caller to provide real proof before closing.
|
|
1343
1377
|
if task.get("must_verify") and clean_outcome == "done":
|
|
1344
1378
|
is_trivial, trivial_reason = _is_trivial_evidence(clean_evidence)
|
|
1345
1379
|
if not is_trivial:
|
|
@@ -1349,39 +1383,7 @@ def handle_task_close(
|
|
|
1349
1383
|
resolution="Verification evidence supplied during task_close",
|
|
1350
1384
|
)
|
|
1351
1385
|
else:
|
|
1352
|
-
|
|
1353
|
-
if protocol_strictness == "strict":
|
|
1354
|
-
if trivial_reason == "empty":
|
|
1355
|
-
err = "Cannot close task as 'done' without evidence."
|
|
1356
|
-
hint = (
|
|
1357
|
-
"Provide the `evidence` parameter with verifiable proof: "
|
|
1358
|
-
"test output, curl response, screenshot path, or real "
|
|
1359
|
-
"command output."
|
|
1360
|
-
)
|
|
1361
|
-
else:
|
|
1362
|
-
err = (
|
|
1363
|
-
"Cannot close task as 'done' with trivial evidence "
|
|
1364
|
-
f"({trivial_reason})."
|
|
1365
|
-
)
|
|
1366
|
-
hint = (
|
|
1367
|
-
f"Evidence must be substantive: >= {R03_MIN_EVIDENCE_CHARS} "
|
|
1368
|
-
"characters AND not a single filler word. Attach real "
|
|
1369
|
-
"proof — test output excerpt, curl response, DB row, "
|
|
1370
|
-
"screenshot path, or command stdout."
|
|
1371
|
-
)
|
|
1372
|
-
return json.dumps(
|
|
1373
|
-
{
|
|
1374
|
-
"ok": False,
|
|
1375
|
-
"error": err,
|
|
1376
|
-
"hint": hint,
|
|
1377
|
-
"task_id": task_id,
|
|
1378
|
-
"protocol_strictness": protocol_strictness,
|
|
1379
|
-
"evidence_quality_reason": trivial_reason,
|
|
1380
|
-
},
|
|
1381
|
-
ensure_ascii=False,
|
|
1382
|
-
indent=2,
|
|
1383
|
-
)
|
|
1384
|
-
_record_debt(
|
|
1386
|
+
debt = _ensure_open_debt(
|
|
1385
1387
|
task["session_id"],
|
|
1386
1388
|
task_id,
|
|
1387
1389
|
"claimed_done_without_evidence",
|
|
@@ -1393,6 +1395,39 @@ def handle_task_close(
|
|
|
1393
1395
|
),
|
|
1394
1396
|
debts=debts_created,
|
|
1395
1397
|
)
|
|
1398
|
+
if trivial_reason == "empty":
|
|
1399
|
+
err = "Cannot close task as 'done' without evidence."
|
|
1400
|
+
hint = (
|
|
1401
|
+
"Provide the `evidence` parameter with verifiable proof: "
|
|
1402
|
+
"test output, curl response, screenshot path, or real "
|
|
1403
|
+
"command output."
|
|
1404
|
+
)
|
|
1405
|
+
else:
|
|
1406
|
+
err = (
|
|
1407
|
+
"Cannot close task as 'done' with trivial evidence "
|
|
1408
|
+
f"({trivial_reason})."
|
|
1409
|
+
)
|
|
1410
|
+
hint = (
|
|
1411
|
+
f"Evidence must be substantive: >= {R03_MIN_EVIDENCE_CHARS} "
|
|
1412
|
+
"characters AND not a single filler word. Attach real "
|
|
1413
|
+
"proof — test output excerpt, curl response, DB row, "
|
|
1414
|
+
"screenshot path, or command stdout."
|
|
1415
|
+
)
|
|
1416
|
+
return json.dumps(
|
|
1417
|
+
{
|
|
1418
|
+
"ok": False,
|
|
1419
|
+
"error": err,
|
|
1420
|
+
"hint": hint,
|
|
1421
|
+
"task_id": task_id,
|
|
1422
|
+
"blocked_by": "g1_verify",
|
|
1423
|
+
"debt_id": debt.get("id"),
|
|
1424
|
+
"debt_type": "claimed_done_without_evidence",
|
|
1425
|
+
"evidence_quality_reason": trivial_reason,
|
|
1426
|
+
"protocol_strictness": get_protocol_strictness(),
|
|
1427
|
+
},
|
|
1428
|
+
ensure_ascii=False,
|
|
1429
|
+
indent=2,
|
|
1430
|
+
)
|
|
1396
1431
|
|
|
1397
1432
|
# ── Release checklist: require channel alignment evidence for release tasks ──
|
|
1398
1433
|
is_release = _is_release_task(
|
|
@@ -1430,7 +1465,7 @@ def handle_task_close(
|
|
|
1430
1465
|
(clean_change_verify or clean_evidence)[:500],
|
|
1431
1466
|
)
|
|
1432
1467
|
if "error" in change:
|
|
1433
|
-
|
|
1468
|
+
debt = _ensure_open_debt(
|
|
1434
1469
|
task["session_id"],
|
|
1435
1470
|
task_id,
|
|
1436
1471
|
"missing_change_log",
|
|
@@ -1438,6 +1473,21 @@ def handle_task_close(
|
|
|
1438
1473
|
evidence=f"change_log failed: {change['error']}",
|
|
1439
1474
|
debts=debts_created,
|
|
1440
1475
|
)
|
|
1476
|
+
if clean_outcome == "done":
|
|
1477
|
+
return json.dumps(
|
|
1478
|
+
{
|
|
1479
|
+
"ok": False,
|
|
1480
|
+
"error": "Cannot close task as 'done' because change_log creation failed.",
|
|
1481
|
+
"hint": "Capture the changed files and create the change log successfully before closing as done.",
|
|
1482
|
+
"task_id": task_id,
|
|
1483
|
+
"blocked_by": "g1_change_log",
|
|
1484
|
+
"debt_id": debt.get("id"),
|
|
1485
|
+
"debt_type": "missing_change_log",
|
|
1486
|
+
"change_log_error": change.get("error"),
|
|
1487
|
+
},
|
|
1488
|
+
ensure_ascii=False,
|
|
1489
|
+
indent=2,
|
|
1490
|
+
)
|
|
1441
1491
|
else:
|
|
1442
1492
|
change_log_id = change.get("id")
|
|
1443
1493
|
resolve_protocol_debts(
|
|
@@ -1446,7 +1496,7 @@ def handle_task_close(
|
|
|
1446
1496
|
resolution="Change log created by nexo_task_close",
|
|
1447
1497
|
)
|
|
1448
1498
|
else:
|
|
1449
|
-
|
|
1499
|
+
debt = _ensure_open_debt(
|
|
1450
1500
|
task["session_id"],
|
|
1451
1501
|
task_id,
|
|
1452
1502
|
"missing_change_log",
|
|
@@ -1454,6 +1504,20 @@ def handle_task_close(
|
|
|
1454
1504
|
evidence="Task required change_log but no changed files were supplied or recorded.",
|
|
1455
1505
|
debts=debts_created,
|
|
1456
1506
|
)
|
|
1507
|
+
if clean_outcome == "done":
|
|
1508
|
+
return json.dumps(
|
|
1509
|
+
{
|
|
1510
|
+
"ok": False,
|
|
1511
|
+
"error": "Cannot close task as 'done' without changed files for the required change_log.",
|
|
1512
|
+
"hint": "Pass `files_changed` (or open the task with files) so nexo_task_close can persist the change log before closing as done.",
|
|
1513
|
+
"task_id": task_id,
|
|
1514
|
+
"blocked_by": "g1_change_log",
|
|
1515
|
+
"debt_id": debt.get("id"),
|
|
1516
|
+
"debt_type": "missing_change_log",
|
|
1517
|
+
},
|
|
1518
|
+
ensure_ascii=False,
|
|
1519
|
+
indent=2,
|
|
1520
|
+
)
|
|
1457
1521
|
|
|
1458
1522
|
if correction:
|
|
1459
1523
|
if (learning_title or "").strip() and (learning_content or "").strip():
|
|
@@ -1564,7 +1628,7 @@ def handle_task_close(
|
|
|
1564
1628
|
resolution="High-stakes action task has a persisted Cortex evaluation.",
|
|
1565
1629
|
)
|
|
1566
1630
|
else:
|
|
1567
|
-
|
|
1631
|
+
debt = _ensure_open_debt(
|
|
1568
1632
|
task["session_id"],
|
|
1569
1633
|
task_id,
|
|
1570
1634
|
"missing_cortex_evaluation",
|
|
@@ -1572,6 +1636,20 @@ def handle_task_close(
|
|
|
1572
1636
|
evidence="High-stakes action task closed without nexo_cortex_decide / persisted evaluation.",
|
|
1573
1637
|
debts=debts_created,
|
|
1574
1638
|
)
|
|
1639
|
+
if clean_outcome == "done":
|
|
1640
|
+
return json.dumps(
|
|
1641
|
+
{
|
|
1642
|
+
"ok": False,
|
|
1643
|
+
"error": "Cannot close high-stakes action task as 'done' without a persisted cortex evaluation.",
|
|
1644
|
+
"hint": "Run `nexo_cortex_decide(...)` for this task and then close it again with the final evidence.",
|
|
1645
|
+
"task_id": task_id,
|
|
1646
|
+
"blocked_by": "g1_cortex",
|
|
1647
|
+
"debt_id": debt.get("id"),
|
|
1648
|
+
"debt_type": "missing_cortex_evaluation",
|
|
1649
|
+
},
|
|
1650
|
+
ensure_ascii=False,
|
|
1651
|
+
indent=2,
|
|
1652
|
+
)
|
|
1575
1653
|
|
|
1576
1654
|
if task.get("guard_has_blocking") and not files_changed_list:
|
|
1577
1655
|
open_task_debts = list_protocol_debts(status="open", task_id=task_id, limit=200)
|
package/src/resonance_map.py
CHANGED
|
@@ -219,6 +219,8 @@ SYSTEM_OWNED_CALLERS: dict[str, str] = {
|
|
|
219
219
|
"learning_validator": "medio",
|
|
220
220
|
"outcome_checker": "medio",
|
|
221
221
|
"check_context": "medio",
|
|
222
|
+
"semantic_reasoner": "muy_bajo",
|
|
223
|
+
"cortex_decision_critic": "alto",
|
|
222
224
|
|
|
223
225
|
# ---- Agent orchestration ----------------------------------------------
|
|
224
226
|
"agent_run/generic": "alto",
|