Metadata-Version: 2.4
Name: komodor-agentops
Version: 0.3.2
Summary: Python SDK for AgentOps observability.
Author: Komodor Ltd.
License-Expression: LicenseRef-Komodor-Proprietary
Project-URL: Homepage, https://agentops.komodor.com
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: agentops-otel~=0.3.0
Requires-Dist: agentops-rpc~=0.3.0
Requires-Dist: croniter>=2.0
Requires-Dist: httpx
Requires-Dist: pydantic>=2.0
Requires-Dist: pydantic-settings>=2.0
Requires-Dist: python-frontmatter>=1.2.0
Requires-Dist: pyyaml>=6.0.3
Provides-Extra: server
Provides-Extra: langchain
Requires-Dist: langchain-core; extra == "langchain"
Provides-Extra: claude-code
Requires-Dist: claude-agent-sdk>=0.2.82; extra == "claude-code"
Provides-Extra: adk
Requires-Dist: google-adk[extensions]>=2.2.0; extra == "adk"
Provides-Extra: agno
Requires-Dist: agno<3,>=2.6; extra == "agno"
Requires-Dist: anthropic>=0.40; extra == "agno"
Provides-Extra: all
Requires-Dist: komodor-agentops[adk,agno,claude-code,langchain]; extra == "all"
Dynamic: license-file

# komodor-agentops

Python SDK for AgentOps observability, including the worker runtime, with
optional extras for framework integrations (LangChain, Claude Agent SDK, ADK,
Agno).

## Install

```bash
pip install komodor-agentops

# With framework adapters
pip install komodor-agentops[langchain]
pip install komodor-agentops[claude-code]
pip install komodor-agentops[adk]
pip install komodor-agentops[agno]
pip install komodor-agentops[all]           # Everything
```

## Quick Start

### @observe() decorator

Wrap functions to emit span events automatically:

```python
from komodor_agentops import observe

@observe(name="summarize", as_type="llm")
async def summarize(text: str) -> str:
    ...
```

### AgentOps client (lightweight event buffer)

```python
from komodor_agentops import AgentOps

client = AgentOps(agent_id="my-agent", endpoint="http://localhost:8000")
await client.log("Processing started", run_id="run_1")
await client.flush()
```

### AgentOpsWorker — the whole of a worker's `main()`

A worker takes runs and returns results. It serves no HTTP and needs no inbound network access: it
connects out to the control plane, is told when work is available, and claims it.

```python
from pathlib import Path

from komodor_agentops import AgentOpsWorker, AgentSpec, Run

spec = AgentSpec.from_dir(
    Path(__file__).parent,
    agent_card={"skills": [{"id": "search", "name": "search"}]},
)

async def on_run(run: Run) -> dict:
    return {"answer": f"handled {run.input.get('prompt', '')}"}

def main() -> None:
    AgentOpsWorker(agent=spec, on_run=on_run).run()
```

`run.input` is the payload the run was created with; the dict you return becomes the run's output.
Raise to fail the run — that is `on_run`'s only failure channel, and the error is recorded on the run.

**No host or port.** Nothing dials your worker, so there is nothing to bind. A deployment needs egress
to the control plane and no Service, Ingress or open port; the customer namespace chart denies inbound
traffic by default.

**A worker built against `komodor-agentops` 0.1.x may instead be using `run_worker(agent, handler)` with a
handler taking an `A2AMessage` and returning an `A2ATask`, served on a port.** That runtime is gone: it
served an A2A app nothing dials any more. Move the handler to `on_run=` — it receives a `Run` and returns a
mapping — and construct `AgentOpsWorker` from the package root as above. `Run.input` is the whole payload
the message's parts used to carry, and the answer's human-readable half is the reserved `text` key rather
than an artifact.

The standard AgentOps agent layout is:

```text
my_agent/
  agent-spec.yaml
  agent.md
  worker.py
  skills/
    triage.md
    rca/SKILL.md
```

`AgentSpec.from_dir(Path(__file__).parent, ...)` requires `agent-spec.yaml`, then loads `agent.md` and `skills/` automatically. The worker registers loaded skills during heartbeat, and SDK-owned LLM adapters can prepend the agent context at invocation time.

```yaml
schema_version: 1
agent_id: my-agent
name: My Agent
description: Does useful work.
owner: AgentOps
repo: https://github.com/komodorio/agentops
source_path: packages/workers/my_agent
labels:
  category: example
```

