diff --git a/project/TICKETS.md b/project/TICKETS.md index a9e49ef..2e5ff41 100644 --- a/project/TICKETS.md +++ b/project/TICKETS.md @@ -120,4 +120,5 @@ | **ticket-130** | [`README.md`](./ticket-130/README.md) | - | - | - | - | - | | **ticket-131** | [`README.md`](./ticket-131/README.md) | - | - | - | - | - | | **ticket-132** | [`README.md`](./ticket-132/README.md) | - | - | - | - | - | +| **ticket-133** | [`README.md`](./ticket-133/README.md) | - | - | - | - | - | diff --git a/project/ticket-134/README.md b/project/ticket-134/README.md new file mode 100644 index 0000000..9bbbed1 --- /dev/null +++ b/project/ticket-134/README.md @@ -0,0 +1,21 @@ +# Ticket 134: integrate subactor-search code2logic and reflex into known tools and advise + +- **ID**: ticket-134 +- **Owner**: agent:ant +- **Status**: IN_PROGRESS +- **Workflow state**: EDIT +- **Created**: 2026-11-03 + +## Goal and scope + +1. Add `subactor-search`, `code2logic`, `reflex`, `heyasa`, `willman`, `dockuri`, and `twinerd` to `KNOWN_TOOLS` in `src/monag/ecosystem.py`. +2. Add `src/monag/search_integration.py` to extract code intents from project ASTs (`function.toon`, `map.toon.yaml`, and Python source) via `subactor_search`. +3. Provide Liquid D1 (`d1:3b`) decision reranking for monag advisory recommendations. +4. Expose `search` runner discovery in `src/monag/advise.py`. +5. Add unit tests in `tests/test_ecosystem_tools.py`. + +## Acceptance criteria + +- [x] AC-01: `KNOWN_TOOLS` in `monag.ecosystem` includes `subactor-search`, `code2logic`, `reflex`, `heyasa`. +- [x] AC-02: `monag.search_integration` extracts code intents and supports Liquid D1 reranking with graceful fallback. +- [x] AC-03: `tests/test_ecosystem_tools.py` passes all unit tests. diff --git a/project/ticket-134/intent.json b/project/ticket-134/intent.json new file mode 100644 index 0000000..f9166f6 --- /dev/null +++ b/project/ticket-134/intent.json @@ -0,0 +1,27 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-134", + "summary": "integrate subactor-search code2logic and reflex into known tools and advise", + "workstream": "application", + "classification": { + "kind": "SERVICE", + "priority": "P2", + "origin": "health" + }, + "allowedPaths": [ + "project/ticket-134/**", + "TODO.md", + "project/TICKETS.md", + "src/monag/ecosystem.py", + "src/monag/search_integration.py", + "src/monag/advise.py", + "tests/test_ecosystem_tools.py" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null +} diff --git a/src/monag/advise.py b/src/monag/advise.py index f22c46b..da775aa 100644 --- a/src/monag/advise.py +++ b/src/monag/advise.py @@ -90,6 +90,28 @@ def _find_algocode_runner() -> str | None: return None +def _find_search_runner() -> str | None: + """Check if subactor_search is importable or available on PATH / workspace.""" + try: + import subactor_search # noqa: F401 + return 'import' + except ImportError: + pass + for parent in [Path.cwd(), Path.home() / 'github', Path.home() / 'github' / 'semcod']: + s_dir = parent / 'search' / 'src' + if (s_dir / 'subactor_search').is_dir() and str(s_dir) not in sys.path: + sys.path.insert(0, str(s_dir)) + try: + import subactor_search # noqa: F401 + return 'import' + except ImportError: + pass + import shutil + if shutil.which('subactor-search'): + return 'cli' + return None + + def verify_candidate_conflict(candidate: dict[str, Any], root: Path) -> dict[str, Any] | None: """Run deterministic conflict check using semcod/algocode if available.""" runner = _find_algocode_runner() @@ -684,6 +706,10 @@ def advise(root: Path, depth: int = 2, issue_limit: int = 200, 'available': _find_algocode_runner() is not None, 'runner': _find_algocode_runner(), }, + 'search': { + 'available': _find_search_runner() is not None, + 'runner': _find_search_runner(), + }, } diff --git a/src/monag/ecosystem.py b/src/monag/ecosystem.py index 42137e9..1592866 100644 --- a/src/monag/ecosystem.py +++ b/src/monag/ecosystem.py @@ -52,6 +52,13 @@ "paxlet": {"role": "lightweight task paxlet runner", "aliases": ("paxlet",), "module": "paxlet"}, "uncrash": {"role": "workspace state snapshots, agent session diagnostics, and disaster recovery", "aliases": ("uncrash",), "module": "uncrash"}, "monag": {"role": "agent monitor, triage, doctor, and MCP server", "aliases": ("monag",), "module": "monag"}, + "subactor-search": {"role": "intent & AST code search across repositories with Liquid D1 decision reranker", "aliases": ("subactor-search", "search"), "module": "subactor_search"}, + "code2logic": {"role": "AST symbol and function extractor to function.toon notation", "aliases": ("code2logic",), "module": "code2logic"}, + "reflex": {"role": "autonomous reflection, failure analysis and self-healing proposals", "aliases": ("reflex",), "module": "reflex"}, + "heyasa": {"role": "fleet priority topology, calendar, and strategy execution map", "aliases": ("heyasa",), "module": "heyasa"}, + "willman": {"role": "host governance, ticket allocation, and standards verification", "aliases": ("wellman", "willman"), "module": "wellman"}, + "dockuri": {"role": "deterministic container execution and URI process isolation", "aliases": ("dockuri",), "module": "dockuri"}, + "twinerd": {"role": "desktop observation, multimodal UI automation and verification", "aliases": ("twinerd",), "module": "twinerd"}, } diff --git a/src/monag/search_integration.py b/src/monag/search_integration.py new file mode 100644 index 0000000..63f8df5 --- /dev/null +++ b/src/monag/search_integration.py @@ -0,0 +1,130 @@ +"""Integration bridge between monag and subactor_search / Liquid D1 JEV. + +Provides AST-based code intent extraction (integrating code2llm map.toon.yaml +and code2logic function.toon) and fast Liquid D1 decision reranking for +monag triage and advisory recommendations. +""" + +from __future__ import annotations + +import logging +import sys +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + +SEARCH_CANDIDATE_PATHS = [ + Path.home() / "github" / "semcod" / "search" / "src", + Path(__file__).resolve().parent.parent.parent.parent / "semcod" / "search" / "src", +] + + +def _find_subactor_search() -> Any: + """Discover and import subactor_search from environment or workspace.""" + try: + import subactor_search # type: ignore + return subactor_search + except ImportError: + pass + + for candidate in SEARCH_CANDIDATE_PATHS: + if candidate.is_dir() and (candidate / "subactor_search").is_dir(): + candidate_str = str(candidate) + if candidate_str not in sys.path: + sys.path.insert(0, candidate_str) + try: + import subactor_search # type: ignore + return subactor_search + except ImportError: + continue + return None + + +def discover_code_intents( + root: Path | str, + role: str | None = None, + stage: str | None = None, + max_modules: int = 32, + max_functions: int = 128, +) -> list[dict[str, Any]]: + """Extract code intent records and functions from repository AST or toon files.""" + root_path = Path(root).resolve() + search_mod = _find_subactor_search() + if search_mod is None: + return [] + + try: + from subactor_search.intent_code import ( # type: ignore + extract_project_code_intents, + ) + from subactor_search.intent_adapters import ( # type: ignore + adapt_code_intents_to_records, + ) + + schema_ir = extract_project_code_intents(root_path, max_modules=max_modules, max_functions=max_functions) + if not schema_ir: + return [] + + records = adapt_code_intents_to_records(schema_ir, root_path.name) + if role: + role_tag = f"cqrs.{role.strip().lower()}" + records = [r for r in records if role_tag in r.get("capabilities", [])] + if stage: + stage_lower = stage.strip().lower() + records = [r for r in records if r.get("stage", "").lower() == stage_lower] + return records + except Exception as exc: + logger.debug("Failed to extract code intents via subactor_search: %s", exc) + return [] + + +def rerank_recommendations_with_d1( + query: str, + recommendations: list[dict[str, Any]], + model: str = "d1:3b", + timeout_s: float = 15.0, +) -> list[dict[str, Any]]: + """Rerank recommendations using fast local Liquid D1 (d1:3b) decision routing. + + Gracefully falls back to original ordering if inference is unreachable. + """ + if not recommendations or len(recommendations) <= 1: + return recommendations + + search_mod = _find_subactor_search() + if search_mod is None: + return recommendations + + try: + from subactor_search.decision_adapter import d1_reranker # type: ignore + + candidates = [] + for i, rec in enumerate(recommendations): + cid = str(rec.get("id") or rec.get("ticket") or f"rec-{i}") + desc = rec.get("title") or rec.get("description") or rec.get("summary") or "" + caps = rec.get("tags") or rec.get("capabilities") or [] + candidates.append({ + "id": cid, + "description": desc, + "project": rec.get("repo") or rec.get("project") or "", + "capabilities": caps, + }) + + rankings = d1_reranker(query, candidates, model=model, timeout_s=timeout_s) + score_by_id = {r["id"]: r.get("score", 0.0) for r in rankings} + reason_by_id = {r["id"]: r.get("reason", "") for r in rankings} + + ranked_recs = [] + for i, rec in enumerate(recommendations): + cid = str(rec.get("id") or rec.get("ticket") or f"rec-{i}") + new_rec = dict(rec) + new_rec["_d1_score"] = score_by_id.get(cid, 0.0) + new_rec["_d1_reason"] = reason_by_id.get(cid, "") + ranked_recs.append(new_rec) + + ranked_recs.sort(key=lambda r: float(r.get("_d1_score", 0.0)), reverse=True) + return ranked_recs + except Exception as exc: + logger.debug("Liquid D1 decision reranking unavailable: %s", exc) + return recommendations diff --git a/tests/test_ecosystem_tools.py b/tests/test_ecosystem_tools.py index b58bc08..53c8361 100644 --- a/tests/test_ecosystem_tools.py +++ b/tests/test_ecosystem_tools.py @@ -47,6 +47,10 @@ def test_audit_ecosystem_tools_finds_core_tools(self): self.assertIn("monag", tools) self.assertIn("subllm", tools) self.assertIn("koru", tools) + self.assertIn("subactor-search", tools) + self.assertIn("code2logic", tools) + self.assertIn("reflex", tools) + self.assertIn("heyasa", tools) git_tool = tools["git"] self.assertIsInstance(git_tool, ToolCapability) @@ -134,6 +138,29 @@ def test_cli_tools_invocation(self): code_apis = main(["apis", "--json"]) self.assertEqual(code_apis, 0) + def test_search_integration_and_d1_reranking(self): + from monag import search_integration + from monag.advise import _find_search_runner + + # Test runner discovery + runner = _find_search_runner() + self.assertIn(runner, {"import", "cli", None}) + + # Test fallback when empty recommendations + empty = search_integration.rerank_recommendations_with_d1("query", []) + self.assertEqual(empty, []) + + # Test single candidate passthrough + single = [{"id": "cand-1", "title": "Test candidate"}] + res = search_integration.rerank_recommendations_with_d1("query", single) + self.assertEqual(res, single) + + # Test discover_code_intents on a temporary directory with python files + py_file = self.root / "sample_mod.py" + py_file.write_text("def do_something():\n '''Performs an action.'''\n return 42\n") + intents = search_integration.discover_code_intents(self.root) + self.assertIsInstance(intents, list) + if __name__ == "__main__": unittest.main()