Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e88fa2c
feat(events): add event system for SDK observability
jkbennitt Mar 21, 2026
a91b57f
fix(events): defensive _event_bus instance attribute + TODO annotations
jkbennitt Mar 21, 2026
6604c74
Merge pull request #20 from AppSprout-dev/feat/event-system
jkbennitt Mar 21, 2026
c5ee919
feat(logging): add structured logging and EventLogBridge
jkbennitt Mar 21, 2026
7b29e2c
fix(events): cap history size to prevent unbounded memory growth
jkbennitt Mar 21, 2026
8814449
Merge pull request #22 from AppSprout-dev/feat/structured-logging
jkbennitt Mar 21, 2026
3c36512
feat(streaming): add streaming types, handlers, and accumulator
jkbennitt Mar 21, 2026
10ab143
Merge pull request #23 from AppSprout-dev/feat/streaming
jkbennitt Mar 21, 2026
d4ef678
feat(spawning): add dynamic confidence-driven agent spawning
jkbennitt Mar 21, 2026
8605a03
refactor: extract shared STOPWORDS to utils/text.py
jkbennitt Mar 21, 2026
5e2c73b
Merge pull request #24 from AppSprout-dev/feat/dynamic-spawning
jkbennitt Mar 21, 2026
2742923
feat(cli): add felix console command with init, run, version
jkbennitt Mar 21, 2026
855ae56
Merge pull request #25 from AppSprout-dev/feat/cli
jkbennitt Mar 21, 2026
e9d4845
feat: add reusable terminal helix visualizer module
jkbennitt Mar 21, 2026
7ac2ec6
fix: remove unused import and format visualizer files
jkbennitt Mar 21, 2026
00e4501
refactor: consolidate COLOR_MAP into terminal.py
jkbennitt Mar 21, 2026
94b4189
feat(visualization): integrate events, streaming, spawning, and CLI
jkbennitt Mar 21, 2026
334315e
Merge pull request #21 from AppSprout-dev/feat/visualization
jkbennitt Mar 21, 2026
be92e62
feat: examples v2, version bump to 0.2.0, changelog
jkbennitt Mar 21, 2026
d704958
fix(examples): address review notes
jkbennitt Mar 21, 2026
f34145a
Merge pull request #26 from AppSprout-dev/feat/examples-v2
jkbennitt Mar 21, 2026
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
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,57 @@

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

## [0.2.0] — 2026-03-21

Phase 2: Developer Experience. Zero breaking changes to v0.1.0 API.

### Event System
- `EventBus` — synchronous pub/sub with exact, prefix (`"agent.*"`), and catch-all subscriptions
- `FelixEvent` — frozen dataclass with event_type, source, data, timestamp
- `EventType` — enum covering agent, workflow, task, message, stream, and spawn events
- `EventEmitterMixin` — opt-in mixin for event emission from any component
- Wired into `FelixWorkflow`, `LLMAgent`, and `CentralPost`

### Structured Logging
- `configure_logging()` — one-call setup for all `felix_agent_sdk.*` loggers
- `FelixLogConfig` — text or JSON format, per-subsystem level overrides
- `JSONFormatter` — structured JSON output with event metadata fields
- `EventLogBridge` — subscribes to EventBus and auto-logs events

### Streaming
- `StreamEvent` / `StreamEventType` — token-level streaming events
- `StreamHandler`, `CallbackStreamHandler`, `EventBusStreamHandler`
- `StreamAccumulator` — bridges provider `stream()` to SDK `StreamEvent`
- `LLMAgent.process_task_streaming()` — full streaming with same lifecycle as `process_task()`

### Dynamic Spawning
- `ConfidenceMonitor` — per-agent/team confidence tracking, stagnation detection
- `ContentAnalyzer` — keyword-based topic coverage gap analysis
- `TeamSizeOptimizer` — heuristic team size recommendations
- `DynamicSpawner` — orchestrates monitor + analyzer, emits spawn events
- `WorkflowConfig.enable_dynamic_spawning` and `max_dynamic_agents` fields
- Wired into `FelixWorkflow` round loop

### CLI
- `felix init <name> [--template]` — scaffold projects (research, analysis, review templates)
- `felix run <config.yaml> [--provider] [--verbose]` — run workflows from YAML
- `felix version` — print SDK version
- YAML config loader with helix presets, team composition, all config fields

### Examples
- `06_event_system.py` — event subscriptions and workflow timeline
- `07_streaming_output.py` — token-level streaming demo
- `08_dynamic_spawning.py` — confidence-driven agent creation
- `09_structured_logging.py` — JSON and text log output
- `10_yaml_workflow/` — CLI workflow from YAML config

### Infrastructure
- Shared `STOPWORDS` extracted to `utils/text.py` (deduplicated from 3 modules)
- EventBus history capped at 10,000 events (configurable)
- 792+ tests, Python 3.10-3.12

---

## [0.1.0] — 2026-03-19

Initial public release of the Felix Agent SDK.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,9 @@ provider = auto_detect_provider() # Reads from environment

