Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
version: 2
updates:
- package-ecosystem: pip
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 3
groups:
routine-python:
patterns:
- "*"
update-types:
- minor
- patch

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 3
groups:
routine-actions:
patterns:
- "*"
update-types:
- minor
- patch
116 changes: 116 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
tests:
name: Tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install package and test dependencies
run: python -m pip install --disable-pip-version-check ".[test]"
- name: Run test suite
run: python -m pytest -q

framework-integrations:
name: Integration (${{ matrix.framework }})
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- framework: langgraph
extra: langgraph
test-file: tests/integration/test_langgraph_smoke.py
- framework: crewai
extra: crewai
test-file: tests/integration/test_crewai_smoke.py
- framework: autogen
extra: autogen
test-file: tests/integration/test_autogen_smoke.py
- framework: langchain
extra: langchain
test-file: tests/integration/test_langchain_smoke.py
- framework: openai-agents
extra: openai-agents
test-file: tests/integration/test_openai_agents_smoke.py
- framework: claude-agent-sdk
extra: claude-agent-sdk
test-file: tests/integration/test_claude_agent_sdk_smoke.py
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install framework integration
run: python -m pip install --disable-pip-version-check ".[test,${{ matrix.extra }}]"
- name: Run real-framework offline smoke test
run: |
results_file="$(mktemp)"
python -m pytest -q "${{ matrix.test-file }}" -m integration --junitxml="$results_file"
python - "$results_file" <<'PY'
import sys
import xml.etree.ElementTree as ET

suites = ET.parse(sys.argv[1]).getroot().findall(".//testsuite")
executed = sum(int(suite.get("tests", "0")) for suite in suites)
skipped = sum(int(suite.get("skipped", "0")) for suite in suites)
if executed == 0 or skipped:
raise SystemExit(
f"integration gate requires executed tests with zero skips; "
f"executed={executed}, skipped={skipped}"
)
PY

quality-and-package:
name: Lint, security, and package
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
cache: pip
- name: Install quality and packaging tools
run: python -m pip install --disable-pip-version-check ".[dev]"
- name: Lint source and tests
run: python -m ruff check loopgain tests
- name: Scan application source for security issues
run: python -m bandit -q -r loopgain -c pyproject.toml -ll
- name: Build source and wheel distributions
run: python -m build
- name: Validate package metadata
run: python -m twine check dist/*
- name: Smoke-test the built wheel
run: |
smoke_venv="$(mktemp -d)/venv"
python -m venv "$smoke_venv"
"$smoke_venv/bin/python" -m pip install --disable-pip-version-check dist/*.whl
cd /tmp
"$smoke_venv/bin/python" -c "import loopgain; print(loopgain.__version__)"
"$smoke_venv/bin/loopgain" --help
1 change: 0 additions & 1 deletion loopgain/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from loopgain.classifier import (
TrajectoryThresholds,
classify_trajectory,
extract_features,
)


Expand Down
7 changes: 2 additions & 5 deletions loopgain/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
from typing import TYPE_CHECKING, Any, Optional
from urllib.parse import urlparse

from loopgain._version import __version__ as LIBRARY_VERSION


def _safe_float(x: Any) -> Any:
"""Coerce inf / -inf / NaN to None so the payload stays strict JSON.
Expand Down Expand Up @@ -128,11 +130,6 @@ def resolve_telemetry_config(
return ep, tok


# Library version sourced from loopgain._version so there's exactly one
# string to bump per release. _version.py has no project imports, so this
# is safe to import at module load.
from loopgain._version import __version__ as LIBRARY_VERSION

# Cap on per-iteration trajectory length sent to telemetry. Loops longer than
# this are truncated to the first PER_ITERATION_CAP entries with a
# ``truncated: true`` flag in the payload. 256 is well above the typical
Expand Down
16 changes: 16 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ loopgain = "loopgain.cli:main"

[project.optional-dependencies]
test = ["pytest>=7.0"]
dev = [
"bandit[toml]>=1.7.10,<2",
"build>=1.2,<2",
"pytest>=7,<10",
"ruff>=0.11,<1",
"twine>=5,<7",
]
# Framework adapter extras. Install with e.g. `pip install loopgain[langgraph]`.
# Versions are floors only — any reasonably recent release should work since
# the adapters duck-type the framework's public stream/callback surface.
Expand Down Expand Up @@ -117,3 +124,12 @@ testpaths = ["tests"]
markers = [
"integration: tests that import a real framework (langgraph, crewai, autogen, langchain, openai-agents, claude-agent-sdk). Skipped automatically when the framework isn't installed.",
]

[tool.ruff]
target-version = "py310"

[tool.ruff.lint]
select = ["E4", "E7", "E9", "F"]

[tool.bandit]
exclude_dirs = ["tests"]
1 change: 0 additions & 1 deletion tests/test_classifier_synthetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
FAST_CONVERGE,
OSCILLATING,
STALLING,
TrajectoryFeatures,
TrajectoryThresholds,
classify_trajectory,
extract_features,
Expand Down
1 change: 0 additions & 1 deletion tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from __future__ import annotations

import math

import pytest

Expand Down
3 changes: 2 additions & 1 deletion tests/test_funnel.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
from loopgain.funnel import Funnel


FIXED_CLOCK = lambda: datetime(2026, 5, 30, 9, 37, 12, 500, tzinfo=timezone.utc)
def FIXED_CLOCK():
return datetime(2026, 5, 30, 9, 37, 12, 500, tzinfo=timezone.utc)


class _Capture:
Expand Down
4 changes: 1 addition & 3 deletions tests/test_stress.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@

from __future__ import annotations

import math

import pytest

from loopgain import (
LoopGain,
Expand Down Expand Up @@ -280,7 +278,7 @@ def test_observe_rejects_string():
lg = LoopGain()
# We choose to accept strings (len semantics) but it's an unexpected path.
# If we ever tighten this, update the test.
state = lg.observe("abc") # len("abc") = 3
lg.observe("abc") # len("abc") = 3
assert lg.result.error_history[0] == 3.0


Expand Down
11 changes: 4 additions & 7 deletions tests/test_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
import inspect
import json
import re
import socket
import socket as _socket
import urllib.error as _uerr
from datetime import datetime, timezone
from pathlib import Path

import pytest

from loopgain import LoopGain, build_telemetry_payload
from loopgain import telemetry as _tele
from loopgain import LoopGain
from loopgain.telemetry import (
SCHEMA_VERSION,
LIBRARY_VERSION,
Expand Down Expand Up @@ -687,11 +689,6 @@ def test_send_payload_refuses_redirects():

# ----- send_payload retry behavior (transient failures) -----

import socket as _socket
import urllib.error as _uerr

from loopgain import telemetry as _tele


class _OkResp:
status = 202
Expand Down
1 change: 0 additions & 1 deletion tests/test_termination_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

from __future__ import annotations

import pytest

from loopgain import CONVERGING, FAST_CONVERGE, LoopGain, classify_trajectory

Expand Down