diff --git a/README.md b/README.md index 1fcdc18..1557050 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ coreason-maco-builder ### Dependency on Shared Kernel (coreason-manifest) -The `coreason-maco-builder` explicitly depends on the `coreason-manifest` shared kernel (v0.7.0+) for all output artifact definitions. +The `coreason-maco-builder` explicitly depends on the `coreason-manifest` shared kernel (v0.9.0+) for all output artifact definitions. **Why?** - **Contract-First Design**: The Builder acts as a compiler that translates vague user intent into a strict, executable specification. By importing schemas directly from the Kernel, we ensure that the output is always compliant with the Runtime's expectations. @@ -27,6 +27,13 @@ The `coreason-maco-builder` explicitly depends on the `coreason-manifest` shared **Value:** This architecture guarantees that any valid output from the Builder is readable by the CoReason Runtime, facilitating robust versioning and protocol upgrades. +## Changes in v0.2.0 + +- Upgraded `coreason-manifest` dependency to `^0.9.0`. +- Implemented `Compiler` bridge to ensure strict `RecipeManifest` output with integrity hashing. +- Refactored `Weaver` to use the `Compiler` for final artifact generation. +- Updated internal schemas to align with Shared Kernel definitions (e.g., `CouncilConfig`). + ## Getting Started ### Prerequisites diff --git a/docs/coreason_manifest_integration.md b/docs/coreason_manifest_integration.md new file mode 100644 index 0000000..bdf16a7 --- /dev/null +++ b/docs/coreason_manifest_integration.md @@ -0,0 +1,68 @@ +# Coreason Manifest Integration + +This document details the integration between `coreason-maco-builder` (The Compiler) and `coreason-manifest` (The Kernel). + +## Overview + +The `coreason-maco-builder` serves as the Architect and Compiler for the CoReason platform. It converts high-level, natural language intent into a strict, executable specification known as a `RecipeManifest`. + +The `RecipeManifest` and its constituent parts are defined in the `coreason-manifest` library, which serves as the Shared Kernel between the Builder (Design Time) and the Runtime (Execution Time). + +## Kernel Architecture (v0.9.0+) + +### 1. RecipeManifest +The top-level artifact produced by the Builder. +- **Class:** `coreason_manifest.recipes.RecipeManifest` +- **Purpose:** A self-contained, versioned, and immutable definition of a multi-agent strategy. +- **Key Fields:** + - `id`: Unique identifier (UUID or Name). + - `version`: SemVer string. + - `topology`: The execution graph (`GraphTopology`). + - `interface`: Input/Output schema (currently empty in Builder output). + - `state`: Internal state schema (currently empty). + +### 2. GraphTopology +The directed graph structure defining the flow of execution. +- **Class:** `coreason_manifest.definitions.topology.GraphTopology` +- **Purpose:** Container for Nodes and Edges. +- **Key Fields:** + - `nodes`: List of polymorphic nodes (`AgentNode`, etc.). + - `edges`: List of directed connections (`Edge`). + +### 3. AgentNode +A node representing an AI Agent. +- **Class:** `coreason_manifest.definitions.topology.AgentNode` +- **Purpose:** Defines the runtime configuration for a specific agent step. +- **Key Fields:** + - `id`: Unique UUID. + - `agent_name`: The role/persona identifier (mapped from Builder's `CompiledNode.base_role`). + - `council_config`: Optional configuration for voting/consensus logic. + - `type`: Literal "agent". + +### 4. CouncilConfig +Configuration for multi-agent decision making (Voting). +- **Class:** `coreason_manifest.definitions.topology.CouncilConfig` +- **Purpose:** Defines how a decision is reached when multiple voters are involved. +- **Key Fields:** + - `strategy`: The voting mechanism (e.g., "majority", "consensus"). + - `voters`: List of agent roles participating in the vote. + +## The Compilation Process + +The Builder uses a "Compiler Bridge" (`src/coreason_maco_builder/core/compiler/manifest.py`) to transform its internal workspace state (`RecipeDraft`) into the Kernel artifact. + +1. **Drafting:** The user iteratively designs the strategy. The state is stored as a `RecipeDraft`, which allows for incomplete nodes (`RED` status) and flexible structures. +2. **Resolution:** The `Weaver` resolves `LogicalNode` intents into `CompiledNode` configurations (Roles, Prompts). +3. **Compilation:** + - The `compile_to_manifest` function is called. + - `CompiledNode` objects are mapped to `AgentNode` objects. + - **Note:** Internal Builder metadata (e.g., specific `system_prompt` text) is currently *not* embedded directly into the `AgentNode` in v0.9.0, as the Runtime loads these from the Role Registry based on `agent_name`. + - Topological connections are mapped to `Edge` objects. + - An **Integrity Hash** is computed based on the canonical JSON representation of the topology and metadata. + - The final `RecipeManifest` is returned, ready for deployment. + +## Versioning Policy + +The Builder is strictly coupled to the `coreason-manifest` version. +- **Builder v0.2.0** -> **Manifest v0.9.0** +- Any changes to the Kernel schema require a corresponding update and refactor in the Builder's compiler logic. diff --git a/docs/coreason_manifest_v0.10.0_proposal.md b/docs/coreason_manifest_v0.10.0_proposal.md new file mode 100644 index 0000000..d258377 --- /dev/null +++ b/docs/coreason_manifest_v0.10.0_proposal.md @@ -0,0 +1,83 @@ +# Coreason Manifest v0.10.0 Proposal + +**From:** coreason-maco-builder Team +**To:** coreason-manifest Kernel Team +**Subject:** Request for Schema Enhancements in v0.10.0 + +During the refactoring of the Builder to align with v0.9.0, we identified several "friction points" where the Kernel schema constrained our ability to express the full intent of the strategy or forced us to use workarounds. + +We propose the following changes for v0.10.0 to better support the Builder's needs while maintaining the Runtime's strictness. + +## 1. Enable "Ad-Hoc" Agents (Restore `config` / `system_prompt`) + +**Current State:** +In v0.9.0, `AgentNode` only accepts `agent_name`. The Runtime presumably looks up the actual prompt and configuration from a "Role Registry" using this name. + +**Problem:** +The Builder often creates "Ad-Hoc" agents—specialized variations optimized by the `Optimizer` module—that do not strictly exist in the static Role Registry. Currently, we have to drop the optimized `system_prompt` and `model_config` when compiling, meaning the Runtime executes the generic Role instead of the optimized one. + +**Proposal:** +Add optional `config` and `system_prompt` fields to `AgentNode`. If present, the Runtime should use these *instead of* or *merged with* the registry defaults. + +```python +class AgentNode(Node): + # ... existing fields + system_prompt: Optional[str] = None # Override registry prompt + config: Optional[Dict[str, Any]] = None # Runtime-specific config (model, temp, etc.) +``` + +## 2. Formalize Integrity Hashing + +**Current State:** +The Builder calculates a cryptographic hash of the recipe to ensure integrity. In v0.9.0, `RecipeManifest` lacks a dedicated `integrity_hash` field, forcing us to append this critical metadata to the human-readable `description` field. + +**Problem:** +Parsing the hash from the description is brittle and not machine-readable. + +**Proposal:** +Add a dedicated, validated `integrity_hash` field to `RecipeManifest`. + +```python +class RecipeManifest(BaseModel): + # ... existing fields + integrity_hash: Optional[str] = Field( + default=None, + description="SHA256 hash of the canonical JSON topology" + ) +``` + +## 3. Builder Metadata Channel + +**Current State:** +The compilation process is currently "lossy". Information useful for the *design* process (e.g., `resolution_status`, `optimization_log`, specific UI coordinates) is lost when converting `RecipeDraft` to `RecipeManifest`. + +**Problem:** +If a user wants to "reload" a deployed `RecipeManifest` back into the Builder to edit it, we lack the context to reconstruct the full workspace state. + +**Proposal:** +Add a `metadata` dictionary to `RecipeManifest` and `Node` that is explicitly ignored by the Runtime execution engine but preserved for tooling. + +```python +class RecipeManifest(BaseModel): + # ... + metadata: Dict[str, Any] = Field(default_factory=dict, description="Tooling-specific metadata") +``` + +## 4. Flexible Interface Definition + +**Current State:** +The `interface` field requires explicit `inputs` and `outputs` dictionaries. + +**Problem:** +For many dynamic recipes, the interface is implicit or "Open". Requiring an explicit empty definition adds boilerplate. + +**Proposal:** +Make `interface` optional or allow a flag for `dynamic: true`. + +--- + +**Summary of Impact:** +These changes would allow the Builder to: +1. Deploy optimized, ad-hoc agents immediately without registry updates. +2. Guarantee artifact integrity in a standard way. +3. Support "Round-Trip" engineering (Builder -> Manifest -> Builder). diff --git a/docs/coreason_manifest_v0.10.0_requirements.md b/docs/coreason_manifest_v0.10.0_requirements.md new file mode 100644 index 0000000..55fe6c9 --- /dev/null +++ b/docs/coreason_manifest_v0.10.0_requirements.md @@ -0,0 +1,88 @@ +# Coreason Manifest v0.10.0 Technical Specification + +**To:** Coreason Kernel Engineering +**From:** Coreason MACO Builder Team +**Priority:** High +**Context:** The Builder requires these changes to support "Ad-Hoc" optimized agents, strict artifact integrity verification, and "Round-Trip" design workflows (Builder -> Runtime -> Builder). + +--- + +## 1. `AgentNode` Enhancements + +**Goal:** Allow the Builder to define "Ad-Hoc" agents (agents with dynamically optimized prompts/configs) that do not exist in the static Role Registry. + +**Current Limitation:** `AgentNode` only accepts `agent_name`. The Runtime looks up the prompt from a registry. This prevents the `Optimizer` from deploying improved prompts without a registry update. + +**Required Change:** +Update `coreason_manifest.definitions.topology.AgentNode` to include optional override fields. + +```python +class AgentNode(Node): + # ... existing fields + + # NEW FIELDS + system_prompt: Optional[str] = Field( + default=None, + description="Overrides the registry default prompt. Required for ad-hoc/optimized agents." + ) + + config: Optional[Dict[str, Any]] = Field( + default=None, + description="Runtime-specific configuration (e.g., model parameters, temperature). Merged with registry defaults." + ) +``` + +## 2. `RecipeManifest` Integrity & Metadata + +**Goal:** +1. Standardize artifact verification via a canonical hash. +2. Enable the Builder to re-import a compiled Manifest and restore the full design workspace (positions, colors, draft status). + +**Current Limitation:** +- We currently append the hash to the `description` string (brittle). +- We lose all design-time metadata (e.g., "Node X is RED status") during compilation. + +**Required Change:** +Update `coreason_manifest.recipes.RecipeManifest`. + +```python +class RecipeManifest(BaseModel): + # ... existing fields + + # NEW FIELD: Integrity + integrity_hash: Optional[str] = Field( + default=None, + description="SHA256 hash of the canonical JSON representation of the topology. Enforced by Builder, verified by Runtime." + ) + + # NEW FIELD: Tooling Metadata + # This field should be explicitly IGNORED by the Runtime execution engine. + metadata: Dict[str, Any] = Field( + default_factory=dict, + description="Container for design-time data (UI coordinates, resolution logs, draft status) to support re-hydration." + ) +``` + +## 3. `GraphTopology` State Schema + +**Goal:** Reduce boilerplate for "Open Interface" recipes. + +**Current Limitation:** `state_schema` is mandatory. + +**Required Change:** +Make `state_schema` optional in `GraphTopology`. + +```python +class GraphTopology(BaseModel): + # ... + state_schema: Optional[StateSchema] = Field(default=None) +``` + +--- + +## Summary of Success Criteria + +The v0.10.0 release will be considered successful if the Builder can: +1. Compile a recipe where an agent has a unique, optimizer-generated `system_prompt` that the Runtime actually executes. +2. Store the `integrity_hash` in its own field. +3. Store UI coordinates in `manifest.metadata["ui_layout"]` and retrieve them later. diff --git a/poetry.lock b/poetry.lock index e4cb938..1b7ef2b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -586,14 +586,14 @@ types-aiofiles = ">=23.0.0,<24.0.0" [[package]] name = "coreason-manifest" -version = "0.7.0" +version = "0.9.0" description = "This package is the definitive source of truth. If it isn't in the manifest, it doesn't exist. If it violates the manifest, it doesn't run." optional = false python-versions = ">=3.12" groups = ["main"] files = [ - {file = "coreason_manifest-0.7.0-py3-none-any.whl", hash = "sha256:6ad90158f4375a3777f1603ed5853759f75b7233abda11db88b60a9b316539c8"}, - {file = "coreason_manifest-0.7.0.tar.gz", hash = "sha256:89a391eb4bb2cbd27b6a86e1a9465426e233abc503274c92bdcdbf9769fe8534"}, + {file = "coreason_manifest-0.9.0-py3-none-any.whl", hash = "sha256:c815005fd55d588dd0fa065b43bd704230861e49449f888e05b83495cb11a9aa"}, + {file = "coreason_manifest-0.9.0.tar.gz", hash = "sha256:31ebedb35dcac8c407b3604e677d69b09bbee0446d7d2d27442a8a0c98f0394e"}, ] [package.dependencies] @@ -3525,4 +3525,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.12, !=3.14.1, <3.15" -content-hash = "8c94edad6e8e3e902d84b4d281796726463233979226362eeb416583a0d105f2" +content-hash = "9b6ee1666880d73f8c73039553b1a1750f72dd1f5b40e620262f8753d99a6796" diff --git a/pyproject.toml b/pyproject.toml index b9cdbc0..5af29da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "coreason_maco_builder" -version = "0.1.0" +version = "0.2.0" description = "coreason-maco-builder" authors = ["Gowtham A Rao "] license = "Prosperity-3.0" @@ -17,7 +17,7 @@ httpx = "^0.28.0" networkx = "^3.4.0" litellm = "^1.50.0" jinja2 = "^3.1.4" -coreason-manifest = "^0.7.0" +coreason-manifest = "^0.9.0" [tool.poetry.group.dev.dependencies] pytest = "^8.2.2" @@ -33,7 +33,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "coreason_maco_builder" -version = "0.1.0" +version = "0.2.0" description = "coreason-maco-builder" readme = "README.md" requires-python = ">=3.11" diff --git a/src/coreason_maco_builder/core/compiler/manifest.py b/src/coreason_maco_builder/core/compiler/manifest.py index 65bcf09..e28394a 100644 --- a/src/coreason_maco_builder/core/compiler/manifest.py +++ b/src/coreason_maco_builder/core/compiler/manifest.py @@ -8,6 +8,10 @@ # # Source Code: https://github.com/CoReason-AI/coreason_maco_builder +import hashlib +import json +from typing import Any, Dict, List + # Strictly import from Shared Kernel from coreason_manifest.definitions.topology import ( AgentNode, @@ -21,6 +25,8 @@ ) from coreason_manifest.recipes import RecipeManifest +from coreason_maco_builder.schemas.domain import RecipeDraft + __all__ = [ "RecipeManifest", "AgentNode", @@ -31,4 +37,68 @@ "Edge", "VisualMetadata", "GraphTopology", + "compute_integrity_hash", + "compile_to_manifest", ] + + +def compute_integrity_hash(data: Dict[str, Any]) -> str: + """Canonical hashing matching Kernel standards.""" + encoded = json.dumps(data, sort_keys=True, separators=(",", ":")).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def compile_to_manifest(draft: RecipeDraft, name: str, version: str = "1.0.0") -> RecipeManifest: + """ + Compiles a RecipeDraft into a strict RecipeManifest. + """ + # 1. Convert Nodes + kernel_nodes: List[Node] = [] + for node_id, compiled_node in draft.compiled_nodes.items(): + # Map Builder 'CompiledNode' to Kernel 'AgentNode' + k_node = AgentNode( + id=str(node_id), + type="agent", + agent_name=compiled_node.base_role, + council_config=compiled_node.council_config, + # Note: AgentNode in v0.9.0 does not accept 'config' or 'system_prompt'. + # These are currently not mapped into the Manifest AgentNode. + ) + kernel_nodes.append(k_node) + + # 2. Convert Edges + kernel_edges: List[Edge] = [] + for e in draft.topology.edges: + k_edge = Edge( + source_node_id=str(e.source), + target_node_id=str(e.target), + condition=e.relation, + ) + kernel_edges.append(k_edge) + + # 3. Build Topology + topology = GraphTopology(nodes=kernel_nodes, edges=kernel_edges) + + # 4. Prepare Data for Hashing + raw_data = { + "name": name, + "version": version, + "topology": topology.model_dump(mode="json"), + } + + # 5. Compute Hash + integrity_hash = compute_integrity_hash(raw_data) + + # 6. Return Strict Artifact + return RecipeManifest( + id=name, # Using name as ID + name=name, + version=version, + description=f"Generated Recipe. Integrity: {integrity_hash}", + interface={"inputs": {}, "outputs": {}}, + state={"schema": {}}, + parameters={}, + topology=topology, + # integrity_hash=integrity_hash, # Not supported in v0.9.0 schema + # created_at=datetime.utcnow(), # Not supported in v0.9.0 schema + ) diff --git a/src/coreason_maco_builder/core/weaver/assembler.py b/src/coreason_maco_builder/core/weaver/assembler.py index 006f30b..ae1acd8 100644 --- a/src/coreason_maco_builder/core/weaver/assembler.py +++ b/src/coreason_maco_builder/core/weaver/assembler.py @@ -11,11 +11,8 @@ from typing import Any, Dict, Optional from coreason_maco_builder.core.compiler.manifest import ( - AgentNode, - CouncilConfig, - Edge, - GraphTopology, RecipeManifest, + compile_to_manifest, ) from coreason_maco_builder.schemas.domain import ( CompiledNode, @@ -155,53 +152,12 @@ def assemble(self, draft: RecipeDraft, recipe_name: str, version: str = "1.0.0") if status != NodeStatus.GREEN: raise CompilationError(f"Node {node_id} is not fully resolved (Status: {status}). Cannot compile.") - # 2. Build Nodes - manifest_nodes = [] - + # 2. Validation: Ensure all green nodes have configs for logical_node in draft.topology.nodes: - compiled_config = draft.compiled_nodes.get(logical_node.id) - - if not compiled_config: + if not draft.compiled_nodes.get(logical_node.id): raise CompilationError( f"Logical Node {logical_node.id} is marked GREEN but missing compilation config." ) - # Map to AgentNode - # Note: We currently only support AgentNode mapping from CompiledNode - # In the future, we might support HumanNode/LogicNode distinctions based on CompiledNode type. - - # Map CouncilConfig - mapped_council = None - if compiled_config.council_config: - mapped_council = CouncilConfig( - strategy=compiled_config.council_config.strategy, - voters=compiled_config.council_config.participants, - ) - - agent_node = AgentNode( - id=str(logical_node.id), - agent_name=compiled_config.base_role, - council_config=mapped_council, - visual=None, # Visuals are not in LogicalNode yet, could be added later - type="agent", - ) - manifest_nodes.append(agent_node) - - # 3. Build Edges - manifest_edges = [] - for edge in draft.topology.edges: - manifest_edge = Edge( - source_node_id=str(edge.source), - target_node_id=str(edge.target), - condition=edge.relation, - ) - manifest_edges.append(manifest_edge) - - # 4. Construct Manifest - return RecipeManifest( - id=recipe_name, # Using name as ID for now, or could be a UUID - name=recipe_name, - version=version, - inputs={}, # Inputs are not yet defined in RecipeDraft explicitly, defaulting to empty - graph=GraphTopology(nodes=manifest_nodes, edges=manifest_edges), - ) + # 3. Delegate to Compiler + return compile_to_manifest(draft, recipe_name, version) diff --git a/src/coreason_maco_builder/main.py b/src/coreason_maco_builder/main.py index 0e6dcdf..ba33d7a 100644 --- a/src/coreason_maco_builder/main.py +++ b/src/coreason_maco_builder/main.py @@ -8,6 +8,7 @@ # # Source Code: https://github.com/CoReason-AI/coreason_maco_builder +from coreason_manifest.definitions.topology import GraphTopology from coreason_manifest.recipes import RecipeManifest from fastapi import FastAPI from fastapi.responses import JSONResponse @@ -39,8 +40,11 @@ def create_skeleton_recipe(name: str) -> RecipeManifest: id=name, name=name, version="1.0.0", - inputs={}, - graph={"nodes": [], "edges": []}, + description="Skeleton Recipe", + interface={"inputs": {}, "outputs": {}}, + state={"schema": {}}, + parameters={}, + topology=GraphTopology(nodes=[], edges=[]), ) diff --git a/src/coreason_maco_builder/schemas/domain.py b/src/coreason_maco_builder/schemas/domain.py index 8cf5872..5d8d0a8 100644 --- a/src/coreason_maco_builder/schemas/domain.py +++ b/src/coreason_maco_builder/schemas/domain.py @@ -14,12 +14,22 @@ from typing import Dict, List, Optional from uuid import UUID -from pydantic import BaseModel, Field - # --- Shared Kernel Integration --- # We import the strict definition of CouncilConfig from the manifest # to ensure that what we build is compatible with the Runtime. -from coreason_manifest.recipes import CouncilConfig +from coreason_manifest.definitions.topology import CouncilConfig +from pydantic import BaseModel, Field + +__all__ = [ + "NodeStatus", + "LogicalNode", + "CompiledNode", + "Edge", + "GraphTopology", + "OptimizationLogEntry", + "RecipeDraft", + "CouncilConfig", +] class NodeStatus(str, Enum): @@ -48,7 +58,7 @@ class CompiledNode(BaseModel): base_role: str = Field(..., description="Reference to coreason-construct Role") system_prompt: str = Field(..., description="The fully resolved, optimized string prompt") tools: List[str] = Field(default_factory=list, description="List of enabled tools/primitives") - + # Uses the Shared Kernel definition for strict validation council_config: Optional[CouncilConfig] = Field(default=None, description="Voting logic settings") @@ -83,11 +93,8 @@ class RecipeDraft(BaseModel): """ topology: GraphTopology - resolution_status: Dict[UUID, NodeStatus] = Field( - description="Map of Nodes to (Green/Yellow/Red) status" - ) + resolution_status: Dict[UUID, NodeStatus] = Field(description="Map of Nodes to (Green/Yellow/Red) status") optimization_log: List[OptimizationLogEntry] = Field(default_factory=list) compiled_nodes: Dict[UUID, CompiledNode] = Field( - default_factory=dict, - description="Map of Logical Node IDs to their Compiled Configurations" - ) \ No newline at end of file + default_factory=dict, description="Map of Logical Node IDs to their Compiled Configurations" + ) diff --git a/tests/core/compiler/test_compiler.py b/tests/core/compiler/test_compiler.py new file mode 100644 index 0000000..bbea2ab --- /dev/null +++ b/tests/core/compiler/test_compiler.py @@ -0,0 +1,99 @@ +# Copyright (c) 2025 CoReason, Inc. +# +# This software is proprietary and dual-licensed. +# Licensed under the Prosperity Public License 3.0 (the "License"). +# A copy of the license is available at https://prosperitylicense.com/versions/3.0.0 +# For details, see the LICENSE file. +# Commercial use beyond a 30-day trial requires a separate license. +# +# Source Code: https://github.com/CoReason-AI/coreason_maco_builder + +from typing import List +from uuid import uuid4 + +from coreason_manifest.definitions.topology import CouncilConfig + +from coreason_maco_builder.core.compiler.manifest import ( + RecipeManifest, + compile_to_manifest, + compute_integrity_hash, +) +from coreason_maco_builder.schemas.domain import ( + CompiledNode, + Edge, + GraphTopology, + LogicalNode, + NodeStatus, + RecipeDraft, +) + + +def test_compute_integrity_hash() -> None: + data = {"b": 2, "a": 1} + # Canonical JSON: {"a":1,"b":2} + expected_hash = "43258cff783fe7036d8a43033f830adfc60ec037382473548ac742b888292777" + assert compute_integrity_hash(data) == expected_hash + + +def test_compile_to_manifest_structure() -> None: + node_id = uuid4() + nodes = [LogicalNode(id=node_id, intent="Task A")] + edges: List[Edge] = [] + topology = GraphTopology(nodes=nodes, edges=edges) + + compiled_nodes = { + node_id: CompiledNode( + base_role="RoleA", + system_prompt="PromptA", + council_config=CouncilConfig(strategy="majority", voters=["v1", "v2"]), + ) + } + + draft = RecipeDraft( + topology=topology, + resolution_status={node_id: NodeStatus.GREEN}, + compiled_nodes=compiled_nodes, + ) + + manifest = compile_to_manifest(draft, "TestRecipe", "1.0.1") + + assert isinstance(manifest, RecipeManifest) + assert manifest.name == "TestRecipe" + assert manifest.version == "1.0.1" + + # Check Topology + assert len(manifest.topology.nodes) == 1 + k_node = manifest.topology.nodes[0] + assert k_node.id == str(node_id) + assert k_node.agent_name == "RoleA" + assert k_node.council_config.strategy == "majority" + assert k_node.council_config.voters == ["v1", "v2"] + + # Check Hash in description (since v0.9.0 puts it there or we put it there) + assert "Integrity:" in manifest.description + + +def test_compile_to_manifest_edges() -> None: + n1 = uuid4() + n2 = uuid4() + nodes = [LogicalNode(id=n1, intent="A"), LogicalNode(id=n2, intent="B")] + edges = [Edge(source=n1, target=n2, relation="next")] + + compiled_nodes = { + n1: CompiledNode(base_role="A", system_prompt="S"), + n2: CompiledNode(base_role="B", system_prompt="S"), + } + + draft = RecipeDraft( + topology=GraphTopology(nodes=nodes, edges=edges), + resolution_status={n1: NodeStatus.GREEN, n2: NodeStatus.GREEN}, + compiled_nodes=compiled_nodes, + ) + + manifest = compile_to_manifest(draft, "EdgeTest") + + assert len(manifest.topology.edges) == 1 + k_edge = manifest.topology.edges[0] + assert k_edge.source_node_id == str(n1) + assert k_edge.target_node_id == str(n2) + assert k_edge.condition == "next" diff --git a/tests/core/compiler/test_compiler_complex.py b/tests/core/compiler/test_compiler_complex.py new file mode 100644 index 0000000..413893f --- /dev/null +++ b/tests/core/compiler/test_compiler_complex.py @@ -0,0 +1,111 @@ +# Copyright (c) 2025 CoReason, Inc. +# +# This software is proprietary and dual-licensed. +# Licensed under the Prosperity Public License 3.0 (the "License"). +# A copy of the license is available at https://prosperitylicense.com/versions/3.0.0 +# For details, see the LICENSE file. +# Commercial use beyond a 30-day trial requires a separate license. +# +# Source Code: https://github.com/CoReason-AI/coreason_maco_builder + +from typing import List +from uuid import uuid4 + +from coreason_manifest.definitions.topology import CouncilConfig + +from coreason_maco_builder.core.compiler.manifest import compile_to_manifest +from coreason_maco_builder.schemas.domain import ( + CompiledNode, + Edge, + GraphTopology, + LogicalNode, + NodeStatus, + RecipeDraft, +) + + +def test_complex_strategy_compilation() -> None: + """ + Test a complex "Book Writing" strategy. + + Flow: + [Idea] -> [Outliner] -> [Ch1 Writer] --\ + -> [Ch2 Writer] --> [Editor] -> [Publisher] + + Features: + - Branching (Outliner -> Ch1, Ch2) + - Merging (Ch1, Ch2 -> Editor) + - Mixed roles + - Council config on Editor (Consensus required) + """ + + # 1. Define Nodes + idea_node = LogicalNode(id=uuid4(), intent="Generate Book Idea") + outliner_node = LogicalNode(id=uuid4(), intent="Create Outline") + ch1_node = LogicalNode(id=uuid4(), intent="Write Chapter 1") + ch2_node = LogicalNode(id=uuid4(), intent="Write Chapter 2") + editor_node = LogicalNode(id=uuid4(), intent="Edit Full Draft") + publisher_node = LogicalNode(id=uuid4(), intent="Publish") + + nodes = [idea_node, outliner_node, ch1_node, ch2_node, editor_node, publisher_node] + + # 2. Define Edges (Topology) + edges: List[Edge] = [ + Edge(source=idea_node.id, target=outliner_node.id, relation="next"), + Edge(source=outliner_node.id, target=ch1_node.id, relation="split_1"), + Edge(source=outliner_node.id, target=ch2_node.id, relation="split_2"), + Edge(source=ch1_node.id, target=editor_node.id, relation="merge"), + Edge(source=ch2_node.id, target=editor_node.id, relation="merge"), + Edge(source=editor_node.id, target=publisher_node.id, relation="finalize"), + ] + + # 3. Define Compilation (Resolved State) + compiled_nodes = { + idea_node.id: CompiledNode(base_role="Ideator", system_prompt="Generate ideas"), + outliner_node.id: CompiledNode(base_role="Planner", system_prompt="Create structure"), + ch1_node.id: CompiledNode(base_role="Writer", system_prompt="Write Ch1"), + ch2_node.id: CompiledNode(base_role="Writer", system_prompt="Write Ch2"), + editor_node.id: CompiledNode( + base_role="Editor", + system_prompt="Review content", + # Complex: Editor needs consensus from 3 reviewers + council_config=CouncilConfig(strategy="consensus", voters=["Senior Ed", "Copy Ed", "Legal"]), + ), + publisher_node.id: CompiledNode(base_role="Publisher", system_prompt="Release"), + } + + # 4. Create Draft + draft = RecipeDraft( + topology=GraphTopology(nodes=nodes, edges=edges), + resolution_status={n.id: NodeStatus.GREEN for n in nodes}, + compiled_nodes=compiled_nodes, + ) + + # 5. Compile + manifest = compile_to_manifest(draft, "BookWritingRecipe", "2.5.0") + + # 6. Verification + + # Basic Metadata + assert manifest.name == "BookWritingRecipe" + assert manifest.version == "2.5.0" + assert "Integrity: " in str(manifest.description) + + # Topology Count + assert len(manifest.topology.nodes) == 6 + assert len(manifest.topology.edges) == 6 + + # Deep Node Check (Editor) + k_editor = next(n for n in manifest.topology.nodes if n.id == str(editor_node.id)) + assert k_editor.agent_name == "Editor" + assert k_editor.council_config.strategy == "consensus" + assert len(k_editor.council_config.voters) == 3 + + # Connectivity Check + k_edges_to_editor = [e for e in manifest.topology.edges if e.target_node_id == str(editor_node.id)] + assert len(k_edges_to_editor) == 2 # From Ch1 and Ch2 + + # 7. Determinism Re-check (Robustness) + # Re-compiling the exact same draft should yield the EXACT same hash description + manifest_retry = compile_to_manifest(draft, "BookWritingRecipe", "2.5.0") + assert manifest.description == manifest_retry.description diff --git a/tests/core/compiler/test_compiler_edge_cases.py b/tests/core/compiler/test_compiler_edge_cases.py new file mode 100644 index 0000000..398950a --- /dev/null +++ b/tests/core/compiler/test_compiler_edge_cases.py @@ -0,0 +1,144 @@ +# Copyright (c) 2025 CoReason, Inc. +# +# This software is proprietary and dual-licensed. +# Licensed under the Prosperity Public License 3.0 (the "License"). +# A copy of the license is available at https://prosperitylicense.com/versions/3.0.0 +# For details, see the LICENSE file. +# Commercial use beyond a 30-day trial requires a separate license. +# +# Source Code: https://github.com/CoReason-AI/coreason_maco_builder + +from uuid import uuid4 + +from coreason_manifest.definitions.topology import CouncilConfig + +from coreason_maco_builder.core.compiler.manifest import ( + compile_to_manifest, + compute_integrity_hash, +) +from coreason_maco_builder.schemas.domain import ( + CompiledNode, + Edge, + GraphTopology, + LogicalNode, + NodeStatus, + RecipeDraft, +) + + +def test_integrity_hash_determinism() -> None: + """Test that the hash is stable for the same input, regardless of key order.""" + data1 = {"a": 1, "b": 2, "c": {"x": 10, "y": 20}} + data2 = {"b": 2, "c": {"y": 20, "x": 10}, "a": 1} + + hash1 = compute_integrity_hash(data1) + hash2 = compute_integrity_hash(data2) + + assert hash1 == hash2 + assert hash1 == "480391b44cae05364c707128ab0d799fb57105f21272ae2655b23c2813ce98f2" + + +def test_integrity_hash_sensitivity() -> None: + """Test that a small change produces a completely different hash.""" + data1 = {"val": "test"} + data2 = {"val": "Test"} # Case change + + assert compute_integrity_hash(data1) != compute_integrity_hash(data2) + + +def test_compile_empty_topology() -> None: + """Test compiling a draft with no nodes or edges.""" + draft = RecipeDraft( + topology=GraphTopology(nodes=[], edges=[]), + resolution_status={}, + compiled_nodes={}, + ) + + manifest = compile_to_manifest(draft, "Empty") + assert len(manifest.topology.nodes) == 0 + assert len(manifest.topology.edges) == 0 + # Check that description contains integrity hash + assert "Integrity: " in str(manifest.description) + + +def test_compile_disconnected_nodes() -> None: + """Test compiling nodes without edges.""" + n1 = uuid4() + n2 = uuid4() + + draft = RecipeDraft( + topology=GraphTopology(nodes=[LogicalNode(id=n1, intent="A"), LogicalNode(id=n2, intent="B")], edges=[]), + resolution_status={n1: NodeStatus.GREEN, n2: NodeStatus.GREEN}, + compiled_nodes={ + n1: CompiledNode(base_role="RoleA", system_prompt=""), + n2: CompiledNode(base_role="RoleB", system_prompt=""), + }, + ) + + manifest = compile_to_manifest(draft, "Disconnected") + assert len(manifest.topology.nodes) == 2 + assert len(manifest.topology.edges) == 0 + + +def test_compile_with_special_characters() -> None: + """Test compiling with names containing special characters.""" + n1 = uuid4() + special_name = "Role & < > \" '" + + draft = RecipeDraft( + topology=GraphTopology(nodes=[LogicalNode(id=n1, intent="A")], edges=[]), + resolution_status={n1: NodeStatus.GREEN}, + compiled_nodes={ + n1: CompiledNode(base_role=special_name, system_prompt="Prompt"), + }, + ) + + manifest = compile_to_manifest(draft, "SpecialChars") + node = manifest.topology.nodes[0] + assert node.agent_name == special_name + + # Ensure hash didn't crash + assert "Integrity: " in str(manifest.description) + + +def test_compile_edges_missing_condition() -> None: + """Test compiling edges where relation is None.""" + n1 = uuid4() + n2 = uuid4() + + draft = RecipeDraft( + topology=GraphTopology( + nodes=[LogicalNode(id=n1, intent="A"), LogicalNode(id=n2, intent="B")], + edges=[Edge(source=n1, target=n2, relation=None)], + ), + resolution_status={n1: NodeStatus.GREEN, n2: NodeStatus.GREEN}, + compiled_nodes={ + n1: CompiledNode(base_role="A", system_prompt=""), + n2: CompiledNode(base_role="B", system_prompt=""), + }, + ) + + manifest = compile_to_manifest(draft, "NullCondition") + edge = manifest.topology.edges[0] + assert edge.condition is None + + +def test_council_config_strategies() -> None: + """Test various council config strategies are preserved.""" + strategies = ["majority", "consensus", "unanimity", "custom"] + + for strategy in strategies: + n_id = uuid4() + draft = RecipeDraft( + topology=GraphTopology(nodes=[LogicalNode(id=n_id, intent="A")], edges=[]), + resolution_status={n_id: NodeStatus.GREEN}, + compiled_nodes={ + n_id: CompiledNode( + base_role="Role", system_prompt="", council_config=CouncilConfig(strategy=strategy, voters=["v"]) + ) + }, + ) + + manifest = compile_to_manifest(draft, f"Strat-{strategy}") + node = manifest.topology.nodes[0] + assert node.council_config.strategy == strategy diff --git a/tests/core/weaver/test_assembler.py b/tests/core/weaver/test_assembler.py index 649c8ba..22711ba 100644 --- a/tests/core/weaver/test_assembler.py +++ b/tests/core/weaver/test_assembler.py @@ -11,6 +11,7 @@ from uuid import uuid4 import pytest +from coreason_manifest.recipes import RecipeManifest from coreason_maco_builder.core.weaver.assembler import CompilationError, ConstructAdapter, Weaver from coreason_maco_builder.schemas.domain import ( @@ -132,7 +133,7 @@ def test_assemble_success(weaver: Weaver) -> None: node_id_2: CompiledNode( base_role="RoleB", system_prompt="PromptB", - council_config=CouncilConfig(enabled=True, participants=["Voter1"]), + council_config=CouncilConfig(strategy="consensus", voters=["Voter1"]), ), } @@ -146,11 +147,12 @@ def test_assemble_success(weaver: Weaver) -> None: manifest = weaver.assemble(draft, "TestRecipe") + assert isinstance(manifest, RecipeManifest) assert manifest.name == "TestRecipe" - assert len(manifest.graph.nodes) == 2 - assert len(manifest.graph.edges) == 1 + assert len(manifest.topology.nodes) == 2 + assert len(manifest.topology.edges) == 1 - node_map = {n.id: n for n in manifest.graph.nodes} + node_map = {n.id: n for n in manifest.topology.nodes} assert str(node_id_1) in node_map assert str(node_id_2) in node_map diff --git a/tests/core/weaver/test_assembler_edge_cases.py b/tests/core/weaver/test_assembler_edge_cases.py index c46c491..e32fd5e 100644 --- a/tests/core/weaver/test_assembler_edge_cases.py +++ b/tests/core/weaver/test_assembler_edge_cases.py @@ -145,8 +145,8 @@ def test_assemble_empty_topology(weaver: Weaver) -> None: manifest = weaver.assemble(draft, "EmptyRecipe", "1.0.0") assert manifest.name == "EmptyRecipe" - assert len(manifest.graph.nodes) == 0 - assert len(manifest.graph.edges) == 0 + assert len(manifest.topology.nodes) == 0 + assert len(manifest.topology.edges) == 0 def test_assemble_with_invalid_version_string(weaver: Weaver) -> None: diff --git a/tests/schemas/test_domain.py b/tests/schemas/test_domain.py index c1dd539..88f60dc 100644 --- a/tests/schemas/test_domain.py +++ b/tests/schemas/test_domain.py @@ -35,7 +35,7 @@ def test_logical_node_creation() -> None: def test_compiled_node_creation() -> None: """Test creating a CompiledNode with nested CouncilConfig.""" - config = CouncilConfig(enabled=True, participants=["Alice", "Bob"]) + config = CouncilConfig(strategy="consensus", voters=["Alice", "Bob"]) node = CompiledNode( base_role="Reviewer", system_prompt="You are a reviewer.", @@ -46,8 +46,8 @@ def test_compiled_node_creation() -> None: assert node.system_prompt == "You are a reviewer." assert node.tools == ["search"] assert node.council_config is not None - assert node.council_config.enabled is True - assert node.council_config.participants == ["Alice", "Bob"] + assert node.council_config.strategy == "consensus" + assert node.council_config.voters == ["Alice", "Bob"] def test_recipe_draft_creation() -> None: diff --git a/tests/schemas/test_domain_complex.py b/tests/schemas/test_domain_complex.py index e082ba6..b35a834 100644 --- a/tests/schemas/test_domain_complex.py +++ b/tests/schemas/test_domain_complex.py @@ -62,7 +62,7 @@ def test_invalid_node_status_enum() -> None: with pytest.raises(ValidationError) as excinfo: RecipeDraft( topology=GraphTopology(nodes=[], edges=[]), - resolution_status={uuid4(): "BLUE"}, # type: ignore[dict-item] # BLUE is not a valid NodeStatus + resolution_status={uuid4(): "BLUE"}, # BLUE is not a valid NodeStatus ) assert "Input should be 'GREEN', 'YELLOW' or 'RED'" in str(excinfo.value) @@ -87,17 +87,17 @@ def test_inconsistent_draft_state_is_allowed() -> None: def test_council_config_edge_cases() -> None: """Test CouncilConfig with edge case values.""" - # Case 1: Enabled but empty participants - config = CouncilConfig(enabled=True, participants=[]) - assert config.enabled is True - assert config.participants == [] + # Case 1: Empty voters + config = CouncilConfig(strategy="majority", voters=[]) + assert config.strategy == "majority" + assert config.voters == [] - # Case 2: Disabled but with participants (valid but effectively ignored) - config_disabled = CouncilConfig(enabled=False, participants=["Alice"]) - assert config_disabled.enabled is False - assert config_disabled.participants == ["Alice"] + # Case 2: Custom strategy + config_custom = CouncilConfig(strategy="custom_strategy", voters=["Alice"]) + assert config_custom.strategy == "custom_strategy" + assert config_custom.voters == ["Alice"] # Integration into CompiledNode node = CompiledNode(base_role="Role", system_prompt="Prompt", council_config=config) assert node.council_config is not None - assert node.council_config.participants == [] + assert node.council_config.voters == [] diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 453074b..d4dd0a8 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -1,6 +1,6 @@ import uuid -from coreason_manifest import CouncilConfig +from coreason_manifest.definitions.topology import CouncilConfig from coreason_maco_builder.schemas.domain import CompiledNode, LogicalNode, RecipeDraft @@ -31,7 +31,8 @@ def test_compiled_node_creation() -> None: def test_recipe_draft_creation() -> None: node_id = uuid.uuid4() - draft = RecipeDraft(topology={"nodes": [], "edges": []}, resolution_status={node_id: "Green"}) - assert draft.topology == {"nodes": [], "edges": []} - assert draft.resolution_status == {node_id: "Green"} + draft = RecipeDraft(topology={"nodes": [], "edges": []}, resolution_status={node_id: "GREEN"}) + assert draft.topology.nodes == [] + assert draft.topology.edges == [] + assert draft.resolution_status == {node_id: "GREEN"} assert draft.optimization_log == []