Skip to content
Draft
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
30 changes: 30 additions & 0 deletions .github/workflows/framework-adapters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Framework Adapter Validation

on:
push:
paths:
- "adapters/**"
- "tests/test_framework_adapters.py"
- "adapters/requirements-frameworks.txt"
- ".github/workflows/framework-adapters.yml"
pull_request:
paths:
- "adapters/**"
- "tests/test_framework_adapters.py"
- "adapters/requirements-frameworks.txt"
- ".github/workflows/framework-adapters.yml"

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install optional framework dependencies
run: python -m pip install -r adapters/requirements-frameworks.txt
- name: Compile adapter code
run: python -m compileall -q adapters tests/test_framework_adapters.py
- name: Run adapter tests
run: python -m pytest tests/test_framework_adapters.py -q
51 changes: 51 additions & 0 deletions adapters/code_test_heal_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Code -> test -> heal workflow using HyperCode's orchestrator."""

from __future__ import annotations

from typing import Any, Dict, List

from adapters.crew_orchestrator_client import CrewOrchestratorClient


WORKFLOW: List[Dict[str, Any]] = [
{
"slug": "coder-agent",
"task_type": "code_generation",
"description": "Implement the requested change and return a patch plus test notes.",
},
{
"slug": "qa-engineer",
"task_type": "quality_assurance",
"description": "Run the relevant tests against the proposed change and report failures.",
},
{
"slug": "healer-agent",
"task_type": "failure_recovery",
"description": "Diagnose test or service failures and propose a safe repair.",
},
]


async def run_code_test_heal(mission: str, client: CrewOrchestratorClient | None = None) -> Dict[str, Any]:
"""Run the staged workflow through crew-orchestrator.

The implementation deliberately keeps each stage explicit so a future
LangGraph graph or CrewAI Crew can call the same stages.
"""

runner = client or CrewOrchestratorClient()
state: Dict[str, Any] = {
"workflow_id": "hyperagent-code-test-heal",
"input": mission,
"requires_approval": True,
"outputs": [],
}

for stage in WORKFLOW:
spec = {"slug": stage["slug"], "role": stage["task_type"], "description": stage["description"]}
stage_state = {**state, "task_type": stage["task_type"], "description": stage["description"]}
result = await runner.call_agent(spec, stage_state)
state["outputs"].append({"stage": stage["slug"], "result": result})
state["last_result"] = result

