Metadata-Version: 2.4
Name: marona
Version: 0.8.5
Summary: Provider-neutral AI agent runtime and MCP/Skill gateway for Marona Hub connections, managed agents, and bring-your-own-agent integrations.
Author: Blessing Nyuwani
License-Expression: MIT
Project-URL: Homepage, https://www.marona.ai
Project-URL: Documentation, https://hub.marona.ai
Keywords: marona,sdk,mcp,agents,ai,runtime
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx<1.0,>=0.28
Provides-Extra: dev
Requires-Dist: pytest<9.0,>=8.0; extra == "dev"
Requires-Dist: build<2.0,>=1.2; extra == "dev"
Requires-Dist: twine<7.0,>=6.0; extra == "dev"
Dynamic: license-file

# Marona Python SDK

Provider-neutral AI agent runtime and MCP/Skill gateway for Marona Hub
connections, managed agents, and bring-your-own-agent integrations.

```bash
pip install marona
```

## 1. Marona Hub

Connect Apps and governed Skills as one neutral MCP tool collection.

```python
from marona import Marona

marona = Marona(api_key="YOUR_MARONA_API_KEY")

# Connect every App and Skill available to this developer key.
connection = marona.hub.connect()
```

Select a smaller capability set when needed:

```python
connection = marona.hub.connect(
    apps=["group-fund"],
    skills=["create-group-fund"],
)
```

Use the connection directly:

```python
tools = connection.list_tools()
result = connection.call_tool(
    "skill__create_group_fund",
    {"request": "Create a family savings group fund"},
)
```

`MCPConnection` is not tied to OpenAI, LangGraph, CrewAI, or another model
vendor. It exposes:

```python
connection.list_tools()
connection.call_tool(name, arguments)
connection.server_url
connection.server_urls
connection.warnings
connection.session
```

An unresolved App or Skill name does not discard valid tools. Check
`connection.warnings` for its `code`, `selector_type`, `slug`, and corrective
message. Authentication, permission, and configured-server failures
remain blocking errors.

Marona Hub owns discovery, identity, permissions, App and Skill resolution,
approvals, governed execution, and online, offline, or hybrid availability.
With no selectors, online and hybrid connections include every capability
available to the developer key; offline connections include every capability
installed on the device.

Connected Apps execute through Marona governance. When an App such as Google
Contacts or Calendar needs sign-in, the tool result includes
`structuredContent.authorization_url` and a `link` artifact. Pass `user_id`
when calling a tool directly so the resulting service connection belongs to
the correct end user:

```python
result = connection.call_tool(
    "contacts__search_contacts",
    {"query": "John"},
    user_id="customer_482",
    conversation_id="chat_91a7",
)
```

Use the asynchronous methods inside an event loop:

```python
connection = await marona.hub.connect_async(
    apps=["group-fund"],
    skills=["create-group-fund"],
)
tools = await connection.list_tools_async()
result = await connection.call_tool_async(
    "skill__create_group_fund",
    {"request": "Create a family savings group fund"},
)
```

## 2. Marona Agent

Use Marona's `Agent` and `Runner` when you want one simple managed agent API.

```python
from marona import Agent, Marona, Runner

marona = Marona(api_key="YOUR_MARONA_API_KEY")

tools = marona.hub.connect(
    apps=["sda-books"],
)

agent = Agent(
    name="Customer Assistant",
    model="marona/gpt-5.6",
    instructions="Help the customer.",
    tools=tools,
)

result = await Runner.run(
    agent,
    "Download Steps to Christ",
    user_id="customer_482",
    session_id="chat_91a7",
)

print(result.final_output)
```

`user_id` is optional. `session_id` is also optional and defaults to `default`.
Marona derives the developer scope from the authenticated API key and keeps
conversation history isolated by developer, user, and session. In synchronous
applications, use `Runner.run_sync(...)`.

## 3. Marona Runtime

Use `responses.create(...)` when Marona should manage model reasoning, tool
selection, permission and approval checks, execution, and the final response.

```python
from marona import Marona

marona = Marona(api_key="YOUR_MARONA_API_KEY")

tools = marona.hub.connect(
    apps=["group-fund"],
    skills=["create-group-fund"],
)

response = marona.responses.create(
    model="marona/gpt-5.6",
    tools=tools,
    input="Create a family savings group fund",
)

print(response.output)
```

Every request requires the Marona developer key. Managed models use only that
key and consume the developer's Marona balance at the underlying provider
price. Direct-provider models also require the provider key and are billed by
that provider.

```python
model="marona/auto"
model="marona/gpt-5.6"
model="marona/gpt-5-mini"
model="marona/gpt-5-nano"
model="marona/claude-sonnet"
model="marona/gemini-2.5-pro"
model="marona/deepseek-chat"
```

Use `marona/auto` when Marona should select both the model and provider. Hard
capability requirements are enforced first, a small internal classifier reasons
about task type and complexity, and provider health, cost, and latency select
where the chosen model runs:

```python
response = marona.responses.create(
    model="marona/auto",
    routing={
        "strategy": "balanced",  # balanced | quality | cost | latency
        "max_cost_usd": 0.02,
        "target_latency_ms": 3000,
        "timeout_ms": 30000,
        "allowed_models": ["marona/gpt-5-mini", "marona/claude-sonnet"],
        "allowed_providers": ["openai", "anthropic", "openrouter"],
        "fallback": True,
    },
    input="Review this distributed system design.",
)

print(response.output)
print(response.model)
print(response.routing)
```

