Metadata-Version: 2.4
Name: agent-env-protocol
Version: 0.2.0
Summary: Agent Environment Protocol - 基于文件系统的 Agent 管理协议
Requires-Python: >=3.12
Requires-Dist: loguru>=0.7.0
Requires-Dist: mcp>=1.26.0
Requires-Dist: strictyaml>=1.7.3
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

<p align="right">
<a href="README_zh.md">中文</a>
</p>

# AEP - Agent Environment Protocol

<p align="center">
<img src="docs/aep-logo.svg" alt="AEP Logo" width="720" />
</p>

<p align="center">
<strong>A file-system-first terminal environment for LLM agents.</strong>

<em>Manage capabilities with <code>Profile</code>; run session-bound tools with <code>AEP</code>.</em>
</p>

---

## Why AEP?

AEP is designed around one assumption: an agent already knows how to work in a terminal. Instead of stuffing every capability into prompt text or forcing everything through remote wrappers, AEP mounts capabilities into a workspace and gives the host a small runtime API.

It keeps three capability categories:

- `tools/`: a shared Python tool environment, invoked through `tools run`
- `skills/`: isolated skill packages, invoked through `skills run`
- `library/`: tree-structured reference documents with generated `index.md`

The public Python surface is intentionally small:

- `Profile`: add tools, skills, library docs, MCP config, then generate indexes
- `AEP`: mount one profile into one workspace, build agent context, expose tool schemas for one logical session, route tool calls for that session
- `ToolSchemaBinding`: lightweight `session_id + schemas` pair returned by `AEP.tool_schemas(session_id)`

## Architecture

Current structure is split into two public surfaces:

1. `Profile`: resource management and indexing
2. `AEP`: runtime mounting, agent context generation, and session-bound tool execution

Internal implementation is organized under:

- `src/aep/runtime/`: runtime facade, context rendering, session runtime, tool-call dispatcher
- `src/aep/capability/`: tools, skills, library, indexing
- `src/aep/profile.py`: public config facade

<details>
<summary>Profile directory layout</summary>

```text
config_dir/
├── tools/
│   ├── .venv/
│   ├── requirements.txt
│   ├── index.md
│   └── *.py
├── skills/
│   ├── index.md
│   └── <skill-name>/
│       ├── .venv/
│       ├── SKILL.md
│       ├── requirements.txt
│       └── scripts/...
├── library/
│   ├── index.md
│   └── ...
└── _mcp/
    └── <server>/config.json
```

</details>

## API Summary

```python
from aep import AEP, Profile, ToolSchemaBinding
```

- `Profile(config_dir, ...)`
- `Profile.add_tool(...)`
- `Profile.add_skill(...)`
- `Profile.add_library(...)`
- `Profile.add_mcp_server(...)`
- `Profile.index()`
- `AEP(config_or_profile, workspace=..., agent_dir=".agents")`
- `AEP.build_context()`
- `AEP.tool_schemas(session_id) -> ToolSchemaBinding`
- `AEP.call_tool(session_id, name, arguments)`
- `AEP.detach()`

`tool_schemas(session_id)` returns:

```python
ToolSchemaBinding(
    session_id="agent-1",
    schemas=[...],
)
```

`schemas` is what you send to the model. `session_id` stays on the host side and is used to route later tool calls back into the correct logical session.

## Quick Start

### Installation

Once published, install via pip:

```bash
pip install agent-env-protocol
```

For local development:

```bash
git clone https://github.com/Slipstream-Max/Agent-Environment-Protocol
cd Agent-Environment-Protocol
uv sync --extra dev
```

### CLI

```bash
# 1. Create or update a profile
aep index --profile ./agent_config

# 2. Add capabilities
aep tool add ./examples/calc.py --name calc --profile ./agent_config
aep skill add ./examples/greeter --name greeter --profile ./agent_config
aep library add ./docs/guide.md --target-dir intro --profile ./agent_config
aep index --profile ./agent_config

# 3. Start a shell inside one workspace
aep shell --profile ./agent_config --workspace ./workspace

# Inside the shell:
# > tools list
# > tools run "tools.calc.add(1, 2)"
# > tools run PY<<
# > import pandas as pd
# > print(tools.calc.add(1, 2))
# > PY
# > skills run greeter/main.py
```

### Minimal Python API