`agent-spec.yaml` can also declare `triggers` — synced to the control plane on heartbeat and shown on the Fleet → Triggers surface. Supported types are `schedule` (cron), `webhook` (inbound HTTP endpoint), and `slack_channel` (subscribe the agent to Slack channels by NAME, not ID — the control plane materialises a routing rule per channel on heartbeat and dispatches matching messages; the agent is invoked when @mentioned in one of those channels):

```yaml
triggers:
  - id: incidents-sub
    type: slack_channel
    name: Incident channels
    channels:
      - "#incidents" # normalized to "incidents" — lowercase, no "#"
      - alerts
```

To make a worker available in the AgentOps Chat UI, advertise chat capability on its agent card:

```python
spec = AgentSpec(
    agent_id="my-chat-agent",
    agent_card={
        "name": "My Chat Agent",
        "capabilities": {"chat": True, "ask": True, "streaming": True},
    },
)
```

A chat run arrives like any other run: `run.input` carries `messages` (the conversation history), `model`,
and `prompt` (the latest user text). There are no A2A message parts to unpack — the control plane creates a
run and your worker claims it, so a chat handler reads the same `run.input` dict as every other handler.

To stream the answer as it is produced rather than returning it whole, emit it through the ambient client;
the control plane relays each new piece of the run's answer text to the browser as it appears.

## Event Hooks — rewriting or withholding telemetry before it leaves

Subclass `AgentOpsHook` and pass instances to the worker. Each callback may return the event unchanged,
return a modified one, or return `None` to **drop it** — so a tool payload carrying customer data can be
rewritten or withheld, and nothing downstream sees the original. `before_flush` does the same for a whole
batch; `after_flush` reports what was sent.

Every event this worker records crosses one place on its way out, so a hook sees all of it — including the
spans the adapters record ambiently, which never pass through your code.

```python
from komodor_agentops import AgentOpsWorker
from komodor_agentops.core.events import AgentOpsEvent
from komodor_agentops.core.hooks import AgentOpsHook


class RedactToolInput(AgentOpsHook):
    async def before_tool(self, event):
        return AgentOpsEvent(**{**vars(event), "payload": {"input": "[redacted]"}})


AgentOpsWorker(agent=spec, on_run=on_run, hooks=[RedactToolInput()]).run()
```

A hook that raises is logged and its event passes through unchanged, so a buggy rule cannot cost a run its
whole transcript. Pass `strict_hooks=True` to invert that: for a hook whose job is to *withhold* data,
passing the event through on failure ships exactly what it existed to remove, so the failure should
propagate instead.

## Framework Adapters

### LangChain

Spans for each chain, model and tool call, nested under the run's own span. It needs no client: the
events go to the ambient buffer the worker's flush cycle already drains, so it only produces spans
inside a run scope.

```python
from komodor_agentops.adapters.langchain import ContextCallbackHandler

chain.invoke(input, config={"callbacks": [ContextCallbackHandler()]})
```

### Claude Code

SDK hooks for `claude-agent-sdk`:

```python
from komodor_agentops.adapters.claude_code.sdk_hooks import agent_ops_hooks_for_run

hooks = agent_ops_hooks_for_run("run_1")
```

## Architecture

```
komodor-agentops
  agentops-rpc, agentops-otel (wire types + OTel bootstrap)
  httpx, pydantic, pydantic-settings, croniter, pyyaml, python-frontmatter
    |
    +-- [langchain]   -> langchain-core
    +-- [claude-code] -> claude-agent-sdk
    +-- [adk]         -> google-adk
    +-- [agno]        -> agno, anthropic
    +-- [server]      -> no-op alias (kept so existing [server] refs resolve; the SDK
                         serves no HTTP, so there is nothing for it to pull in)
```

## Package Structure

```
src/komodor_agentops/
  __init__.py       # Public API — the root is the API; submodule paths are internal
  worker_runtime.py # AgentOpsWorker: connect, register, claim, run, report
  worker_client.py  # The worker protocol, and the run scope
  messaging.py      # The transport's composition root (downlink + uplink)
  client.py         # Event buffer + flush
  py.typed          # PEP 561 marker
  transport/        # The byte-level connection (SSE, reconnect)
  bus/              # Frames: the envelope, the router, the uplink
  machinery/        # Delivery: batching, retry, sequencing
  core/             # Context, events, secrets, types, @observe
  worker/           # Run contract, liveness, local runner, log forwarding
  adapters/         # Claude Code, ADK, Agno, LangChain, nanobot
```