return state
61 changes: 61 additions & 0 deletions adapters/crew_orchestrator_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Async bridge from framework adapters to HyperCode crew-orchestrator."""

from __future__ import annotations

import os
from typing import Any, Dict, Optional

import httpx


class CrewOrchestratorClient:
"""Small adapter client; HyperCode remains the execution authority."""

def __init__(
self,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
timeout: float = 60.0,
) -> None:
self.base_url = (base_url or os.getenv("HYPERCODE_ORCHESTRATOR_URL", "http://localhost:8081")).rstrip("/")
self.api_key = api_key or os.getenv("HYPERCODE_ORCHESTRATOR_API_KEY")
self.timeout = timeout

async def call_agent(self, spec: Dict[str, Any], state: Dict[str, Any]) -> Dict[str, Any]:
"""Dispatch one HyperAgent task and normalize the response."""

slug = spec.get("slug") or spec.get("id")
if not slug:
raise ValueError("HyperAgent spec requires 'slug' or 'id'")

task = {
"id": state.get("workflow_id", "framework-adapter-run"),
"type": state.get("task_type", "framework_adapter_task"),
"description": state.get("input") or state.get("description", ""),
"agents": [slug],
"requires_approval": state.get("requires_approval", True),
"context": state,
}
headers = {"Content-Type": "application/json"}
if self.api_key:
headers["X-API-Key"] = self.api_key

async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(f"{self.base_url}/execute", json={"task": task}, headers=headers)
response.raise_for_status()
payload = response.json()

result = payload.get("results", {}).get(slug, payload)
if isinstance(result, dict) and "result" in result:
result = result["result"]
if not isinstance(result, dict):
result = {"output": result}
result.setdefault("agent_slug", slug)
result.setdefault("status", "ok")
return result


async def call_agent_fn(spec: Dict[str, Any], state: Dict[str, Any]) -> Dict[str, Any]:
"""Drop-in callback for LangGraph/CrewAI adapter builders."""

return await CrewOrchestratorClient().call_agent(spec, state)
6 changes: 6 additions & 0 deletions adapters/requirements-frameworks.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Optional framework validation dependencies.
# Core HyperAgent-SDK remains framework-independent.
langgraph>=0.2,<1.0
crewai>=0.80,<2.0
httpx>=0.27,<1.0
pytest>=8.0,<9.0
56 changes: 56 additions & 0 deletions docs/FRAMEWORK_ADAPTER_VALIDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Framework Adapter Validation

## Purpose

This validation layer proves that HyperAgent-SDK can expose its agents to LangGraph and CrewAI while HyperCode remains the execution authority.

## Local setup

PowerShell:

```powershell
python -m venv .venv-adapters
.\.venv-adapters\Scripts\Activate.ps1
python -m pip install -r adapters/requirements-frameworks.txt
python -m pytest tests/test_framework_adapters.py -q
python -m compileall -q adapters tests/test_framework_adapters.py
```

Linux/macOS:

```bash
python3 -m venv .venv-adapters
source .venv-adapters/bin/activate
python -m pip install -r adapters/requirements-frameworks.txt
python -m pytest tests/test_framework_adapters.py -q
python -m compileall -q adapters tests/test_framework_adapters.py
```

## Connecting HyperCode

Set these environment variables at runtime. Do not commit them:

```text
HYPERCODE_ORCHESTRATOR_URL=http://localhost:8081
HYPERCODE_ORCHESTRATOR_API_KEY=<orchestrator-key>
```

The adapter sends `POST /execute` with the existing HyperCode task envelope and forwards `X-API-Key` when configured. The sample workflow runs `coder-agent` → `qa-engineer` → `healer-agent` in explicit order.

## Safety

- The workflow defaults to `requires_approval=true`.
- Frameworks do not receive Docker socket access.
- Secrets remain environment or Docker-secret managed.
- The adapters are glue; HyperCode remains the runtime authority.

## Release gate

Before merging or publishing a new SDK version:

1. Run the local commands above.
2. Confirm the GitHub Actions workflow is green.
3. Run an integration smoke test against a local crew-orchestrator.
4. Review the generated diff and permissions.
5. Merge the pull request.
6. Publish a patch release only after the adapter contract is stable.
39 changes: 39 additions & 0 deletions tests/test_framework_adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Fast, dependency-light tests for the framework adapter glue."""

from __future__ import annotations

import asyncio
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from adapters.code_test_heal_workflow import WORKFLOW, run_code_test_heal
from adapters.langgraph_crewai_sample import merge_state_with_result


class FakeClient:
def __init__(self) -> None:
self.calls = []

async def call_agent(self, spec, state):
self.calls.append(spec["slug"])
return {"agent_slug": spec["slug"], "status": "ok", "output": state["input"]}


def test_state_merge_does_not_mutate_input():
original = {"outputs": []}
merged = merge_state_with_result(original, {"agent_slug": "coder-agent", "output": "patch"})
assert original["outputs"] == []
assert merged["outputs"][0]["agent"] == "coder-agent"


def test_workflow_order_is_code_test_heal():
assert [stage["slug"] for stage in WORKFLOW] == ["coder-agent", "qa-engineer", "healer-agent"]


def test_workflow_dispatches_all_stages():
client = FakeClient()
result = asyncio.run(run_code_test_heal("Add adapter tests", client))
assert client.calls == ["coder-agent", "qa-engineer", "healer-agent"]
assert len(result["outputs"]) == 3
Loading