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
32 changes: 29 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ on:

jobs:
test:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]
os: [ubuntu-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
Expand All @@ -20,6 +22,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: 'pip'

- name: Install dependencies
Expand All @@ -31,7 +34,30 @@ jobs:
run: ruff check src/

- name: Test
run: pytest tests/ -v --tb=short
run: pytest tests/ -v --tb=short --cov=felix_agent_sdk --cov-report=term-missing

typecheck:
name: Type check (mypy, vendor SDKs installed)
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"

# Install the optional provider SDKs so mypy checks the provider layer
# against their real types rather than the ignore-missing-imports `Any`.
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[all,dev]"

- name: Type check
run: mypy src/

build:
name: Verify package builds
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,22 @@ jobs:
python -m pip install --upgrade pip
pip install -e ".[dev]"

- name: Verify release tag matches package version
run: |
PKG_VERSION=$(python -c "from felix_agent_sdk import __version__; print(__version__)")
TAG_VERSION="${GITHUB_REF_NAME#v}"
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
echo "::error::Release tag $GITHUB_REF_NAME does not match package version $PKG_VERSION (_version.py)."
exit 1
fi
echo "Version check OK: $PKG_VERSION"

- name: Lint
run: ruff check src/

# No mypy gate here: CI enforces it on every PR, and an unpinned
# mypy minor release should not be able to block a tagged release.

- name: Test
run: pytest tests/ -v --tb=short

Expand Down
89 changes: 89 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,95 @@

All notable changes to Felix Agent SDK will be documented in this file.

## [0.3.0] — 2026-06-10

Phase 3: Hardening & Async. Driven by a full-repo code review and a follow-up
adversarial re-review from the RLE-consumer perspective.

### Added
- **Async provider interface**: `AnthropicProvider`, `OpenAIProvider`, and
`LocalProvider` now implement `acomplete()` and `astream()` using the
vendor SDKs' async clients.
- `Agent.set_progress()` — public API to place an agent at a specific helix
position (replaces orchestrators poking `_progress`). Placing at `t=1.0`
transitions the agent to `COMPLETED`, making the placement stable across
subsequent position updates.
- `HubCapacityError` (subclass of `RuntimeError`), exported from
`felix_agent_sdk` and `felix_agent_sdk.communication`.
- `extract_status_code()` / `extract_retry_after()` helpers in
`providers.errors`; `RateLimitError.retry_after` is now populated from
the Retry-After header when present.
- `SQLiteBackend` supports the context manager protocol and is now safe
for multi-threaded use (shared connection + internal lock).

### Changed
- **Breaking**: `CentralPost.register_agent()` raises `HubCapacityError` at
capacity instead of returning `None`, matching `register_agent_id()`
(which now raises the same subclass — `except RuntimeError` still works).
- **Behavior**: `CentralPost` processed-message history is now bounded
(`message_history_limit=1000` by default); previously it grew without
limit. Pass `message_history_limit=None` to restore unbounded history.
- `KnowledgeStore.add_relationship()` returns `False` instead of storing a
relationship when either entry is missing or soft-deleted (such
relationships were invisible to `get_relationships()` anyway).
- `HelixGeometry`/`HelixConfig` `turns` is typed `float` (fractional turns
are geometrically valid and the YAML loader already produced floats).
- `LLMAgent.get_position_info()` now includes `agent_type`.
- `ProviderConfig.__repr__` masks the API key.
- Provider error translation prefers HTTP status codes from vendor SDK
exceptions over message string matching; all translated errors carry
`status_code` when available.
- `ProviderRegistry` detection priorities are stored per provider and
honored during `auto_detect()` (previously priorities were reset on
re-registration and ignored by detection).
- Default Anthropic model updated to `claude-sonnet-4-6`.
- `felix run` now prints the underlying cause when provider creation fails.
- `SpokeManager.create_spoke(auto_connect=True)` raises `HubCapacityError`
when the hub is at capacity instead of silently returning a disconnected
spoke (the spoke is not retained on failure).
- SQL identifier validation uses `re.fullmatch` (closing a trailing-newline
bypass) and explicitly rejects `]` so it cannot break out of `[..]`
bracket quoting.

### Fixed
- Dynamic spawning gap analysis received an empty `agent_type` for every
result, so type-based spawn recommendations could never trigger.
- `SQLiteBackend.query()` interpolated `order_by` into SQL unvalidated;
identifiers are now strictly validated and checked against known columns
(session schema plus on-disk columns, so schema evolution still sorts).
- `KnowledgeStore.get_relationships()` no longer returns relationships
involving soft-deleted entries.
- OpenAI/Local `stream()`/`astream()` stop consuming only at a true terminal
chunk (usage with empty choices or a finish_reason), so servers that emit
running usage on content chunks (e.g. vLLM `continuous_usage_stats`) are no
longer silently truncated. A `close()` failure during stream teardown is
swallowed (debug-logged) instead of masking a successful stream as a
`ProviderError`.
- Provider error translation maps HTTP 404 to `ModelNotFoundError`, and the
SDK exception class name (`NotFoundError`) is now matched correctly (the
previous `"not_found"` substring check could never match).
- CentralPost async queue is created eagerly (removes a lazy-init race).
- 32 mypy strict errors across 13 files; `mypy src/` now gates CI.

### Infrastructure
- CI: Windows + Ubuntu matrix, Python 3.10–3.14, coverage report. A dedicated
type-check job installs the provider SDKs (`.[all,dev]`) so mypy checks the
provider layer against real vendor types rather than `Any`.
- Publish workflow verifies the release tag matches `_version.py`.
- `types-PyYAML` added to dev dependencies; explicit `asyncio_mode = "strict"`.

### Notes (behavior worth knowing)
- Provider HTTP 403 now maps to `AuthenticationError` — a per-model permission
denial aborts rather than being retried as a generic error.
- `CentralPost.register_agent` no longer substitutes `id(agent)` for an
empty-string `agent_id`; an empty id registers verbatim as `""`.
- A stream that completes without the server ever sending usage emits no
`is_final` event (there is no terminal usage chunk to mark).
- `extract_retry_after` reads the numeric `Retry-After` header form only;
`retry-after-ms` and HTTP-date forms are not yet parsed.

---

## [0.2.2] — 2026-06-09

### Fixed
Expand Down
17 changes: 10 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,13 @@ The original Felix source is at `https://github.com/CalebisGross/felix`. Key fil

## Phased Implementation

- **Phase 1** (current): Package skeleton + provider layer
- **Phase 2** `feat/core-geometry`: Port HelixGeometry, add HelixConfig/HelixPosition
- **Phase 3** `feat/providers`: Tests for provider layer
- **Phase 4** `feat/agents`: Port agent classes, wire to BaseProvider
- **Phase 5** `feat/communication`: Port CentralPost, Spoke, messages
- **Phase 6** `feat/memory`: Port KnowledgeStore, TaskMemory, ContextCompressor
- **Phase 7** `feat/workflows`: Port workflow runner + templates
All porting phases below are **complete** (shipped across v0.1.0–v0.2.x). Current
work is post-port hardening, async support, and release engineering (v0.3.x).

- **Phase 1** ✅: Package skeleton + provider layer
- **Phase 2** ✅ `feat/core-geometry`: Port HelixGeometry, add HelixConfig/HelixPosition
- **Phase 3** ✅ `feat/providers`: Tests for provider layer
- **Phase 4** ✅ `feat/agents`: Port agent classes, wire to BaseProvider
- **Phase 5** ✅ `feat/communication`: Port CentralPost, Spoke, messages
- **Phase 6** ✅ `feat/memory`: Port KnowledgeStore, TaskMemory, ContextCompressor
- **Phase 7** ✅ `feat/workflows`: Port workflow runner + templates
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Felix provides three specialized agent types, each designed for a phase of the c
from felix_agent_sdk import AgentFactory
from felix_agent_sdk.providers import AnthropicProvider

provider = AnthropicProvider(model="claude-sonnet-4-5")
provider = AnthropicProvider(model="claude-sonnet-4-6")
factory = AgentFactory(provider)

team = factory.create_specialized_team("moderate")
Expand Down Expand Up @@ -252,7 +252,7 @@ Felix supports multiple LLM providers through a clean abstraction layer:
from felix_agent_sdk.providers import AnthropicProvider, OpenAIProvider, LocalProvider

# Anthropic Claude
provider = AnthropicProvider(model="claude-sonnet-4-5", api_key="sk-...")
provider = AnthropicProvider(model="claude-sonnet-4-6", api_key="sk-...")

# OpenAI
provider = OpenAIProvider(model="gpt-4o", api_key="sk-...")
Expand All @@ -269,7 +269,7 @@ provider = LocalProvider(
```bash
export FELIX_PROVIDER=anthropic
export ANTHROPIC_API_KEY=sk-ant-...
export FELIX_MODEL=claude-sonnet-4-5
export FELIX_MODEL=claude-sonnet-4-6
```

```python
Expand All @@ -284,9 +284,11 @@ provider = auto_detect_provider() # Reads from environment

**Phase 1 — Core SDK (v0.1.0):** Pip-installable package with provider abstraction, core primitives, and documentation.

**Phase 2 — Developer Experience (v0.2.0, Current):** Event system, structured logging, streaming, dynamic spawning, CLI tooling (`felix init`, `felix run`), expanded examples.
**Phase 2 — Developer Experience (v0.2.0):** Event system, structured logging, streaming, dynamic spawning, CLI tooling (`felix init`, `felix run`), expanded examples.

**Phase 3 — Community & Ecosystem:** MCP server integration, vector database connectors, observability adapters, community contribution framework.
**Phase 3 — Hardening & Async (v0.3.0, Current):** Async provider interface (`acomplete`/`astream`), typed provider errors with retry-after extraction, strict type-checking gate, Windows + Python 3.13 CI, SQLite hardening, bounded message history.

**Phase 4 — Community & Ecosystem:** Tool use / structured output support in the provider layer, MCP server integration, vector database connectors, observability adapters, community contribution framework.

---

Expand Down
2 changes: 1 addition & 1 deletion examples/02_research_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def get_provider(provider_name: str):
try:
from felix_agent_sdk.providers import AnthropicProvider

return AnthropicProvider(model="claude-sonnet-4-5")
return AnthropicProvider(model="claude-sonnet-4-6")
except Exception as e:
print(f"Could not create Anthropic provider: {e}")
return None
Expand Down
13 changes: 12 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
authors = [
{ name = "AppSprout", email = "team@appsprout.com" },
{ name = "AppSprout", email = "team@appsprout.dev" },
]
keywords = [
"ai", "agents", "multi-agent", "orchestration", "llm",
Expand All @@ -24,6 +24,8 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]

Expand All @@ -48,6 +50,7 @@ dev = [
"pytest-cov>=4.0",
"ruff>=0.1",
"mypy>=1.5",
"types-PyYAML>=6.0",
"mkdocs-material>=9.0",
]

Expand All @@ -66,10 +69,18 @@ path = "src/felix_agent_sdk/_version.py"
[tool.hatch.build.targets.wheel]
packages = ["src/felix_agent_sdk"]

[tool.pytest.ini_options]
asyncio_mode = "strict"

[tool.ruff]
target-version = "py310"
line-length = 100

[tool.mypy]
python_version = "3.10"
strict = true

[[tool.mypy.overrides]]
# Optional provider SDKs — not installed in the base dev environment.
module = ["anthropic", "anthropic.*", "openai", "openai.*", "tiktoken", "tiktoken.*"]
ignore_missing_imports = true
11 changes: 9 additions & 2 deletions src/felix_agent_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Quickstart:
from felix_agent_sdk.providers import AnthropicProvider

provider = AnthropicProvider(model="claude-sonnet-4-5")
provider = AnthropicProvider(model="claude-sonnet-4-6")

Full documentation: https://github.com/AppSprout-dev/felix-agent-sdk
"""
Expand All @@ -30,7 +30,13 @@
OpenAIProvider,
auto_detect_provider,
)
from felix_agent_sdk.communication import CentralPost, Message, MessageType, Spoke
from felix_agent_sdk.communication import (
CentralPost,
HubCapacityError,
Message,
MessageType,
Spoke,
)
from felix_agent_sdk.events import EventBus, EventType, FelixEvent
from felix_agent_sdk.memory import ContextCompressor, KnowledgeStore, TaskMemory
from felix_agent_sdk.spawning import ConfidenceMonitor, DynamicSpawner
Expand Down Expand Up @@ -60,6 +66,7 @@
"AgentFactory",
# Communication
"CentralPost",
"HubCapacityError",
"Message",
"MessageType",
"Spoke",
Expand Down
2 changes: 1 addition & 1 deletion src/felix_agent_sdk/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.2"
__version__ = "0.3.0"
27 changes: 27 additions & 0 deletions src/felix_agent_sdk/agents/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,33 @@ def update_position(self, current_time: float) -> None:
if self._progress >= 1.0:
self._state = AgentState.COMPLETED

def set_progress(self, progress: float) -> None:
"""Place the agent directly at *progress* along the helix.

Bypasses time-based progression — used by orchestrators that need an
agent at a specific phase (e.g. a synthesis agent at t=1.0).

Mirrors :meth:`update_position` semantics: placing the agent at
``progress >= 1.0`` transitions it to ``COMPLETED``, which also makes
the placement stable (completed agents are exempt from time-based
recomputation). Placements below 1.0 are a starting point only — a
later :meth:`update_position`/:meth:`get_position` call recomputes
progress from elapsed time.

Raises:
ValueError: If *progress* is outside [0, 1], or the agent has
not been spawned, or has already completed/failed.
"""
if not (0.0 <= progress <= 1.0):
raise ValueError("progress must be between 0 and 1")
if self._state == AgentState.WAITING:
raise ValueError("Cannot set progress of unspawned agent")
if self._state in (AgentState.COMPLETED, AgentState.FAILED):
raise ValueError(f"Cannot set progress of {self._state.value} agent")
self._progress = progress
if progress >= 1.0:
self._state = AgentState.COMPLETED

def get_position(self, current_time: float) -> Optional[Tuple[float, float, float]]:
"""Return (x, y, z) on the helix, or ``None`` if not yet spawned."""
if self._state == AgentState.WAITING:
Expand Down
Loading
Loading