## Roadmap

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

**Phase 2 — Developer Experience:** CLI tooling, helix visualization dashboard, structured logging, expanded examples.
**Phase 2 — Developer Experience (v0.2.0, Current):** 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.

Expand Down
51 changes: 51 additions & 0 deletions examples/06_event_system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Event system — subscribe to workflow events and print a timeline.

Demonstrates the EventBus, EventType, and event subscriptions added
in Phase 2. Runs entirely offline with a mock provider.

Usage:
python examples/06_event_system.py
"""

from _mock import make_mock_provider

from felix_agent_sdk import EventBus, EventType, WorkflowConfig, run_felix_workflow


def main():
bus = EventBus()

# Subscribe to specific event types
def on_round(event):
print(f" [ROUND] {event.data}")

def on_task(event):
agent = event.data.get("agent_type", "?")
conf = event.data.get("confidence", "?")
print(f" [TASK] {event.source} ({agent}) confidence={conf}")

def on_started(event):
print(f">>> Workflow started: {event.data['task'][:60]}")

def on_converged(event):
print(f">>> Converged at round {event.data['round']}!")

def on_completed(event):
print(f">>> Workflow done in {event.data['elapsed_seconds']}s")

bus.subscribe(EventType.WORKFLOW_STARTED, on_started)
bus.subscribe("workflow.round.*", on_round)
bus.subscribe(EventType.TASK_COMPLETED, on_task)
bus.subscribe(EventType.WORKFLOW_CONVERGED, on_converged)
bus.subscribe(EventType.WORKFLOW_COMPLETED, on_completed)

provider = make_mock_provider()
config = WorkflowConfig(max_rounds=2)

result = run_felix_workflow(config, provider, "Evaluate renewable energy trends", event_bus=bus)
print(f"\nFinal synthesis: {result.synthesis[:100]}...")


if __name__ == "__main__":
main()
82 changes: 82 additions & 0 deletions examples/07_streaming_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Streaming output — process a single agent task with token-level streaming.

Demonstrates StreamHandler, CallbackStreamHandler, and
LLMAgent.process_task_streaming(). Runs offline with a mock provider.

Usage:
python examples/07_streaming_output.py
"""

from unittest.mock import MagicMock

from felix_agent_sdk import LLMAgent, LLMTask
from felix_agent_sdk.core.helix import HelixConfig, HelixGeometry
from felix_agent_sdk.providers.base import BaseProvider
from felix_agent_sdk.providers.types import CompletionResult, StreamChunk
from felix_agent_sdk.streaming import CallbackStreamHandler


def _make_streaming_provider():
"""Mock provider whose stream() yields word-by-word chunks."""
text = (
"Solar energy deployment has reached unprecedented levels globally. "
"Key markets show 40% year-over-year capacity growth, driven by "
"declining panel costs and supportive policy frameworks. Grid-scale "
"battery storage is emerging as the critical enabler for intermittency "
"management, with lithium-ion costs declining 85% since 2010."
)
words = text.split(" ")
provider = MagicMock(spec=BaseProvider)

def _stream(messages, **kwargs):
for i, word in enumerate(words):
is_last = i == len(words) - 1
yield StreamChunk(
text=word + ("" if is_last else " "),
is_final=is_last,
usage={"prompt_tokens": 30, "completion_tokens": len(words), "total_tokens": 30 + len(words)} if is_last else {},
)

provider.stream.side_effect = _stream
provider.complete.return_value = CompletionResult(
content=text, model="mock",
usage={"prompt_tokens": 30, "completion_tokens": len(words), "total_tokens": 30 + len(words)},
)
provider.count_tokens.return_value = 30
return provider


def main():
provider = _make_streaming_provider()
cfg = HelixConfig.default()
helix = HelixGeometry(cfg.top_radius, cfg.bottom_radius, cfg.height, cfg.turns)
agent = LLMAgent("research-001", provider, helix, agent_type="research")
agent.spawn(0.1)
agent.update_position(0.5)

print("=== Streaming Agent Output ===\n")

token_count = [0]

def on_token(event):
token_count[0] += 1
print(event.content, end="", flush=True)

def on_result(event):
print("\n\n--- Stream complete ---")
print(f"Tokens: {event.token_index}")
print(f"Length: {len(event.accumulated)} chars")

handler = CallbackStreamHandler(on_token=on_token, on_result=on_result)

task = LLMTask(task_id="demo-1", description="Analyse renewable energy trends")
result = agent.process_task_streaming(task, handler)

print(f"Confidence: {result.confidence:.3f}")
print(f"Temperature: {result.temperature_used:.3f}")
print(f"Phase: {result.position_info.get('phase', 'unknown')}")


if __name__ == "__main__":
main()
61 changes: 61 additions & 0 deletions examples/08_dynamic_spawning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Dynamic spawning — run a workflow with confidence-driven agent creation.

Demonstrates enable_dynamic_spawning in WorkflowConfig. When confidence
is low, the system automatically spawns additional agents. Uses event
bus to observe spawn decisions.

