Skip to content

Commit bec9248

Browse files
committed
feat: new features in scope (memory decay half_life, observability, examples, almock)
1 parent 539c736 commit bec9248

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+1489
-541
lines changed

CHANGELOG.md

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,48 +5,31 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [Unreleased]
8+
## [0.2.0] - 2026-02-26
99

1010
### Added
1111

12-
- **Code quality (Step 6):** `docs/ARCHITECTURE.md` with package layout, dependency direction, extension points, and §6 consistent patterns (naming: *Protocol/*Backend/*Config/*Store/*Manager; errors: SyrinError; Protocol vs ABC). `docs/code-quality.md` for mypy, ruff, public API typing, coverage, and dead-code rules. `tests/code_quality/`: `test_public_api_exports.py` (all `__all__` importable, no duplicates, run return type), `test_run_and_config_api.py` (run/config valid and edge cases with mocks), `test_critical_path_edge_cases.py` (threshold, config, run empty input).
13-
14-
- **Core stability (Step 4):** New test suite `tests/core_stability/` with TDD tests for agent lifecycle (sync/async parity), loop strategies (LoopResult shape, exception handling), model resolution (valid provider, strict mode), budget enforcement (per-run, threshold actions), memory (remember/recall/forget, conversation path), Response contract (cost/tokens/tool_calls/stop_reason), and structured output validation.
15-
- **Model resolution:** `ProviderNotFoundError` exception and `get_provider(name, strict=True)`; when `strict=True`, unknown provider names raise with a clear message listing known providers. Agent uses strict resolution when resolving from `ModelConfig` so invalid `provider=` fails fast.
16-
- **Observability:** Span coverage for agent runs: root agent span plus automatic child spans for each LLM call and tool execution when the loop uses a tracer. `AgentRunContext` now exposes an optional `tracer` property so loops can create LLM/tool spans.
17-
- **Observability:** `_llm_span_context` and `_tool_span_context` helpers in `loop` so SingleShot and React loops create `SpanKind.LLM` and `SpanKind.TOOL` spans with semantic attributes (tokens, model, tool name, input/output).
18-
- **Observability:** OTLP exporter implementation in `syrin.observability.otlp`: `OTLPExporter` converts Syrin spans to OpenTelemetry and exports to OTLP HTTP endpoint when optional dependency is installed; no-op when not.
19-
- **Tests:** `tests/observability/test_observability_integration.py` — TDD tests for span coverage (agent/LLM/tool), session propagation, metrics schema, sampling parent-child consistency, debug mode, OTLP exporter, hook coverage, and export format.
12+
- **Almock (LLM Mock)**`Model.Almock()` for development and testing without an API key. Configurable pricing tiers (LOW, MEDIUM, HIGH, ULTRA_HIGH), latency (default 1–3s or custom), and response (Lorem Ipsum or custom text). Examples and docs use Almock by default; swap to a real model with one line.
13+
- **Memory decay and consolidation** — Decay strategies with configurable min importance and optional reinforcement on access. `Memory.consolidate()` for content deduplication with optional budget. Entries without IDs receive auto-generated IDs.
14+
- **Checkpoint triggers** — Auto-save on STEP, TOOL, ERROR, or BUDGET in addition to MANUAL. Loop strategy comparison (REACT, SINGLE_SHOT, PLAN_EXECUTE, CODE_ACTION) documented in advanced topics.
15+
- **Provider resolution**`ProviderNotFoundError` when using an unknown provider, with a message listing known providers. Strict resolution available via `get_provider(name, strict=True)`.
16+
- **Observability** — Agent runs emit a root span plus child spans for each LLM call and tool execution. Session ID propagates to all spans. Optional OTLP exporter (`syrin.observability.otlp`) for OpenTelemetry.
17+
- **Documentation** — Architecture guide, code quality guidelines, CONTRIBUTING.md. Community links (Discord, X) and corrected doc links.
2018

2119
### Changed
2220

23-
- **Documentation (Step 7):** README: Discord link updated to https://discord.gg/p4jnKxYKpB, Twitter to https://x.com/syrin_dev; added “Connect here” community section. Docs: fixed dead links (anomalyco → syrin-labs/syrin-python, CHANGES.md → CHANGELOG.md); docs/README.md Quick Start uses `syrin` (lowercase). Created CONTRIBUTING.md and docs/code-quality.md. docs/guardrails.md and docs/ratelimit.md: corrected support/import links and `from syrin` imports. docs/advanced-topics.md: rate-limiting.md → ratelimit.md. Removed plan/*.md references from public docs (CHANGELOG).
24-
25-
- **Code quality (Step 6):** `syrin.run()` signature: `tools` typed as `list[ToolSpec] | None`, return as `Response[str]`. Removed duplicate entries from `syrin.__all__` (BudgetThreshold, CheckpointTrigger). `syrin.config.__all__`: removed duplicate GlobalConfig. Ruff ignore list in pyproject.toml documented (E501, E402, ARG002, ARG001, F821, F811).
26-
27-
- **Model resolution:** `get_provider(provider_name, *, strict=False)` — new `strict` parameter; Agent construction with `ModelConfig(provider="typo")` now raises `ProviderNotFoundError` (breaking for callers relying on fallback to LiteLLM).
28-
- **Observability:** Session ID from `trace.session()` now propagates to all spans created during agent runs (spans already had `session_id`; context was already used by tracer).
29-
- **Docs:** `docs/observability.md` — Updated "What's Traced by Default" with exact span kinds and tree shape; added Metrics schema subsection; fixed Production Setup imports (create_sampler from sampling).
30-
- **Plan:** Observability checklist for v0.2.0 marked complete.
21+
- **Model resolution** — Agent construction with an invalid `provider` in `ModelConfig` now raises `ProviderNotFoundError` instead of falling back (breaking for callers relying on LiteLLM fallback).
22+
- **API**`syrin.run()` return type and `tools` parameter typing clarified. Duplicate symbols removed from public API exports.
23+
- **Docs** — README and guides use lowercase `syrin` imports. Guardrails and rate-limit docs: fixed imports and references.
3124

3225
### Fixed
3326

34-
- **Step 5 (Bug fixes):** Response data regression tests for sync/async parity and tool_calls from loop. recall() contract tests (Agent/Memory/MemoryStore return shape). spawn() return type (Response vs Agent) and budget inheritance tests. Hook emission audit (agent run emits only Hook enum). Rate limit: threshold behavior is fully controlled by the user’s `action(ctx)` callback on `RateLimitThreshold` (no separate `rate_limit_action`; user implements stop/wait/switch in the callback). Guardrail block tests (input block skips LLM; output block returns GUARDRAIL response). Checkpoint trigger tests (STEP fires, MANUAL does not; save/load). Session `span_count` updated when spans are exported in session context. Edge-case tests (empty tools, no budget, no persistent memory, unknown provider) and typed errors.
35-
36-
### Deprecated
37-
38-
### Removed
39-
40-
### Security
27+
- Response and recall contract; spawn return type and budget inheritance. Rate limit thresholds fully controlled by user action callback. Guardrail input block skips LLM; output block returns GUARDRAIL response. Checkpoint trigger behavior (STEP, MANUAL). Session span count when exporting. Edge cases: empty tools, no budget, unknown provider.
4128

4229
---
4330

4431
## [0.1.1] - 2026-02-25
4532

46-
**syrin** v0.1.1 — Python library for building AI agents with budget management, declarative definitions, and production-ready observability.
47-
48-
Initial Release
49-
50-
---
33+
- Initial release. Python library for building AI agents with budget management, declarative definitions, and observability.
5134

5235
**Install:** `pip install syrin==0.1.1`

README.md

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ import os
7272
from syrin import Agent, Model, Budget, stop_on_exceeded
7373

7474
class Researcher(Agent):
75-
model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
75+
# model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
76+
model = Model.Almock() # No API Key needed
7677
budget = Budget(run=0.50, on_exceeded=stop_on_exceeded)
7778

7879
result = Researcher().response("Summarize quantum computing in 3 sentences")
@@ -84,6 +85,8 @@ print(f"Cost: ${result.cost:.4f} | Budget used: ${result.budget_used:.4f}")
8485

8586
Pass your API key explicitly. The run is capped at $0.50; when the budget is exceeded, the agent stops.
8687

88+
**No API key?** Examples and docs use `Model.Almock()` by default; comment it out and uncomment the real model when you have an API key.
89+
8790
---
8891

8992
## Basic agent (no budget)
@@ -95,7 +98,8 @@ import os
9598
from syrin import Agent, Model
9699

97100
class Greeter(Agent):
98-
model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
101+
# model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
102+
model = Model.Almock() # No API Key needed
99103
system_prompt = "You are a helpful assistant."
100104

101105
result = Greeter().response("Say hello in one sentence.")
@@ -116,7 +120,8 @@ import os
116120
from syrin import Agent, Model, Budget, stop_on_exceeded
117121

118122
class SafeResearcher(Agent):
119-
model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
123+
# model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
124+
model = Model.Almock() # No API Key needed
120125
budget = Budget(run=0.50, on_exceeded=stop_on_exceeded)
121126

122127
result = SafeResearcher().response("Explain photosynthesis briefly.")
@@ -138,7 +143,8 @@ import os
138143
from syrin import Agent, Model, Budget, BudgetThreshold
139144

140145
class AdaptiveResearcher(Agent):
141-
model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
146+
# model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
147+
model = Model.Almock() # No API Key needed
142148
budget = Budget(
143149
run=0.50,
144150
thresholds=[
@@ -184,7 +190,8 @@ from syrin import Agent, Model, Budget
184190
from syrin.enums import Hook
185191

186192
class ObservableAgent(Agent):
187-
model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
193+
# model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
194+
model = Model.Almock() # No API Key needed
188195
system_prompt = "You are a research assistant."
189196
budget = Budget(run=0.50)
190197

@@ -212,7 +219,8 @@ from syrin import Agent, Model, Memory
212219
from syrin.enums import MemoryType
213220

214221
agent = Agent(
215-
model=Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY")),
222+
# model=Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY")),
223+
model=Model.Almock(), # No API Key needed
216224
memory=Memory(types=[MemoryType.CORE, MemoryType.EPISODIC]),
217225
)
218226

@@ -241,7 +249,8 @@ from syrin import Agent, Model, GuardrailChain
241249
from syrin.guardrails import LengthGuardrail, ContentFilter
242250

243251
class SafeAgent(Agent):
244-
model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
252+
# model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
253+
model = Model.Almock() # No API Key needed
245254
guardrails = GuardrailChain([
246255
LengthGuardrail(max_length=4000),
247256
ContentFilter(blocked_words=["spam", "malicious"]),
@@ -269,7 +278,8 @@ from syrin import Agent, Model
269278
from syrin.context import Context
270279

271280
agent = Agent(
272-
model=Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY")),
281+
# model=Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY")),
282+
model=Model.Almock(), # No API Key needed
273283
context=Context(max_tokens=8000),
274284
)
275285

@@ -291,7 +301,8 @@ import os
291301
from syrin import Agent, Model, CheckpointConfig, CheckpointTrigger
292302

293303
agent = Agent(
294-
model=Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY")),
304+
# model=Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY")),
305+
model=Model.Almock(), # No API Key needed
295306
checkpoint=CheckpointConfig(storage="memory", trigger=CheckpointTrigger.STEP),
296307
)
297308

@@ -322,7 +333,8 @@ def search_web(query: str) -> str:
322333
return f"Results for: {query}" # Replace with real search in production
323334

324335
class Assistant(Agent):
325-
model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
336+
# model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
337+
model = Model.Almock() # No API Key needed
326338
tools = [search_web]
327339
budget = Budget(run=0.25)
328340

@@ -345,7 +357,8 @@ import os
345357
from syrin import Agent, Model
346358

347359
class Writer(Agent):
348-
model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
360+
# model = Model.OpenAI("gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
361+
model = Model.Almock() # No API Key needed
349362

350363
async def main():
351364
agent = Writer()

docs/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ Copy and run this to test your setup:
9191
from syrin import Agent, Model
9292

9393
class MyAgent(Agent):
94-
model = Model.OpenAI("gpt-4o-mini")
94+
# model = Model.OpenAI("gpt-4o-mini")
95+
model = Model.Almock() # No API Key needed
9596
system_prompt = "You are helpful."
9697

9798
agent = MyAgent()

docs/advanced-topics.md

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ The test suite in `tests/core_stability/` encodes these guarantees with TDD test
2121

2222
---
2323

24+
## Loop strategy comparison
25+
26+
| Strategy | When to use it | Tool use | Typical flow |
27+
|----------------|----------------|----------|---------------|
28+
| **REACT** | General agents that may call tools; one step per turn. | Yes | User → LLM → (tool calls or answer) → optional tool loop → final answer. |
29+
| **SINGLE_SHOT**| No tools; one prompt, one completion. | No | User → LLM → answer. |
30+
| **PLAN_EXECUTE**| Multi-step tasks: plan first, then execute steps. | Yes | User → plan → execute steps (each step can use tools). |
31+
| **CODE_ACTION**| Agent emits code or actions to run (e.g. sandbox). | Yes | User → LLM → code/actions → execute → optional loop. |
32+
33+
All strategies return the same `LoopResult` shape (content, stop_reason, iterations, cost_usd, token_usage, tool_calls). Choose REACT for most agents with tools; SINGLE_SHOT when you have no tools; PLAN_EXECUTE for explicit planning; CODE_ACTION when the agent outputs code or structured actions.
34+
35+
---
36+
2437
## Lifecycle Hooks
2538

2639
Hooks let you execute code at specific moments during an agent's execution. This is essential for logging, metrics, authentication, and custom behaviors.
@@ -45,7 +58,8 @@ from Syrin.enums import Hook
4558

4659
# Create agent
4760
agent = Agent(
48-
model=Model.OpenAI("gpt-4o-mini"),
61+
# model=Model.OpenAI("gpt-4o-mini"),
62+
model=Model.Almock(), # No API Key needed
4963
system_prompt="You are a helpful assistant.",
5064
)
5165

@@ -71,7 +85,8 @@ from Syrin import Agent, Model
7185
from Syrin.enums import Hook
7286

7387
agent = Agent(
74-
model=Model.OpenAI("gpt-4o-mini"),
88+
# model=Model.OpenAI("gpt-4o-mini"),
89+
model=Model.Almock(), # No API Key needed
7590
system_prompt="You are a helpful assistant.",
7691
)
7792

@@ -206,7 +221,8 @@ for span in memory_exporter.spans:
206221

207222
# Or enable debug mode on agent for automatic console output
208223
agent = Agent(
209-
model=Model.OpenAI("gpt-4o-mini"),
224+
# model=Model.OpenAI("gpt-4o-mini"),
225+
model=Model.Almock(), # No API Key needed
210226
debug=True, # Enables trace output
211227
)
212228
```
@@ -242,7 +258,8 @@ from Syrin.checkpoint import Checkpointer, CheckpointState
242258
checkpointer = Checkpointer()
243259

244260
agent = Agent(
245-
model=Model.OpenAI("gpt-4o-mini"),
261+
# model=Model.OpenAI("gpt-4o-mini"),
262+
model=Model.Almock(), # No API Key needed
246263
system_prompt="You are a helpful assistant.",
247264
)
248265

@@ -344,7 +361,8 @@ from Syrin.guardrails import ContentFilter, PIIScanner
344361

345362
# Simple content filtering
346363
agent = Agent(
347-
model=Model.OpenAI("gpt-4o"),
364+
# model=Model.OpenAI("gpt-4o"),
365+
model=Model.Almock(), # No API Key needed
348366
guardrails=[
349367
ContentFilter(blocked_words=["password", "secret"]),
350368
PIIScanner(redact=True),
@@ -433,7 +451,8 @@ from Syrin import Agent, Model
433451
from Syrin.loop import SingleShotLoop
434452

435453
agent = Agent(
436-
model=Model.OpenAI("gpt-4o-mini"),
454+
# model=Model.OpenAI("gpt-4o-mini"),
455+
model=Model.Almock(), # No API Key needed
437456
system_prompt="You are a helpful assistant.",
438457
loop=SingleShotLoop(),
439458
)
@@ -451,7 +470,8 @@ from Syrin import Agent, Model
451470
from Syrin.loop import ReactLoop
452471

453472
agent = Agent(
454-
model=Model.OpenAI("gpt-4o-mini"),
473+
# model=Model.OpenAI("gpt-4o-mini"),
474+
model=Model.Almock(), # No API Key needed
455475
system_prompt="You are a helpful assistant.",
456476
loop=ReactLoop(), # Default when tools are present
457477
tools=[my_tool],
@@ -473,7 +493,8 @@ async def approve_tool(tool_name: str, arguments: dict) -> bool:
473493
return response.lower() == 'y'
474494

475495
agent = Agent(
476-
model=Model.OpenAI("gpt-4o-mini"),
496+
# model=Model.OpenAI("gpt-4o-mini"),
497+
model=Model.Almock(), # No API Key needed
477498
system_prompt="You are a helpful assistant.",
478499
loop=HumanInTheLoop(approve_tool),
479500
tools=[dangerous_tool],
@@ -490,7 +511,8 @@ The `PlanExecuteLoop` is a 3-phase loop that first generates a plan, then execut
490511
from Syrin.loop import PlanExecuteLoop
491512

492513
agent = Agent(
493-
model=Model.OpenAI("gpt-4o-mini"),
514+
# model=Model.OpenAI("gpt-4o-mini"),
515+
model=Model.Almock(), # No API Key needed
494516
loop=PlanExecuteLoop(
495517
max_plan_iterations=3, # Max iterations for planning
496518
max_execution_iterations=20, # Max iterations for execution
@@ -510,7 +532,8 @@ The `CodeActionLoop` generates Python code, executes it, and interprets the resu
510532
from Syrin.loop import CodeActionLoop
511533

512534
agent = Agent(
513-
model=Model.OpenAI("gpt-4o-mini"),
535+
# model=Model.OpenAI("gpt-4o-mini"),
536+
model=Model.Almock(), # No API Key needed
514537
loop=CodeActionLoop(
515538
max_iterations=5, # Max code generation attempts
516539
timeout_seconds=30, # Code execution timeout
@@ -545,7 +568,8 @@ class MyCustomLoop(Loop):
545568
)
546569

547570
agent = Agent(
548-
model=Model.OpenAI("gpt-4o-mini"),
571+
# model=Model.OpenAI("gpt-4o-mini"),
572+
model=Model.Almock(), # No API Key needed
549573
loop=MyCustomLoop(),
550574
)
551575
```
@@ -617,7 +641,8 @@ tracer = get_tracer()
617641
tracer.add_exporter(ConsoleExporter())
618642

619643
agent = Agent(
620-
model=Model.OpenAI("gpt-4o"),
644+
# model=Model.OpenAI("gpt-4o"),
645+
model=Model.Almock(), # No API Key needed
621646
system_prompt="You are a professional customer support agent.",
622647
budget=budget,
623648
guardrails=guardrails,

docs/agent/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ Complete documentation for creating and configuring AI agents in Syrin. This dir
77
## Quick Start
88

99
```python
10-
from syrin import Agent
11-
from syrin.model import Model
10+
from syrin import Agent, Model
1211

1312
agent = Agent(
14-
model=Model.OpenAI("gpt-4o-mini"),
13+
# model=Model.OpenAI("gpt-4o-mini"),
14+
model=Model.Almock(), # No API Key needed
1515
system_prompt="You are a helpful assistant.",
1616
)
1717
response = agent.response("Hello!")

docs/agent/constructor.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@ LLM used by the agent.
4343
```python
4444
from syrin.model import Model
4545

46-
agent = Agent(model=Model.OpenAI("gpt-4o-mini"))
47-
agent = Agent(model=Model.Anthropic("claude-sonnet-4-5"))
48-
agent = Agent(model=Model.Ollama("llama3"))
46+
agent = Agent(
47+
# model=Model.OpenAI("gpt-4o-mini"),
48+
model=Model.Almock(), # No API Key needed
49+
)
50+
# Or: Model.Anthropic("claude-sonnet-4-5"), Model.Ollama("llama3"), etc.
4951
```
5052

5153
Must be provided on the class or at construction. Raises `TypeError` if missing.
@@ -390,7 +392,8 @@ def search(query: str) -> str:
390392
return f"Results: {query}"
391393

392394
agent = Agent(
393-
model=Model.OpenAI("gpt-4o-mini"),
395+
# model=Model.OpenAI("gpt-4o-mini"),
396+
model=Model.Almock(), # No API Key needed
394397
system_prompt="You are a research assistant.",
395398
tools=[search],
396399
budget=Budget(run=0.50),

0 commit comments

Comments
 (0)