innerloop


Nameinnerloop JSON
Version 0.0.0.dev2 PyPI version JSON
download
home_pageNone
SummaryLLM in a loop with tools, MCP, sessions, and structured outputs.
upload_time2025-11-02 17:50:48
maintainerNone
docs_urlNone
authorNone
requires_python>=3.10
licenseNone
keywords agent ai anthropic automation claude codex devtool gemini llm openai sdk
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # InnerLoop

Python SDK for invoking the [OpenCode](https://opencode.ai/) coding CLI in a headless, non-interactive manner.

Supports:

- Synchronous and Asynchronous modes
- Structured outputs using Pydantic models
- Sessions for multiple invocations with shared context
- Permission configuration for reading, writing, web, and bash tools
- Configurable working directory per loop, session, or call

## Quick Start

See [docs/guides/installing-opencode.md](docs/guides/installing-opencode.md) for installation and configuration.

Quick install:
```bash
make install_opencode  # Handles bun config automatically
```

Or manually:
```bash
npm install -g opencode-ai
export OPENCODE_API_KEY=your-key
opencode models
```

## Installation

You can install `innerloop` using `uv` (recommended) or `pip`.

```bash
# Using uv
uv pip install innerloop

# Using pip
pip install innerloop
```

## Prerequisites

**InnerLoop requires the OpenCode CLI.** Install it and ensure `opencode` is on your PATH:

```bash
opencode --version
```

If the command fails, install OpenCode from [opencode.ai](https://opencode.ai/) and add it to your PATH.

See the [OpenCode documentation](https://opencode.ai/docs/) for installation instructions.

## Usage

<!-- BEGIN USAGE -->
We summarize results below each snippet without `print()` in the examples.
Summary shows: Output (first 100 chars), Duration (ms), Events.

To render yourself from a Response object:

```python
def show(resp):
    print('Output:', str(resp.output)[:100])
    dur = (resp.time.end - resp.time.start) if resp.time else 0
    print(f'Duration: {dur} ms')
    print(f'Events: {resp.event_count}')
    print(resp.model_dump_json(by_alias=True, indent=4))
```

See: src/innerloop/response.py


### Synchronous Run

```python
from innerloop import Loop

loop = Loop(model="anthropic/claude-haiku-4-5")
response = loop.run("Say hello, one short line.")
```

```text
Output: Hello! How can I help you with your code today?
Duration: 3203 ms
Events: 3
```

<details>
  <summary>JSON Output</summary>

```json
{
    "session_id": "ses_5c3c3ddbaffe1xdYfKWI7bvq3I",
    "input": "Say hello, one short line.",
    "output": "Hello! How can I help you with your code today?",
    "attempts": 1,
    "events": [
        {
            "seq": 1,
            "timestamp": 1761947166821,
            "type": "step_start"
        },
        {
            "seq": 2,
            "timestamp": 1761947166943,
            "type": "text",
            "text": "Hello! How can I help you with your code today?"
        },
        {
            "seq": 3,
            "timestamp": 1761947166991,
            "type": "step_finish"
        }
    ],
    "time": {
        "start": 1761947165017,
        "end": 1761947168220
    },
    "event_count": 3
}
```
</details>

### Asynchronous Run

```python
import asyncio
from innerloop import Loop

async def main():
    loop = Loop(model="anthropic/claude-haiku-4-5")
    async with loop.asession() as s:
        await s("Remember this number: 42")
        response = await s("What was the number?")

asyncio.run(main())
```

```text
Output: The number was 42.
Duration: 4165 ms
Events: 3
```

<details>
  <summary>JSON Output</summary>

```json
{
    "session_id": "ses_5c3c3d139ffeguE8rhOWW70ST2",
    "input": "What was the number?",
    "output": "The number was 42.",
    "attempts": 1,
    "events": [
        {
            "seq": 1,
            "timestamp": 1761947173793,
            "type": "step_start"
        },
        {
            "seq": 2,
            "timestamp": 1761947173971,
            "type": "text",
            "text": "The number was 42."
        },
        {
            "seq": 3,
            "timestamp": 1761947174039,
            "type": "step_finish"
        }
    ],
    "time": {
        "start": 1761947172214,
        "end": 1761947176379
    },
    "event_count": 3
}
```
</details>

### Tool Use (with workdir)

```python
from innerloop import Loop, allow

loop = Loop(
    model="anthropic/claude-haiku-4-5",
    perms=allow("bash"),
)
loop.default_workdir = "src/innerloop"
response = loop.run("Use bash: ls -1\nReturn only the raw command output.")
```

```text
Output: ```
__init__.py
__pycache__
api.py
config.py
errors.py
events.py
helper.py
invoke.py
mcp.py
output.…
Duration: 4802 ms
Events: 6
```

<details>
  <summary>JSON Output</summary>

```json
{
    "session_id": "ses_5c3c3b15cffecPu1g9yj32ee7j",
    "input": "Use bash: ls -1\nReturn only the raw command output.",
    "output": "```\n__init__.py\n__pycache__\napi.py\nconfig.py\nerrors.py\nevents.py\nhelper.py\ninvoke.py\nmcp.py\noutput.py\npermissions.py\nproc.py\nproviders.py\nrequest.py\nresponse.py\nstructured.py\nusage.py\n```",
    "attempts": 1,
    "events": [
        {
            "seq": 1,
            "timestamp": 1761947178427,
            "type": "step_start"
        },
        {
            "seq": 2,
            "timestamp": 1761947178973,
            "type": "tool_use",
            "output": "__init__.py\n__pycache__\napi.py\nconfig.py\nerrors.py\nevents.py\nhelper.py\ninvoke.py\nmcp.py\noutput.py\npe… (truncated)",
            "status": "completed",
            "tool": "bash"
        },
        {
            "seq": 3,
            "timestamp": 1761947179001,
            "type": "step_finish"
        },
        {
            "seq": 4,
            "timestamp": 1761947180024,
            "type": "step_start"
        },
        {
            "seq": 5,
            "timestamp": 1761947180306,
            "type": "text",
            "text": "```\n__init__.py\n__pycache__\napi.py\nconfig.py\nerrors.py\nevents.py\nhelper.py\ninvoke.py\nmcp.py\noutput.py\npermissions.py\nproc.py\nproviders.py\nrequest.py\nresponse.py\nstructured.py\nusage.py\n```"
        },
        {
            "seq": 6,
            "timestamp": 1761947180351,
            "type": "step_finish"
        }
    ],
    "time": {
        "start": 1761947176380,
        "end": 1761947181182
    },
    "event_count": 6
}
```
</details>

### Synchronous Session

```python
from innerloop import Loop

loop = Loop(model="anthropic/claude-haiku-4-5")
with loop.session() as s:
    s("Please remember this word for me: avocado")
    response = s("What was the word I asked you to remember?")
```

```text
Output: The word you asked me to remember was **avocado**.
Duration: 4236 ms
Events: 3
```

<details>
  <summary>JSON Output</summary>

```json
{
    "session_id": "ses_5c3c39e98ffeZy12Mg9NGfkPEY",
    "input": "What was the word I asked you to remember?",
    "output": "The word you asked me to remember was **avocado**.",
    "attempts": 1,
    "events": [
        {
            "seq": 1,
            "timestamp": 1761947187603,
            "type": "step_start"
        },
        {
            "seq": 2,
            "timestamp": 1761947187926,
            "type": "text",
            "text": "The word you asked me to remember was **avocado**."
        },
        {
            "seq": 3,
            "timestamp": 1761947187983,
            "type": "step_finish"
        }
    ],
    "time": {
        "start": 1761947185898,
        "end": 1761947190134
    },
    "event_count": 3
}
```
</details>

### Asynchronous Session

```python
import asyncio
from innerloop import Loop

async def main():
    loop = Loop(model="anthropic/claude-haiku-4-5")
    async with loop.asession() as s:
        await s("Remember this number: 42")
        response = await s("What was the number?")

asyncio.run(main())
```

```text
Output: The number you asked me to remember was **42**.
Duration: 3613 ms
Events: 3
```

<details>
  <summary>JSON Output</summary>

```json
{
    "session_id": "ses_5c3c37b9bffemhsiMcIp7Izjls",
    "input": "What was the number?",
    "output": "The number you asked me to remember was **42**.",
    "attempts": 1,
    "events": [
        {
            "seq": 1,
            "timestamp": 1761947195220,
            "type": "step_start"
        },
        {
            "seq": 2,
            "timestamp": 1761947195416,
            "type": "text",
            "text": "The number you asked me to remember was **42**."
        },
        {
            "seq": 3,
            "timestamp": 1761947195491,
            "type": "step_finish"
        }
    ],
    "time": {
        "start": 1761947193730,
        "end": 1761947197343
    },
    "event_count": 3
}
```
</details>

### Structured Output

```python
from innerloop import Loop, allow
from pydantic import BaseModel

class HNStory(BaseModel):
    title: str
    url: str
    points: int
    comments: int

class HNTop(BaseModel):
    stories: list[HNStory]

loop = Loop(
    model="anthropic/claude-haiku-4-5",
    perms=allow(webfetch=True),
)

prompt = (
    "Using web search, find the current top 5 stories on Hacker News.\n"
    "Prefer news.ycombinator.com (front page or item pages). For each,\n"
    "return: title, url, points (int), comments (int). Output JSON with\n"
    "a 'stories' array. If counts are missing, open the item page and\n"
    "extract them. Keep titles unmodified.\n"
)
response = loop.run(prompt, response_format=HNTop)
```

```text
Output: Futurelock: A subtle risk in async Rust — https://rfd.shared.oxide.computer/rfd/0609
Duration: 9950 ms
Events: 7
```

<details>
  <summary>JSON Output</summary>

```json
{
    "session_id": "ses_5c3c35f7dffetSWcljxI1M3XaO",
    "input": "Using web search, find the current top 5 stories on Hacker News.\nPrefer news.ycombinator.com (front page or item pages). For each,\nreturn: title, url, points (int), comments (int). Output JSON with\na 'stories' array. If counts are missing, open the item page and\nextract them. Keep titles unmodified.\n",
    "output": {
        "stories": [
            {
                "title": "Futurelock: A subtle risk in async Rust",
                "url": "https://rfd.shared.oxide.computer/rfd/0609",
                "points": 119,
                "comments": 40
            },
            {
                "title": "Introducing architecture variants",
                "url": "https://discourse.ubuntu.com/t/introducing-architecture-variants-amd64v3-now-available-in-ubuntu-25-10/71312",
                "points": 127,
                "comments": 95
            },
            {
                "title": "A theoretical way to circumvent Android developer verification",
                "url": "https://enaix.github.io/2025/10/30/developer-verification.html",
                "points": 20,
                "comments": 3
            },
            {
                "title": "Hacking India's largest automaker: Tata Motors",
                "url": "https://eaton-works.com/2025/10/28/tata-motors-hack/",
                "points": 75,
                "comments": 24
            },
            {
                "title": "Leaker reveals which Pixels are vulnerable to Cellebrite phone hacking",
                "url": "https://arstechnica.com/gadgets/2025/10/leaker-reveals-which-pixels-are-vulnerable-to-cellebrite-phone-hacking/",
                "points": 81,
                "comments": 32
            }
        ]
    },
    "attempts": 1,
    "events": [
        {
            "seq": 1,
            "timestamp": 1761947199808,
            "type": "step_start"
        },
        {
            "seq": 2,
            "timestamp": 1761947199992,
            "type": "text",
            "text": "I'll fetch the top stories from Hacker News for you."
        },
        {
            "seq": 3,
            "timestamp": 1761947200523,
            "type": "tool_use",
            "output": "<html lang=\"en\" op=\"news\"><head><meta name=\"referrer\" content=\"origin\"><meta name=\"viewport\" content… (truncated)",
            "status": "completed",
            "tool": "webfetch"
        },
        {
            "seq": 4,
            "timestamp": 1761947200550,
            "type": "step_finish"
        },
        {
            "seq": 5,
            "timestamp": 1761947201907,
            "type": "step_start"
        },
        {
            "seq": 6,
            "timestamp": 1761947205636,
            "type": "text",
            "text": "Perfect! I have the top 5 stories from Hacker News with their points and comment counts. Let me verify the data is complete:\n\n1. Futurelock: A subtle risk in async Rust - 119 points, 40 comments\n2. Introducing architecture variants - 127 points, 95 comments\n3. A theoretical way to circumvent Android developer verification - 20 points, 3 comments\n4. Hacking India's largest automaker: Tata Motors - 75 points, 24 comments\n5. Leaker reveals which Pixels are vulnerable to Cellebrite phone hacking - 81 points, 32 comments\n\n```json\n{\n  \"stories\": [\n    {\n      \"title\": \"Futurelock: A subtle risk in async Rust\",\n      \"url\": \"https://rfd.shared.oxide.computer/rfd/0609\",\n      \"points\": 119,\n      \"comments\": 40\n    },\n    {\n      \"title\": \"Introducing architecture variants\",\n      \"url\": \"https://discourse.ubuntu.com/t/introducing-architecture-variants-amd64v3-now-available-in-ubuntu-25-10/71312\",\n      \"points\": 127,\n      \"comments\": 95\n    },\n    {\n      \"title\": \"A theoretical way to circumvent Android developer verification\",\n      \"url\": \"https://enaix.github.io/2025/10/30/developer-verification.html\",\n      \"points\": 20,\n      \"comments\": 3\n    },\n    {\n      \"title\": \"Hacking India's largest automaker: Tata Motors\",\n      \"url\": \"https://eaton-works.com/2025/10/28/tata-motors-hack/\",\n      \"points\": 75,\n      \"comments\": 24\n    },\n    {\n      \"title\": \"Leaker reveals which Pixels are vulnerable to Cellebrite phone hacking\",\n      \"url\": \"https://arstechnica.com/gadgets/2025/10/leaker-reveals-which-pixels-are-vulnerable-to-cellebrite-phone-hacking/\",\n      \"points\": 81,\n      \"comments\": 32\n    }\n  ]\n}\n```"
        },
        {
            "seq": 7,
            "timestamp": 1761947205701,
            "type": "step_finish"
        }
    ],
    "time": {
        "start": 1761947197346,
        "end": 1761947207296
    },
    "event_count": 7
}
```
</details>

### Providers — LM Studio (local)

```python
from innerloop import Loop

loop = Loop(
    model="lmstudio/google/gemma-3n-e4b",
    providers={
        "lmstudio": {
            "options": {"baseURL": "http://127.0.0.1:1234/v1"}
        },
    },
)

response = loop.run("In one concise sentence, say something creative about coding.")
```

```text
Output: Coding is like sculpting with logic, chipping away at the unknown until a coherent form emerges.
Duration: 4947 ms
Events: 3
```

<details>
  <summary>JSON Output</summary>

```json
{
    "session_id": "ses_5c3c33896ffec5XSCBBCzHTROG",
    "input": "In one concise sentence, say something creative about coding.",
    "output": "Coding is like sculpting with logic, chipping away at the unknown until a coherent form emerges.",
    "attempts": 1,
    "events": [
        {
            "seq": 1,
            "timestamp": 1761947211112,
            "type": "step_start"
        },
        {
            "seq": 2,
            "timestamp": 1761947211448,
            "type": "text",
            "text": "Coding is like sculpting with logic, chipping away at the unknown until a coherent form emerges."
        },
        {
            "seq": 3,
            "timestamp": 1761947211479,
            "type": "step_finish"
        }
    ],
    "time": {
        "start": 1761947207297,
        "end": 1761947212244
    },
    "event_count": 3
}
```
</details>

### MCP — Remote server (Context7)

```python
from innerloop import Loop, mcp

loop = Loop(
    model="anthropic/claude-sonnet-4-5",
    mcp=mcp(context7="https://mcp.context7.com/mcp"),
)
prompt = (
    "Use the context7 MCP server to search for FastAPI's latest "
    "async database patterns. Summarize in 2-3 sentences."
)
response = loop.run(prompt)
```

```text
Output: Based on FastAPI's latest documentation, async database patterns primarily use **dependency injecti…
Duration: 15167 ms
Events: 10
```

<details>
  <summary>JSON Output</summary>

```json
{
    "session_id": "ses_5c3c32543ffe8L35fByCQbDmEb",
    "input": "Use the context7 MCP server to search for FastAPI's latest async database patterns. Summarize in 2-3 sentences.",
    "output": "Based on FastAPI's latest documentation, async database patterns primarily use **dependency injection with `yield`** to manage database sessions. You create an async function that yields a database session (e.g., SQLModel/SQLAlchemy Session), which FastAPI automatically opens before request processing and closes after the response is sent, ensuring proper resource cleanup. The pattern leverages `SessionDep = Annotated[Session, Depends(get_session)]` for cleaner type hints and uses `async with` context managers for startup/shutdown operations like creating tables on application startup.",
    "attempts": 1,
    "events": [
        {
            "seq": 1,
            "timestamp": 1761947214785,
            "type": "step_start"
        },
        {
            "seq": 2,
            "timestamp": 1761947215606,
            "type": "text",
            "text": "I'll search for FastAPI's latest async database patterns using the Context7 MCP server."
        },
        {
            "seq": 3,
            "timestamp": 1761947216155,
            "type": "tool_use",
            "output": "Available Libraries (top matches):\n\nEach result includes:\n- Library ID: Context7-compatible identifi… (truncated)",
            "status": "completed",
            "tool": "context7_resolve_library_id"
        },
        {
            "seq": 4,
            "timestamp": 1761947216186,
            "type": "step_finish"
        },
        {
            "seq": 5,
            "timestamp": 1761947218217,
            "type": "step_start"
        },
        {
            "seq": 6,
            "timestamp": 1761947219639,
            "type": "tool_use",
            "output": "### FastAPI Dependency with `yield` and `async with` (Python)\n\nSource: https://github.com/fastapi/fa… (truncated)",
            "status": "completed",
            "tool": "context7_get_library_docs"
        },
        {
            "seq": 7,
            "timestamp": 1761947219665,
            "type": "step_finish"
        },
        {
            "seq": 8,
            "timestamp": 1761947222237,
            "type": "step_start"
        },
        {
            "seq": 9,
            "timestamp": 1761947225651,
            "type": "text",
            "text": "Based on FastAPI's latest documentation, async database patterns primarily use **dependency injection with `yield`** to manage database sessions. You create an async function that yields a database session (e.g., SQLModel/SQLAlchemy Session), which FastAPI automatically opens before request processing and closes after the response is sent, ensuring proper resource cleanup. The pattern leverages `SessionDep = Annotated[Session, Depends(get_session)]` for cleaner type hints and uses `async with` context managers for startup/shutdown operations like creating tables on application startup."
        },
        {
            "seq": 10,
            "timestamp": 1761947225704,
            "type": "step_finish"
        }
    ],
    "time": {
        "start": 1761947212244,
        "end": 1761947227411
    },
    "event_count": 10
}
```
</details>

### MCP — Local server (BioMCP)

```python
from innerloop import Loop, mcp

loop = Loop(
    model="anthropic/claude-sonnet-4-5",
    mcp=mcp(biomcp="uvx --from biomcp-python biomcp run"),
)
prompt = (
    "Using ONLY the biomcp MCP server tools, look up the BRAF V600E mutation. "
    "List associated cancer types and targeted drug therapies in 2-3 sentences."
)
response = loop.run(prompt, timeout=60.0)
```

```text
Output: Based on the biomcp search results, **BRAF V600E mutation is associated with multiple cancer types …
Duration: 40244 ms
Events: 17
```

<details>
  <summary>JSON Output</summary>

```json
{
    "session_id": "ses_5c3c2ea04ffexW3ZKYVCf4tuJI",
    "input": "Using ONLY the biomcp MCP server tools, look up the BRAF V600E mutation. List associated cancer types and targeted drug therapies in 2-3 sentences.",
    "output": "Based on the biomcp search results, **BRAF V600E mutation is associated with multiple cancer types including melanoma, thyroid cancer, colorectal cancer, and high-grade glioma**. **Targeted therapies include BRAF inhibitors (dabrafenib, vemurafenib) often combined with MEK inhibitors (trametinib), which specifically target the mutated BRAF protein and downstream MAPK pathway signaling**. The literature shows these combination therapies are used across different BRAF V600E-positive malignancies, though resistance mechanisms remain a clinical challenge.",
    "attempts": 1,
    "events": [
        {
            "seq": 1,
            "timestamp": 1761947230522,
            "type": "step_start"
        },
        {
            "seq": 2,
            "timestamp": 1761947231190,
            "type": "text",
            "text": "I'll look up the BRAF V600E mutation using the biomcp tools."
        },
        {
            "seq": 3,
            "timestamp": 1761947234217,
            "type": "tool_use",
            "output": "{\n  \"domain\": \"thinking\",\n  \"result\": \"Added thought 1 to main sequence. Progress: 1/3 thoughts. Nex… (truncated)",
            "status": "completed",
            "tool": "biomcp_think"
        },
        {
            "seq": 4,
            "timestamp": 1761947234312,
            "type": "step_finish"
        },
        {
            "seq": 5,
            "timestamp": 1761947236128,
            "type": "step_start"
        },
        {
            "seq": 6,
            "timestamp": 1761947239225,
            "type": "tool_use",
            "output": "{\n  \"domain\": \"thinking\",\n  \"result\": \"Added thought 2 to main sequence. Progress: 2/3 thoughts. Nex… (truncated)",
            "status": "completed",
            "tool": "biomcp_think"
        },
        {
            "seq": 7,
            "timestamp": 1761947239254,
            "type": "step_finish"
        },
        {
            "seq": 8,
            "timestamp": 1761947241093,
            "type": "step_start"
        },
        {
            "seq": 9,
            "timestamp": 1761947255261,
            "type": "tool_use",
            "output": "# Record 1\nId: chr10:g.32800350C>A\nScore: 1.0\nChrom: 10\n\n## Cadd\nLicense: http://bit.ly/2TIuab9\nPhre… (truncated)",
            "status": "completed",
            "tool": "biomcp_variant_searcher"
        },
        {
            "seq": 10,
            "timestamp": 1761947255927,
            "type": "tool_use",
            "output": "\n### cBioPortal Summary for BRAF\n- **Mutation Frequency**: 0.0% (0 mutations in 0 samples)\n- **Studi… (truncated)",
            "status": "completed",
            "tool": "biomcp_article_searcher"
        },
        {
            "seq": 11,
            "timestamp": 1761947255957,
            "type": "step_finish"
        },
        {
            "seq": 12,
            "timestamp": 1761947257893,
            "type": "step_start"
        },
        {
            "seq": 13,
            "timestamp": 1761947261508,
            "type": "tool_use",
            "output": "{\n  \"domain\": \"thinking\",\n  \"result\": \"Added thought 3 to main sequence. Progress: 3/3 thoughts. Thi… (truncated)",
            "status": "completed",
            "tool": "biomcp_think"
        },
        {
            "seq": 14,
            "timestamp": 1761947261558,
            "type": "step_finish"
        },
        {
            "seq": 15,
            "timestamp": 1761947263073,
            "type": "step_start"
        },
        {
            "seq": 16,
            "timestamp": 1761947266114,
            "type": "text",
            "text": "Based on the biomcp search results, **BRAF V600E mutation is associated with multiple cancer types including melanoma, thyroid cancer, colorectal cancer, and high-grade glioma**. **Targeted therapies include BRAF inhibitors (dabrafenib, vemurafenib) often combined with MEK inhibitors (trametinib), which specifically target the mutated BRAF protein and downstream MAPK pathway signaling**. The literature shows these combination therapies are used across different BRAF V600E-positive malignancies, though resistance mechanisms remain a clinical challenge."
        },
        {
            "seq": 17,
            "timestamp": 1761947266162,
            "type": "step_finish"
        }
    ],
    "time": {
        "start": 1761947227411,
        "end": 1761947267655
    },
    "event_count": 17
}
```
</details>
<!-- END USAGE -->

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "innerloop",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": "agent, ai, anthropic, automation, claude, codex, devtool, gemini, llm, openai, sdk",
    "author": null,
    "author_email": "Ian Maurer <imaurer@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/b7/db/5d185481609691c0d749dd2ec6f1f135a2577469e232aa4b1ef25c5f9e38/innerloop-0.0.0.dev2.tar.gz",
    "platform": null,
    "description": "# InnerLoop\n\nPython SDK for invoking the [OpenCode](https://opencode.ai/) coding CLI in a headless, non-interactive manner.\n\nSupports:\n\n- Synchronous and Asynchronous modes\n- Structured outputs using Pydantic models\n- Sessions for multiple invocations with shared context\n- Permission configuration for reading, writing, web, and bash tools\n- Configurable working directory per loop, session, or call\n\n## Quick Start\n\nSee [docs/guides/installing-opencode.md](docs/guides/installing-opencode.md) for installation and configuration.\n\nQuick install:\n```bash\nmake install_opencode  # Handles bun config automatically\n```\n\nOr manually:\n```bash\nnpm install -g opencode-ai\nexport OPENCODE_API_KEY=your-key\nopencode models\n```\n\n## Installation\n\nYou can install `innerloop` using `uv` (recommended) or `pip`.\n\n```bash\n# Using uv\nuv pip install innerloop\n\n# Using pip\npip install innerloop\n```\n\n## Prerequisites\n\n**InnerLoop requires the OpenCode CLI.** Install it and ensure `opencode` is on your PATH:\n\n```bash\nopencode --version\n```\n\nIf the command fails, install OpenCode from [opencode.ai](https://opencode.ai/) and add it to your PATH.\n\nSee the [OpenCode documentation](https://opencode.ai/docs/) for installation instructions.\n\n## Usage\n\n<!-- BEGIN USAGE -->\nWe summarize results below each snippet without `print()` in the examples.\nSummary shows: Output (first 100 chars), Duration (ms), Events.\n\nTo render yourself from a Response object:\n\n```python\ndef show(resp):\n    print('Output:', str(resp.output)[:100])\n    dur = (resp.time.end - resp.time.start) if resp.time else 0\n    print(f'Duration: {dur} ms')\n    print(f'Events: {resp.event_count}')\n    print(resp.model_dump_json(by_alias=True, indent=4))\n```\n\nSee: src/innerloop/response.py\n\n\n### Synchronous Run\n\n```python\nfrom innerloop import Loop\n\nloop = Loop(model=\"anthropic/claude-haiku-4-5\")\nresponse = loop.run(\"Say hello, one short line.\")\n```\n\n```text\nOutput: Hello! How can I help you with your code today?\nDuration: 3203 ms\nEvents: 3\n```\n\n<details>\n  <summary>JSON Output</summary>\n\n```json\n{\n    \"session_id\": \"ses_5c3c3ddbaffe1xdYfKWI7bvq3I\",\n    \"input\": \"Say hello, one short line.\",\n    \"output\": \"Hello! How can I help you with your code today?\",\n    \"attempts\": 1,\n    \"events\": [\n        {\n            \"seq\": 1,\n            \"timestamp\": 1761947166821,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 2,\n            \"timestamp\": 1761947166943,\n            \"type\": \"text\",\n            \"text\": \"Hello! How can I help you with your code today?\"\n        },\n        {\n            \"seq\": 3,\n            \"timestamp\": 1761947166991,\n            \"type\": \"step_finish\"\n        }\n    ],\n    \"time\": {\n        \"start\": 1761947165017,\n        \"end\": 1761947168220\n    },\n    \"event_count\": 3\n}\n```\n</details>\n\n### Asynchronous Run\n\n```python\nimport asyncio\nfrom innerloop import Loop\n\nasync def main():\n    loop = Loop(model=\"anthropic/claude-haiku-4-5\")\n    async with loop.asession() as s:\n        await s(\"Remember this number: 42\")\n        response = await s(\"What was the number?\")\n\nasyncio.run(main())\n```\n\n```text\nOutput: The number was 42.\nDuration: 4165 ms\nEvents: 3\n```\n\n<details>\n  <summary>JSON Output</summary>\n\n```json\n{\n    \"session_id\": \"ses_5c3c3d139ffeguE8rhOWW70ST2\",\n    \"input\": \"What was the number?\",\n    \"output\": \"The number was 42.\",\n    \"attempts\": 1,\n    \"events\": [\n        {\n            \"seq\": 1,\n            \"timestamp\": 1761947173793,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 2,\n            \"timestamp\": 1761947173971,\n            \"type\": \"text\",\n            \"text\": \"The number was 42.\"\n        },\n        {\n            \"seq\": 3,\n            \"timestamp\": 1761947174039,\n            \"type\": \"step_finish\"\n        }\n    ],\n    \"time\": {\n        \"start\": 1761947172214,\n        \"end\": 1761947176379\n    },\n    \"event_count\": 3\n}\n```\n</details>\n\n### Tool Use (with workdir)\n\n```python\nfrom innerloop import Loop, allow\n\nloop = Loop(\n    model=\"anthropic/claude-haiku-4-5\",\n    perms=allow(\"bash\"),\n)\nloop.default_workdir = \"src/innerloop\"\nresponse = loop.run(\"Use bash: ls -1\\nReturn only the raw command output.\")\n```\n\n```text\nOutput: ```\n__init__.py\n__pycache__\napi.py\nconfig.py\nerrors.py\nevents.py\nhelper.py\ninvoke.py\nmcp.py\noutput.\u2026\nDuration: 4802 ms\nEvents: 6\n```\n\n<details>\n  <summary>JSON Output</summary>\n\n```json\n{\n    \"session_id\": \"ses_5c3c3b15cffecPu1g9yj32ee7j\",\n    \"input\": \"Use bash: ls -1\\nReturn only the raw command output.\",\n    \"output\": \"```\\n__init__.py\\n__pycache__\\napi.py\\nconfig.py\\nerrors.py\\nevents.py\\nhelper.py\\ninvoke.py\\nmcp.py\\noutput.py\\npermissions.py\\nproc.py\\nproviders.py\\nrequest.py\\nresponse.py\\nstructured.py\\nusage.py\\n```\",\n    \"attempts\": 1,\n    \"events\": [\n        {\n            \"seq\": 1,\n            \"timestamp\": 1761947178427,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 2,\n            \"timestamp\": 1761947178973,\n            \"type\": \"tool_use\",\n            \"output\": \"__init__.py\\n__pycache__\\napi.py\\nconfig.py\\nerrors.py\\nevents.py\\nhelper.py\\ninvoke.py\\nmcp.py\\noutput.py\\npe\u2026 (truncated)\",\n            \"status\": \"completed\",\n            \"tool\": \"bash\"\n        },\n        {\n            \"seq\": 3,\n            \"timestamp\": 1761947179001,\n            \"type\": \"step_finish\"\n        },\n        {\n            \"seq\": 4,\n            \"timestamp\": 1761947180024,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 5,\n            \"timestamp\": 1761947180306,\n            \"type\": \"text\",\n            \"text\": \"```\\n__init__.py\\n__pycache__\\napi.py\\nconfig.py\\nerrors.py\\nevents.py\\nhelper.py\\ninvoke.py\\nmcp.py\\noutput.py\\npermissions.py\\nproc.py\\nproviders.py\\nrequest.py\\nresponse.py\\nstructured.py\\nusage.py\\n```\"\n        },\n        {\n            \"seq\": 6,\n            \"timestamp\": 1761947180351,\n            \"type\": \"step_finish\"\n        }\n    ],\n    \"time\": {\n        \"start\": 1761947176380,\n        \"end\": 1761947181182\n    },\n    \"event_count\": 6\n}\n```\n</details>\n\n### Synchronous Session\n\n```python\nfrom innerloop import Loop\n\nloop = Loop(model=\"anthropic/claude-haiku-4-5\")\nwith loop.session() as s:\n    s(\"Please remember this word for me: avocado\")\n    response = s(\"What was the word I asked you to remember?\")\n```\n\n```text\nOutput: The word you asked me to remember was **avocado**.\nDuration: 4236 ms\nEvents: 3\n```\n\n<details>\n  <summary>JSON Output</summary>\n\n```json\n{\n    \"session_id\": \"ses_5c3c39e98ffeZy12Mg9NGfkPEY\",\n    \"input\": \"What was the word I asked you to remember?\",\n    \"output\": \"The word you asked me to remember was **avocado**.\",\n    \"attempts\": 1,\n    \"events\": [\n        {\n            \"seq\": 1,\n            \"timestamp\": 1761947187603,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 2,\n            \"timestamp\": 1761947187926,\n            \"type\": \"text\",\n            \"text\": \"The word you asked me to remember was **avocado**.\"\n        },\n        {\n            \"seq\": 3,\n            \"timestamp\": 1761947187983,\n            \"type\": \"step_finish\"\n        }\n    ],\n    \"time\": {\n        \"start\": 1761947185898,\n        \"end\": 1761947190134\n    },\n    \"event_count\": 3\n}\n```\n</details>\n\n### Asynchronous Session\n\n```python\nimport asyncio\nfrom innerloop import Loop\n\nasync def main():\n    loop = Loop(model=\"anthropic/claude-haiku-4-5\")\n    async with loop.asession() as s:\n        await s(\"Remember this number: 42\")\n        response = await s(\"What was the number?\")\n\nasyncio.run(main())\n```\n\n```text\nOutput: The number you asked me to remember was **42**.\nDuration: 3613 ms\nEvents: 3\n```\n\n<details>\n  <summary>JSON Output</summary>\n\n```json\n{\n    \"session_id\": \"ses_5c3c37b9bffemhsiMcIp7Izjls\",\n    \"input\": \"What was the number?\",\n    \"output\": \"The number you asked me to remember was **42**.\",\n    \"attempts\": 1,\n    \"events\": [\n        {\n            \"seq\": 1,\n            \"timestamp\": 1761947195220,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 2,\n            \"timestamp\": 1761947195416,\n            \"type\": \"text\",\n            \"text\": \"The number you asked me to remember was **42**.\"\n        },\n        {\n            \"seq\": 3,\n            \"timestamp\": 1761947195491,\n            \"type\": \"step_finish\"\n        }\n    ],\n    \"time\": {\n        \"start\": 1761947193730,\n        \"end\": 1761947197343\n    },\n    \"event_count\": 3\n}\n```\n</details>\n\n### Structured Output\n\n```python\nfrom innerloop import Loop, allow\nfrom pydantic import BaseModel\n\nclass HNStory(BaseModel):\n    title: str\n    url: str\n    points: int\n    comments: int\n\nclass HNTop(BaseModel):\n    stories: list[HNStory]\n\nloop = Loop(\n    model=\"anthropic/claude-haiku-4-5\",\n    perms=allow(webfetch=True),\n)\n\nprompt = (\n    \"Using web search, find the current top 5 stories on Hacker News.\\n\"\n    \"Prefer news.ycombinator.com (front page or item pages). For each,\\n\"\n    \"return: title, url, points (int), comments (int). Output JSON with\\n\"\n    \"a 'stories' array. If counts are missing, open the item page and\\n\"\n    \"extract them. Keep titles unmodified.\\n\"\n)\nresponse = loop.run(prompt, response_format=HNTop)\n```\n\n```text\nOutput: Futurelock: A subtle risk in async Rust \u2014 https://rfd.shared.oxide.computer/rfd/0609\nDuration: 9950 ms\nEvents: 7\n```\n\n<details>\n  <summary>JSON Output</summary>\n\n```json\n{\n    \"session_id\": \"ses_5c3c35f7dffetSWcljxI1M3XaO\",\n    \"input\": \"Using web search, find the current top 5 stories on Hacker News.\\nPrefer news.ycombinator.com (front page or item pages). For each,\\nreturn: title, url, points (int), comments (int). Output JSON with\\na 'stories' array. If counts are missing, open the item page and\\nextract them. Keep titles unmodified.\\n\",\n    \"output\": {\n        \"stories\": [\n            {\n                \"title\": \"Futurelock: A subtle risk in async Rust\",\n                \"url\": \"https://rfd.shared.oxide.computer/rfd/0609\",\n                \"points\": 119,\n                \"comments\": 40\n            },\n            {\n                \"title\": \"Introducing architecture variants\",\n                \"url\": \"https://discourse.ubuntu.com/t/introducing-architecture-variants-amd64v3-now-available-in-ubuntu-25-10/71312\",\n                \"points\": 127,\n                \"comments\": 95\n            },\n            {\n                \"title\": \"A theoretical way to circumvent Android developer verification\",\n                \"url\": \"https://enaix.github.io/2025/10/30/developer-verification.html\",\n                \"points\": 20,\n                \"comments\": 3\n            },\n            {\n                \"title\": \"Hacking India's largest automaker: Tata Motors\",\n                \"url\": \"https://eaton-works.com/2025/10/28/tata-motors-hack/\",\n                \"points\": 75,\n                \"comments\": 24\n            },\n            {\n                \"title\": \"Leaker reveals which Pixels are vulnerable to Cellebrite phone hacking\",\n                \"url\": \"https://arstechnica.com/gadgets/2025/10/leaker-reveals-which-pixels-are-vulnerable-to-cellebrite-phone-hacking/\",\n                \"points\": 81,\n                \"comments\": 32\n            }\n        ]\n    },\n    \"attempts\": 1,\n    \"events\": [\n        {\n            \"seq\": 1,\n            \"timestamp\": 1761947199808,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 2,\n            \"timestamp\": 1761947199992,\n            \"type\": \"text\",\n            \"text\": \"I'll fetch the top stories from Hacker News for you.\"\n        },\n        {\n            \"seq\": 3,\n            \"timestamp\": 1761947200523,\n            \"type\": \"tool_use\",\n            \"output\": \"<html lang=\\\"en\\\" op=\\\"news\\\"><head><meta name=\\\"referrer\\\" content=\\\"origin\\\"><meta name=\\\"viewport\\\" content\u2026 (truncated)\",\n            \"status\": \"completed\",\n            \"tool\": \"webfetch\"\n        },\n        {\n            \"seq\": 4,\n            \"timestamp\": 1761947200550,\n            \"type\": \"step_finish\"\n        },\n        {\n            \"seq\": 5,\n            \"timestamp\": 1761947201907,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 6,\n            \"timestamp\": 1761947205636,\n            \"type\": \"text\",\n            \"text\": \"Perfect! I have the top 5 stories from Hacker News with their points and comment counts. Let me verify the data is complete:\\n\\n1. Futurelock: A subtle risk in async Rust - 119 points, 40 comments\\n2. Introducing architecture variants - 127 points, 95 comments\\n3. A theoretical way to circumvent Android developer verification - 20 points, 3 comments\\n4. Hacking India's largest automaker: Tata Motors - 75 points, 24 comments\\n5. Leaker reveals which Pixels are vulnerable to Cellebrite phone hacking - 81 points, 32 comments\\n\\n```json\\n{\\n  \\\"stories\\\": [\\n    {\\n      \\\"title\\\": \\\"Futurelock: A subtle risk in async Rust\\\",\\n      \\\"url\\\": \\\"https://rfd.shared.oxide.computer/rfd/0609\\\",\\n      \\\"points\\\": 119,\\n      \\\"comments\\\": 40\\n    },\\n    {\\n      \\\"title\\\": \\\"Introducing architecture variants\\\",\\n      \\\"url\\\": \\\"https://discourse.ubuntu.com/t/introducing-architecture-variants-amd64v3-now-available-in-ubuntu-25-10/71312\\\",\\n      \\\"points\\\": 127,\\n      \\\"comments\\\": 95\\n    },\\n    {\\n      \\\"title\\\": \\\"A theoretical way to circumvent Android developer verification\\\",\\n      \\\"url\\\": \\\"https://enaix.github.io/2025/10/30/developer-verification.html\\\",\\n      \\\"points\\\": 20,\\n      \\\"comments\\\": 3\\n    },\\n    {\\n      \\\"title\\\": \\\"Hacking India's largest automaker: Tata Motors\\\",\\n      \\\"url\\\": \\\"https://eaton-works.com/2025/10/28/tata-motors-hack/\\\",\\n      \\\"points\\\": 75,\\n      \\\"comments\\\": 24\\n    },\\n    {\\n      \\\"title\\\": \\\"Leaker reveals which Pixels are vulnerable to Cellebrite phone hacking\\\",\\n      \\\"url\\\": \\\"https://arstechnica.com/gadgets/2025/10/leaker-reveals-which-pixels-are-vulnerable-to-cellebrite-phone-hacking/\\\",\\n      \\\"points\\\": 81,\\n      \\\"comments\\\": 32\\n    }\\n  ]\\n}\\n```\"\n        },\n        {\n            \"seq\": 7,\n            \"timestamp\": 1761947205701,\n            \"type\": \"step_finish\"\n        }\n    ],\n    \"time\": {\n        \"start\": 1761947197346,\n        \"end\": 1761947207296\n    },\n    \"event_count\": 7\n}\n```\n</details>\n\n### Providers \u2014 LM Studio (local)\n\n```python\nfrom innerloop import Loop\n\nloop = Loop(\n    model=\"lmstudio/google/gemma-3n-e4b\",\n    providers={\n        \"lmstudio\": {\n            \"options\": {\"baseURL\": \"http://127.0.0.1:1234/v1\"}\n        },\n    },\n)\n\nresponse = loop.run(\"In one concise sentence, say something creative about coding.\")\n```\n\n```text\nOutput: Coding is like sculpting with logic, chipping away at the unknown until a coherent form emerges.\nDuration: 4947 ms\nEvents: 3\n```\n\n<details>\n  <summary>JSON Output</summary>\n\n```json\n{\n    \"session_id\": \"ses_5c3c33896ffec5XSCBBCzHTROG\",\n    \"input\": \"In one concise sentence, say something creative about coding.\",\n    \"output\": \"Coding is like sculpting with logic, chipping away at the unknown until a coherent form emerges.\",\n    \"attempts\": 1,\n    \"events\": [\n        {\n            \"seq\": 1,\n            \"timestamp\": 1761947211112,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 2,\n            \"timestamp\": 1761947211448,\n            \"type\": \"text\",\n            \"text\": \"Coding is like sculpting with logic, chipping away at the unknown until a coherent form emerges.\"\n        },\n        {\n            \"seq\": 3,\n            \"timestamp\": 1761947211479,\n            \"type\": \"step_finish\"\n        }\n    ],\n    \"time\": {\n        \"start\": 1761947207297,\n        \"end\": 1761947212244\n    },\n    \"event_count\": 3\n}\n```\n</details>\n\n### MCP \u2014 Remote server (Context7)\n\n```python\nfrom innerloop import Loop, mcp\n\nloop = Loop(\n    model=\"anthropic/claude-sonnet-4-5\",\n    mcp=mcp(context7=\"https://mcp.context7.com/mcp\"),\n)\nprompt = (\n    \"Use the context7 MCP server to search for FastAPI's latest \"\n    \"async database patterns. Summarize in 2-3 sentences.\"\n)\nresponse = loop.run(prompt)\n```\n\n```text\nOutput: Based on FastAPI's latest documentation, async database patterns primarily use **dependency injecti\u2026\nDuration: 15167 ms\nEvents: 10\n```\n\n<details>\n  <summary>JSON Output</summary>\n\n```json\n{\n    \"session_id\": \"ses_5c3c32543ffe8L35fByCQbDmEb\",\n    \"input\": \"Use the context7 MCP server to search for FastAPI's latest async database patterns. Summarize in 2-3 sentences.\",\n    \"output\": \"Based on FastAPI's latest documentation, async database patterns primarily use **dependency injection with `yield`** to manage database sessions. You create an async function that yields a database session (e.g., SQLModel/SQLAlchemy Session), which FastAPI automatically opens before request processing and closes after the response is sent, ensuring proper resource cleanup. The pattern leverages `SessionDep = Annotated[Session, Depends(get_session)]` for cleaner type hints and uses `async with` context managers for startup/shutdown operations like creating tables on application startup.\",\n    \"attempts\": 1,\n    \"events\": [\n        {\n            \"seq\": 1,\n            \"timestamp\": 1761947214785,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 2,\n            \"timestamp\": 1761947215606,\n            \"type\": \"text\",\n            \"text\": \"I'll search for FastAPI's latest async database patterns using the Context7 MCP server.\"\n        },\n        {\n            \"seq\": 3,\n            \"timestamp\": 1761947216155,\n            \"type\": \"tool_use\",\n            \"output\": \"Available Libraries (top matches):\\n\\nEach result includes:\\n- Library ID: Context7-compatible identifi\u2026 (truncated)\",\n            \"status\": \"completed\",\n            \"tool\": \"context7_resolve_library_id\"\n        },\n        {\n            \"seq\": 4,\n            \"timestamp\": 1761947216186,\n            \"type\": \"step_finish\"\n        },\n        {\n            \"seq\": 5,\n            \"timestamp\": 1761947218217,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 6,\n            \"timestamp\": 1761947219639,\n            \"type\": \"tool_use\",\n            \"output\": \"### FastAPI Dependency with `yield` and `async with` (Python)\\n\\nSource: https://github.com/fastapi/fa\u2026 (truncated)\",\n            \"status\": \"completed\",\n            \"tool\": \"context7_get_library_docs\"\n        },\n        {\n            \"seq\": 7,\n            \"timestamp\": 1761947219665,\n            \"type\": \"step_finish\"\n        },\n        {\n            \"seq\": 8,\n            \"timestamp\": 1761947222237,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 9,\n            \"timestamp\": 1761947225651,\n            \"type\": \"text\",\n            \"text\": \"Based on FastAPI's latest documentation, async database patterns primarily use **dependency injection with `yield`** to manage database sessions. You create an async function that yields a database session (e.g., SQLModel/SQLAlchemy Session), which FastAPI automatically opens before request processing and closes after the response is sent, ensuring proper resource cleanup. The pattern leverages `SessionDep = Annotated[Session, Depends(get_session)]` for cleaner type hints and uses `async with` context managers for startup/shutdown operations like creating tables on application startup.\"\n        },\n        {\n            \"seq\": 10,\n            \"timestamp\": 1761947225704,\n            \"type\": \"step_finish\"\n        }\n    ],\n    \"time\": {\n        \"start\": 1761947212244,\n        \"end\": 1761947227411\n    },\n    \"event_count\": 10\n}\n```\n</details>\n\n### MCP \u2014 Local server (BioMCP)\n\n```python\nfrom innerloop import Loop, mcp\n\nloop = Loop(\n    model=\"anthropic/claude-sonnet-4-5\",\n    mcp=mcp(biomcp=\"uvx --from biomcp-python biomcp run\"),\n)\nprompt = (\n    \"Using ONLY the biomcp MCP server tools, look up the BRAF V600E mutation. \"\n    \"List associated cancer types and targeted drug therapies in 2-3 sentences.\"\n)\nresponse = loop.run(prompt, timeout=60.0)\n```\n\n```text\nOutput: Based on the biomcp search results, **BRAF V600E mutation is associated with multiple cancer types \u2026\nDuration: 40244 ms\nEvents: 17\n```\n\n<details>\n  <summary>JSON Output</summary>\n\n```json\n{\n    \"session_id\": \"ses_5c3c2ea04ffexW3ZKYVCf4tuJI\",\n    \"input\": \"Using ONLY the biomcp MCP server tools, look up the BRAF V600E mutation. List associated cancer types and targeted drug therapies in 2-3 sentences.\",\n    \"output\": \"Based on the biomcp search results, **BRAF V600E mutation is associated with multiple cancer types including melanoma, thyroid cancer, colorectal cancer, and high-grade glioma**. **Targeted therapies include BRAF inhibitors (dabrafenib, vemurafenib) often combined with MEK inhibitors (trametinib), which specifically target the mutated BRAF protein and downstream MAPK pathway signaling**. The literature shows these combination therapies are used across different BRAF V600E-positive malignancies, though resistance mechanisms remain a clinical challenge.\",\n    \"attempts\": 1,\n    \"events\": [\n        {\n            \"seq\": 1,\n            \"timestamp\": 1761947230522,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 2,\n            \"timestamp\": 1761947231190,\n            \"type\": \"text\",\n            \"text\": \"I'll look up the BRAF V600E mutation using the biomcp tools.\"\n        },\n        {\n            \"seq\": 3,\n            \"timestamp\": 1761947234217,\n            \"type\": \"tool_use\",\n            \"output\": \"{\\n  \\\"domain\\\": \\\"thinking\\\",\\n  \\\"result\\\": \\\"Added thought 1 to main sequence. Progress: 1/3 thoughts. Nex\u2026 (truncated)\",\n            \"status\": \"completed\",\n            \"tool\": \"biomcp_think\"\n        },\n        {\n            \"seq\": 4,\n            \"timestamp\": 1761947234312,\n            \"type\": \"step_finish\"\n        },\n        {\n            \"seq\": 5,\n            \"timestamp\": 1761947236128,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 6,\n            \"timestamp\": 1761947239225,\n            \"type\": \"tool_use\",\n            \"output\": \"{\\n  \\\"domain\\\": \\\"thinking\\\",\\n  \\\"result\\\": \\\"Added thought 2 to main sequence. Progress: 2/3 thoughts. Nex\u2026 (truncated)\",\n            \"status\": \"completed\",\n            \"tool\": \"biomcp_think\"\n        },\n        {\n            \"seq\": 7,\n            \"timestamp\": 1761947239254,\n            \"type\": \"step_finish\"\n        },\n        {\n            \"seq\": 8,\n            \"timestamp\": 1761947241093,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 9,\n            \"timestamp\": 1761947255261,\n            \"type\": \"tool_use\",\n            \"output\": \"# Record 1\\nId: chr10:g.32800350C>A\\nScore: 1.0\\nChrom: 10\\n\\n## Cadd\\nLicense: http://bit.ly/2TIuab9\\nPhre\u2026 (truncated)\",\n            \"status\": \"completed\",\n            \"tool\": \"biomcp_variant_searcher\"\n        },\n        {\n            \"seq\": 10,\n            \"timestamp\": 1761947255927,\n            \"type\": \"tool_use\",\n            \"output\": \"\\n### cBioPortal Summary for BRAF\\n- **Mutation Frequency**: 0.0% (0 mutations in 0 samples)\\n- **Studi\u2026 (truncated)\",\n            \"status\": \"completed\",\n            \"tool\": \"biomcp_article_searcher\"\n        },\n        {\n            \"seq\": 11,\n            \"timestamp\": 1761947255957,\n            \"type\": \"step_finish\"\n        },\n        {\n            \"seq\": 12,\n            \"timestamp\": 1761947257893,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 13,\n            \"timestamp\": 1761947261508,\n            \"type\": \"tool_use\",\n            \"output\": \"{\\n  \\\"domain\\\": \\\"thinking\\\",\\n  \\\"result\\\": \\\"Added thought 3 to main sequence. Progress: 3/3 thoughts. Thi\u2026 (truncated)\",\n            \"status\": \"completed\",\n            \"tool\": \"biomcp_think\"\n        },\n        {\n            \"seq\": 14,\n            \"timestamp\": 1761947261558,\n            \"type\": \"step_finish\"\n        },\n        {\n            \"seq\": 15,\n            \"timestamp\": 1761947263073,\n            \"type\": \"step_start\"\n        },\n        {\n            \"seq\": 16,\n            \"timestamp\": 1761947266114,\n            \"type\": \"text\",\n            \"text\": \"Based on the biomcp search results, **BRAF V600E mutation is associated with multiple cancer types including melanoma, thyroid cancer, colorectal cancer, and high-grade glioma**. **Targeted therapies include BRAF inhibitors (dabrafenib, vemurafenib) often combined with MEK inhibitors (trametinib), which specifically target the mutated BRAF protein and downstream MAPK pathway signaling**. The literature shows these combination therapies are used across different BRAF V600E-positive malignancies, though resistance mechanisms remain a clinical challenge.\"\n        },\n        {\n            \"seq\": 17,\n            \"timestamp\": 1761947266162,\n            \"type\": \"step_finish\"\n        }\n    ],\n    \"time\": {\n        \"start\": 1761947227411,\n        \"end\": 1761947267655\n    },\n    \"event_count\": 17\n}\n```\n</details>\n<!-- END USAGE -->\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "LLM in a loop with tools, MCP, sessions, and structured outputs.",
    "version": "0.0.0.dev2",
    "project_urls": {
        "Changelog": "https://botassembly.org/innerloop/changelog/",
        "Documentation": "https://botassembly.org/innerloop",
        "Homepage": "https://github.com/botassembly/innerloop",
        "Issues": "https://github.com/botassembly/innerloop/issues",
        "Repository": "https://github.com/botassembly/innerloop"
    },
    "split_keywords": [
        "agent",
        " ai",
        " anthropic",
        " automation",
        " claude",
        " codex",
        " devtool",
        " gemini",
        " llm",
        " openai",
        " sdk"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "e7bbdbade7331552cdbb215d776c2d3119b8c26b7fdf5efd2080f66bc58e2142",
                "md5": "3de98c04360d4c3859e7651c340260a4",
                "sha256": "05821d8dda5ceeacf4a61f7d527a71b170f2a75b5c8c25d756c0484fa16b811b"
            },
            "downloads": -1,
            "filename": "innerloop-0.0.0.dev2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "3de98c04360d4c3859e7651c340260a4",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 29597,
            "upload_time": "2025-11-02T17:50:47",
            "upload_time_iso_8601": "2025-11-02T17:50:47.132429Z",
            "url": "https://files.pythonhosted.org/packages/e7/bb/dbade7331552cdbb215d776c2d3119b8c26b7fdf5efd2080f66bc58e2142/innerloop-0.0.0.dev2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "b7db5d185481609691c0d749dd2ec6f1f135a2577469e232aa4b1ef25c5f9e38",
                "md5": "e211cc8e843164c32c81e629f0c44c68",
                "sha256": "94a68f0b524f86fab25e5467b1d87c88e988b71fdb58adae61086fcdf31174ec"
            },
            "downloads": -1,
            "filename": "innerloop-0.0.0.dev2.tar.gz",
            "has_sig": false,
            "md5_digest": "e211cc8e843164c32c81e629f0c44c68",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 27790,
            "upload_time": "2025-11-02T17:50:48",
            "upload_time_iso_8601": "2025-11-02T17:50:48.722247Z",
            "url": "https://files.pythonhosted.org/packages/b7/db/5d185481609691c0d749dd2ec6f1f135a2577469e232aa4b1ef25c5f9e38/innerloop-0.0.0.dev2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-11-02 17:50:48",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "botassembly",
    "github_project": "innerloop",
    "github_not_found": true,
    "lcname": "innerloop"
}
        
Elapsed time: 1.60942s