Usage:
python examples/08_dynamic_spawning.py
"""

from _mock import make_mock_provider

from felix_agent_sdk import EventBus, EventType, WorkflowConfig, run_felix_workflow


def main():
bus = EventBus()

# Watch for spawn events
def on_spawn(event):
print(f" >>> SPAWN: {event.data.get('agent_type', '?')} "
f"(reason: {event.data.get('reason', '?')}, "
f"gap: {event.data.get('gap', '?')})")

def on_spawn_done(event):
print(f" >>> SPAWNED: {event.data.get('agent_id', '?')} "
f"(total: {event.data.get('total_spawned', '?')})")

bus.subscribe(EventType.SPAWN_TRIGGERED, on_spawn)
bus.subscribe(EventType.SPAWN_COMPLETED, on_spawn_done)
bus.subscribe(EventType.WORKFLOW_ROUND_COMPLETED, lambda e:
print(f" Round {e.data['round']} done — avg confidence: {e.data['avg_confidence']:.3f}"))

# Low-confidence responses to trigger spawning
responses = [
"Preliminary findings are inconclusive.",
"Weak evidence suggests possible correlation.",
"Need more data to confirm hypothesis.",
"Results remain uncertain after review.",
]
provider = make_mock_provider(responses)

config = WorkflowConfig(
max_rounds=3,
confidence_threshold=0.99, # unreachable — forces all rounds
enable_dynamic_spawning=True,
max_dynamic_agents=2,
)

print("=== Dynamic Spawning Demo ===\n")
result = run_felix_workflow(config, provider, "Investigate uncertain hypothesis", event_bus=bus)

print(f"\nFinal team size: {result.metadata['agents_count']}")
print(f"Rounds: {result.total_rounds}")
print(f"Confidence: {result.final_confidence:.3f}")
print(f"Synthesis: {result.synthesis[:100]}...")


if __name__ == "__main__":
main()
52 changes: 52 additions & 0 deletions examples/09_structured_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Structured logging — run a workflow with JSON log output.

Demonstrates configure_logging() with JSON format and EventLogBridge
for automatic event-to-log bridging.

Usage:
python examples/09_structured_logging.py
"""


from _mock import make_mock_provider

from felix_agent_sdk import EventBus, WorkflowConfig, configure_logging, run_felix_workflow
from felix_agent_sdk.utils.logging import EventLogBridge, FelixLogConfig


def main():
# Configure JSON logging at INFO level (use DEBUG for more detail)
configure_logging(FelixLogConfig(
level="INFO",
format="json",
))

# Create event bus and bridge events to the logger
bus = EventBus()
bridge = EventLogBridge(bus)

provider = make_mock_provider()
config = WorkflowConfig(max_rounds=1)

print("=== Running with JSON structured logging ===\n")
result = run_felix_workflow(config, provider, "Test structured logging", event_bus=bus)

print(f"\nSynthesis: {result.synthesis[:80]}...")

# Clean up
bridge.detach()

# Show that text format also works
print("\n=== Switching to text format ===\n")
configure_logging(FelixLogConfig(level="INFO", format="text"))

bus2 = EventBus()
bridge2 = EventLogBridge(bus2)

run_felix_workflow(config, make_mock_provider(), "Test text logging", event_bus=bus2)
bridge2.detach()


if __name__ == "__main__":
main()
30 changes: 30 additions & 0 deletions examples/10_yaml_workflow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# YAML Workflow Example

Run a Felix workflow from a YAML config file using the CLI.

**Note:** This example requires a real LLM API key. Examples 01-09 run
offline with mock providers — use those first to verify your setup.

## Prerequisites

```bash
pip install felix-agent-sdk[openai]
export OPENAI_API_KEY=sk-... # required — no mock fallback
```

## Usage

```bash
# From the repo root
felix run examples/10_yaml_workflow/felix.yaml

# With verbose logging
felix run examples/10_yaml_workflow/felix.yaml --verbose

# Override provider
felix run examples/10_yaml_workflow/felix.yaml --provider anthropic
```

## Config Reference

See `felix.yaml` in this directory for all available fields.
20 changes: 20 additions & 0 deletions examples/10_yaml_workflow/felix.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Example Felix workflow config
# Run with: felix run examples/10_yaml_workflow/felix.yaml

provider: openai
model: gpt-4o-mini

helix: research_heavy
max_rounds: 3
confidence_threshold: 0.78

team:
- type: research
- type: research
- type: analysis
- type: critic

enable_dynamic_spawning: true
max_dynamic_agents: 2

task: "What are the key challenges and opportunities in quantum computing for cryptography?"
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ dev = [
"mkdocs-material>=9.0",
]

[project.scripts]
felix = "felix_agent_sdk.cli.main:main"

[project.urls]
Homepage = "https://github.com/AppSprout-dev/felix-agent-sdk"
Documentation = "https://felix-sdk.appsprout.dev"
Expand Down
Loading
Loading