With a specific `marona/...` model, Marona keeps that exact model and may only
change provider. Direct-provider routes are never rerouted.

The same request also supports direct-provider, private, and local models:

```python
model="openai/gpt-5.6"                       # Marona key + OpenAI key
model="openrouter/anthropic/claude-sonnet"   # OpenRouter
model="anthropic/claude-sonnet"              # Anthropic directly
model="google/gemini"                        # Google directly
model="ollama/qwen3"                         # Ollama
model="litellm/local-qwen"                   # LiteLLM gateway
model="local/qwen"                           # Downloaded/in-process model
```

Direct provider routes resolve credentials from their standard environment
variables. For OpenRouter, set `OPENROUTER_API_KEY`; Marona automatically uses
`https://openrouter.ai/api/v1` and preserves the remaining OpenRouter model
slug:

```python
response = marona.responses.create(
    model="openrouter/anthropic/claude-sonnet",
    input="Help me with this request",
)

print(response.output)
```

Register only custom providers or downloaded in-process models:

```python
marona.models.register(
    name="office/company-assistant",
    endpoint="https://models.office.example/v1",
    model="company-assistant-v2",
    api_key="YOUR_PROVIDER_KEY",
)

marona.models.register(
    name="local/qwen",
    executor=qwen_executor,
    context_window=8192,
    max_output_tokens=512,
)
```

For asynchronous applications, call `await marona.responses.create_async(...)`.

### Images

```python
from marona import Marona

marona = Marona(api_key="YOUR_MARONA_API_KEY")

response = marona.responses.create(
    model="openai/gpt-5.6",
    input=[
        {
            "role": "user",
            "content": [
                {"type": "input_text", "text": "Summarize this image."},
                {"type": "input_image", "image_url": "https://example.com/image.jpg"},
            ],
        }
    ],
)

print(response.output)
```

### Files

```python
from marona import Marona

marona = Marona(api_key="YOUR_MARONA_API_KEY")

response = marona.responses.create(
    model="openai/gpt-5.6",
    input=[
        {
            "role": "user",
            "content": [
                {"type": "input_text", "text": "Summarize this file."},
                {
                    "type": "input_file",
                    "filename": "report.pdf",
                    "file_data": "data:application/pdf;base64,...",
                    "detail": "high",
                },
            ],
        }
    ],
)

print(response.output)
```

## 4. Bring Your Own Agent

The external framework owns its Agent, reasoning, and orchestration. Marona
supplies neutral MCP tools and retains authorization, approvals, and execution.

### OpenAI Agents SDK Example

```python
from marona import Marona
from agents import Agent, Runner

marona = Marona(api_key="YOUR_MARONA_API_KEY")
connection = marona.hub.connect(
    apps=["group-fund"],
    skills=["create-group-fund"],
)

# Adapt only at the framework boundary. Marona itself remains vendor-neutral.
framework_tools = your_openai_agents_mcp_adapter(connection)

agent = Agent(
    name="Group Fund Assistant",
    model="marona/gpt-5.6",
    instructions="Help users create and manage group funds.",
    tools=framework_tools,
)

result = Runner.run_sync(agent, "Create a family savings group fund")
print(result.final_output)
```

`your_openai_agents_mcp_adapter(...)` represents the OpenAI-specific adapter at
the framework boundary; it is not part of Marona's vendor-neutral core API.

An MCP-compatible framework can map its standard tool-list and tool-call hooks
directly to `connection.list_tools()` and `connection.call_tool(...)`. Marona
does not claim that one Python tool object automatically satisfies every agent
framework's proprietary interface.

## 8. Publish A Skill

Every workflow entry uses `step()`; `type` selects reasoning, approval, or App
execution. Set `visibility="public"` for Hub discovery or `"private"` for only
the owning developer workspace. New Skills default to private.

```python
from marona import Marona
from marona.skills import skill, step


@skill(
    name="create-group-fund",
    description="Create a group fund after explicit user approval.",
    visibility="public",
    governs=["group-fund.create_group"],
)
def create_group_fund():
    request = step(
        id="understand-request",
        type="reasoning",
        instruction="Extract the group name and currency.",
        inputs={"message": "{{ context.user_message }}"},
        outputs={"name": "string", "currency": "string"},
    )
    permission = step(
        id="confirm-create",
        type="approval",
        message=f"Create '{request.name}' in {request.currency}?",
        outputs={"approved": "boolean"},
    )
    return step(
        id="create-group",
        type="app",
        app="group-fund",
        capability="group-fund.create_group",
        instruction="Create the approved group.",
        condition=permission.approved,
        inputs={"name": request.name, "currency": request.currency},
        outputs={"group_id": "string", "name": "string"},
    )


marona = Marona(api_key="YOUR_MARONA_API_KEY")
marona.skills.publish(create_group_fund, version="1.0.0")
```

## Execution Modes

Set mode when creating Marona:

```python
marona = Marona(
    api_key="YOUR_MARONA_API_KEY",
    mode="hybrid",
)
```

- `online`: network models and online MCP targets are allowed.
- `hybrid`: local/private execution may fall back to online execution.
- `offline`: only installed local Apps, Skills, data, and local models run.

Changing `model` never changes App, Skill, permission, approval, or MCP rules.