```python
import asyncio

from aep import AEP, Profile


async def main() -> None:
    profile = Profile("./agent_capabilities")
    profile.index()

    aep = AEP(profile, workspace="./my_project")
    context = aep.build_context()
    binding = aep.tool_schemas("agent-main")

    print(context)
    print(binding.session_id)
    print(binding.schemas)

    result = await aep.call_tool(
        binding.session_id,
        "aep_exec",
        {"command": "tools list"},
    )
    print(result)

    aep.detach()


asyncio.run(main())
```

## Agent Context

`AEP.build_context()` returns one prompt block for the model. It contains:

- an introduction explaining that this is an agent terminal environment
- the exposed tool schemas and what each one does
- the special command conventions: `tools run "..."`, `tools run PY<< ... PY`, and `skills run ...`
- the generated indexes for the current profile

This keeps prompt construction centralized in one place instead of scattering it between profile and adapters.

## OpenAI SDK Integration

```python
import asyncio
import json
from dataclasses import dataclass

from openai import AsyncOpenAI

from aep import AEP, Profile


@dataclass
class AgentState:
    session_id: str
    tools: list[dict]
    messages: list[dict]


def parse_arguments(raw: str | None) -> dict:
    if not raw:
        return {}
    value = json.loads(raw)
    return value if isinstance(value, dict) else {}


async def run_turn(
    *,
    client: AsyncOpenAI,
    model: str,
    aep: AEP,
    agent: AgentState,
    user_text: str,
) -> None:
    agent.messages.append({"role": "user", "content": user_text})

    while True:
        response = await client.chat.completions.create(
            model=model,
            messages=agent.messages,
            tools=agent.tools,
            tool_choice="auto",
        )
        message = response.choices[0].message

        agent.messages.append(
            {
                "role": "assistant",
                "content": message.content or "",
                "tool_calls": [
                    {
                        "id": call.id,
                        "type": call.type,
                        "function": {
                            "name": call.function.name,
                            "arguments": call.function.arguments,
                        },
                    }
                    for call in (message.tool_calls or [])
                ],
            }
        )

        if not message.tool_calls:
            return

        for call in message.tool_calls:
            result = await aep.call_tool(
                agent.session_id,
                call.function.name,
                parse_arguments(call.function.arguments),
            )
            agent.messages.append(
                {
                    "role": "tool",
                    "tool_call_id": call.id,
                    "content": json.dumps(result, ensure_ascii=False),
                }
            )


async def main() -> None:
    profile = Profile("./config")
    profile.index()
    aep = AEP(profile, workspace="./workspace")

    context = aep.build_context()
    binding1 = aep.tool_schemas("agent-1-session")
    binding2 = aep.tool_schemas("agent-2-session")

    system_prompt = (
        "You are working inside an AEP terminal environment.\n\n"
        f"{context}"
    )

    agent1 = AgentState(
        session_id=binding1.session_id,
        tools=binding1.schemas,
        messages=[{"role": "system", "content": system_prompt}],
    )
    agent2 = AgentState(
        session_id=binding2.session_id,
        tools=binding2.schemas,
        messages=[{"role": "system", "content": system_prompt}],
    )

    client = AsyncOpenAI()

    await run_turn(
        client=client,
        model="gpt-4.1-mini",
        aep=aep,
        agent=agent1,
        user_text="Run `pwd` and then export A=1.",
    )
    await run_turn(
        client=client,
        model="gpt-4.1-mini",
        aep=aep,
        agent=agent2,
        user_text="Run `pwd` and then export B=2.",
    )

    env1 = await aep.call_tool(agent1.session_id, "aep_env", {})
    env2 = await aep.call_tool(agent2.session_id, "aep_env", {})
    print(env1)
    print(env2)

    aep.detach()


if __name__ == "__main__":
    asyncio.run(main())
```

Each agent gets the same tool names, but a different host-side `session_id`. Session isolation is explicit on the host and invisible to the model.

## Tool Schemas

The default tool surface is:

- `aep_exec`: execute one command in the current bound session
- `aep_output`: fetch incremental output for one execution
- `aep_kill`: stop a queued or running execution
- `aep_history`: inspect recent command history
- `aep_env`: inspect custom session environment variables

## References

- Detailed API docs: `docs/api.md`
- Architecture notes: `docs/arch.md`

## License

[MIT License](https://www.google.com/search?q=LICENSE)
