Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions project/TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | - | - | - | - | - |
<!-- AUTO:TICKET_INDEX:END -->
21 changes: 21 additions & 0 deletions project/ticket-134/README.md
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 27 additions & 0 deletions project/ticket-134/intent.json
Original file line number Diff line number Diff line change
@@ -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
}
26 changes: 26 additions & 0 deletions src/monag/advise.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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(),
},
}


Expand Down
7 changes: 7 additions & 0 deletions src/monag/ecosystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
}


Expand Down
130 changes: 130 additions & 0 deletions src/monag/search_integration.py
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions tests/test_ecosystem_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Loading