agentforge-oss 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/PKG-INFO +42 -17
  2. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/README.md +31 -16
  3. agentforge_oss-0.3.0/examples/otel_tracing.py +34 -0
  4. agentforge_oss-0.3.0/examples/rag_persistent.py +31 -0
  5. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/__init__.py +15 -13
  6. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/_version.py +1 -1
  7. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/agents/agent.py +0 -1
  8. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/agents/base.py +5 -1
  9. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/agents/supervisor.py +0 -12
  10. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/compliance/audit.py +7 -2
  11. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/config.py +34 -8
  12. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/exceptions.py +0 -15
  13. agentforge_oss-0.3.0/forge/memory/__init__.py +35 -0
  14. agentforge_oss-0.3.0/forge/memory/sqlite.py +147 -0
  15. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/models/providers/__init__.py +2 -1
  16. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/models/providers/anthropic.py +0 -7
  17. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/models/providers/echo.py +0 -4
  18. agentforge_oss-0.3.0/forge/models/providers/ollama.py +241 -0
  19. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/models/providers/openai.py +0 -8
  20. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/models/registry.py +69 -3
  21. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/models/router.py +0 -1
  22. agentforge_oss-0.3.0/forge/observability/__init__.py +34 -0
  23. agentforge_oss-0.3.0/forge/observability/otel.py +319 -0
  24. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/orchestration/orchestrator.py +58 -11
  25. agentforge_oss-0.3.0/forge/tools/builtin/http.py +62 -0
  26. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/types.py +0 -1
  27. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/pyproject.toml +16 -3
  28. agentforge_oss-0.3.0/tests/test_audit_robustness.py +23 -0
  29. agentforge_oss-0.3.0/tests/test_config_validation.py +34 -0
  30. agentforge_oss-0.3.0/tests/test_http_security.py +68 -0
  31. agentforge_oss-0.3.0/tests/test_ollama_provider.py +157 -0
  32. agentforge_oss-0.3.0/tests/test_otel.py +119 -0
  33. agentforge_oss-0.3.0/tests/test_sqlite_concurrency.py +30 -0
  34. agentforge_oss-0.3.0/tests/test_sqlite_memory.py +78 -0
  35. agentforge_oss-0.2.0/forge/memory/__init__.py +0 -7
  36. agentforge_oss-0.2.0/forge/observability/__init__.py +0 -16
  37. agentforge_oss-0.2.0/forge/tools/builtin/http.py +0 -36
  38. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/.gitignore +0 -0
  39. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/LICENSE +0 -0
  40. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/examples/enterprise_governance.py +0 -0
  41. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/examples/quickstart.py +0 -0
  42. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/examples/rag.py +0 -0
  43. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/examples/tool_use.py +0 -0
  44. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/agents/__init__.py +0 -0
  45. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/cli/__init__.py +0 -0
  46. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/cli/console.py +0 -0
  47. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/cli/main.py +0 -0
  48. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/compliance/__init__.py +0 -0
  49. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/compliance/redaction.py +0 -0
  50. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/memory/base.py +0 -0
  51. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/memory/conversation.py +0 -0
  52. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/memory/vector.py +0 -0
  53. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/models/__init__.py +0 -0
  54. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/models/base.py +0 -0
  55. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/observability/events.py +0 -0
  56. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/observability/logging.py +0 -0
  57. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/observability/usage.py +0 -0
  58. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/orchestration/__init__.py +0 -0
  59. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/orchestration/context.py +0 -0
  60. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/py.typed +0 -0
  61. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/security/__init__.py +0 -0
  62. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/security/access.py +0 -0
  63. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/security/sandbox.py +0 -0
  64. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/security/sanitization.py +0 -0
  65. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/tools/__init__.py +0 -0
  66. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/tools/base.py +0 -0
  67. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/tools/builtin/__init__.py +0 -0
  68. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/tools/builtin/calculator.py +0 -0
  69. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/tools/builtin/time.py +0 -0
  70. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/forge/tools/registry.py +0 -0
  71. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/tests/conftest.py +0 -0
  72. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/tests/test_agents.py +0 -0
  73. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/tests/test_audit.py +0 -0
  74. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/tests/test_memory.py +0 -0
  75. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/tests/test_models.py +0 -0
  76. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/tests/test_openai_provider.py +0 -0
  77. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/tests/test_orchestrator.py +0 -0
  78. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/tests/test_parallel_workers.py +0 -0
  79. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/tests/test_security.py +0 -0
  80. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/tests/test_streaming.py +0 -0
  81. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/tests/test_tools.py +0 -0
  82. {agentforge_oss-0.2.0 → agentforge_oss-0.3.0}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentforge-oss
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Forge — an open-source, enterprise-ready multi-agent orchestration platform with cost awareness, governance, and security built in.
5
5
  Project-URL: Homepage, https://github.com/sekacorn/AgentForge
6
6
  Project-URL: Documentation, https://github.com/sekacorn/AgentForge#readme
@@ -30,8 +30,12 @@ Requires-Dist: rich>=13.7
30
30
  Requires-Dist: typer>=0.12
31
31
  Requires-Dist: typing-extensions>=4.10
32
32
  Provides-Extra: all
33
+ Requires-Dist: aiosqlite>=0.19; extra == 'all'
33
34
  Requires-Dist: anthropic>=0.20; extra == 'all'
34
35
  Requires-Dist: openai>=1.0; extra == 'all'
36
+ Requires-Dist: opentelemetry-api>=1.20; extra == 'all'
37
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20; extra == 'all'
38
+ Requires-Dist: opentelemetry-sdk>=1.20; extra == 'all'
35
39
  Provides-Extra: anthropic
36
40
  Requires-Dist: anthropic>=0.20; extra == 'anthropic'
37
41
  Provides-Extra: dev
@@ -42,6 +46,12 @@ Requires-Dist: pytest>=8.0; extra == 'dev'
42
46
  Requires-Dist: ruff>=0.6; extra == 'dev'
43
47
  Provides-Extra: openai
44
48
  Requires-Dist: openai>=1.0; extra == 'openai'
49
+ Provides-Extra: otel
50
+ Requires-Dist: opentelemetry-api>=1.20; extra == 'otel'
51
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20; extra == 'otel'
52
+ Requires-Dist: opentelemetry-sdk>=1.20; extra == 'otel'
53
+ Provides-Extra: sqlite
54
+ Requires-Dist: aiosqlite>=0.19; extra == 'sqlite'
45
55
  Description-Content-Type: text/markdown
46
56
 
47
57
  <div align="center">
@@ -58,7 +68,7 @@ with cost-awareness, security, and compliance built in from line one.**
58
68
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
59
69
  [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)
60
70
  [![Typed](https://img.shields.io/badge/typed-mypy%20strict-blue.svg)](pyproject.toml)
61
- [![Status: Beta](https://img.shields.io/badge/status-beta-orange.svg)](#whats-shipped-v020)
71
+ [![Status: Beta](https://img.shields.io/badge/status-beta-orange.svg)](#whats-shipped-v030)
62
72
 
63
73
  </div>
64
74
 
@@ -101,7 +111,7 @@ asyncio.run(main())
101
111
 
102
112
  ---
103
113
 
104
- ## What's shipped (v0.2.0)
114
+ ## What's shipped (v0.3.0)
105
115
 
106
116
  An honest snapshot of what works today versus what is on the way. Everything marked
107
117
  **Shipped** is implemented, typed, and covered by the test suite.
@@ -113,6 +123,7 @@ An honest snapshot of what works today versus what is on the way. Everything mar
113
123
  | Anthropic provider (Claude Haiku 4.5, Sonnet 4.6, Opus 4.8, Fable 5) | Shipped |
114
124
  | OpenAI provider (gpt-4o-mini, gpt-4o, gpt-4.1, o3) | Shipped |
115
125
  | Offline deterministic provider (zero config, no API key) | Shipped |
126
+ | Ollama provider (local models, zero cost, no API key, auto-detected) | Shipped |
116
127
  | Pre-flight + per-step budget caps | Shipped |
117
128
  | Tool sandboxing (allowlist/denylist, timeouts, dangerous-denied-by-default) | Shipped |
118
129
  | RBAC (admin / operator / developer / viewer) | Shipped |
@@ -122,12 +133,13 @@ An honest snapshot of what works today versus what is on the way. Everything mar
122
133
  | Event bus (21 lifecycle event types) | Shipped |
123
134
  | Streaming token output through the event bus (`stream=True`) | Shipped |
124
135
  | Per-run cost reporting (tokens + USD, per model, per agent) | Shipped |
136
+ | OpenTelemetry export (traces + metrics: console or OTLP to Jaeger/Grafana/Datadog) | Shipped |
125
137
  | Conversation memory + in-memory RAG vector store | Shipped |
138
+ | Durable memory backend (SQLite, persistent RAG, no vector extension) | Shipped |
126
139
  | CLI (`forge run`, `forge models`, `forge audit`) | Shipped |
127
- | 47 tests, mypy strict, ruff clean, CI on 3.11 / 3.12 / 3.13 | Shipped |
128
- | Durable memory backends (pgvector, SQLite-VSS) | Planned |
129
- | OpenTelemetry export for traces and metrics | Planned |
130
- | Ollama / Bedrock / Vertex providers | Planned |
140
+ | 77 tests, mypy strict, ruff clean, CI on 3.11 / 3.12 / 3.13 | Shipped |
141
+ | Durable memory backends (pgvector, Redis) | Planned |
142
+ | Bedrock / Vertex providers | Planned |
131
143
  | Policy-as-code for tool governance | Planned |
132
144
  | Hosted SaaS control plane (TypeScript / Next.js) | Future |
133
145
 
@@ -177,9 +189,9 @@ An honest snapshot of what works today versus what is on the way. Everything mar
177
189
  JSON-Schema generated automatically from your type hints and docstring.
178
190
  - **Pluggable memory.** Short-term conversation memory plus a dependency-free
179
191
  in-memory vector store for RAG — swap in any backend behind one tiny interface.
180
- - **Provider-agnostic core.** Anthropic (Claude) and OpenAI (GPT / o-series) ship in
181
- the box alongside a deterministic offline echo provider; add any provider by
182
- implementing one method.
192
+ - **Provider-agnostic core.** Anthropic (Claude), OpenAI (GPT / o-series), and Ollama
193
+ (local models, zero cost) ship in the box alongside a deterministic offline echo
194
+ provider; add any provider by implementing one method.
183
195
 
184
196
  ### Security from the start
185
197
  - **Tool sandboxing** with allowlists/denylists, per-tool timeouts, and
@@ -204,6 +216,10 @@ An honest snapshot of what works today versus what is on the way. Everything mar
204
216
  `TOKEN_STREAM_START` / `TOKEN_CHUNK` / `TOKEN_STREAM_END` events — each tagged with
205
217
  the agent that produced it, so you can render live output even across parallel
206
218
  workers. `forge run "..." --stream` gives the classic live-typing terminal feel.
219
+ - **OpenTelemetry export.** Every run becomes a tree of spans (`forge.run` →
220
+ `forge.agent` → `forge.model_call` → `forge.tool_call`) exportable to any
221
+ OTel-compatible backend — Jaeger, Grafana, Datadog, Honeycomb, New Relic. Console
222
+ exporter by default (zero infra); set an OTLP endpoint for production.
207
223
  - **Structured logging** (human or JSON) and a per-run **usage/cost report** broken
208
224
  down per model and per agent.
209
225
 
@@ -229,6 +245,15 @@ pip install "agentforge-oss[all,dev]" # everything + test/lint toolin
229
245
  > to route to GPT. Both keys can be set at once; Forge prefers Anthropic by default
230
246
  > (configurable).
231
247
 
248
+ > **Ollama support is built in** — no extra install needed (it uses `httpx`, already a
249
+ > core dependency, so there is no `[ollama]` extra). Just run Ollama locally and Forge
250
+ > auto-detects it (or set `OLLAMA_BASE_URL` for a custom/remote server):
251
+
252
+ ```bash
253
+ ollama serve
254
+ ollama pull llama3.1:8b
255
+ ```
256
+
232
257
  ---
233
258
 
234
259
  ## Quickstart (CLI)
@@ -341,10 +366,10 @@ governance (RBAC + budgets + audit verification).
341
366
  │ │
342
367
  │ ┌───────────────────┴───────────────┐
343
368
  ▼ ▼ ▼
344
- ┌────────────────┐ ┌───────────────────────────┐
345
- │ Model Router │ picks model by │ Model Providers
346
- │ cost / quality │ strategy + budget ───▶│ Anthropic · OpenAI · Echo │
347
- └───────┬────────┘ └───────────────────────────┘
369
+ ┌────────────────┐ ┌────────────────────────────────────┐
370
+ │ Model Router │ picks model by │ Model Providers
371
+ │ cost / quality │ strategy + budget ───▶│ Anthropic · OpenAI · Ollama · Echo │
372
+ └───────┬────────┘ └────────────────────────────────────┘
348
373
  │ pricing
349
374
 
350
375
  ┌────────────────┐ cross-cutting, on every step:
@@ -356,9 +381,9 @@ Every layer is swappable:
356
381
 
357
382
  | Layer | Default | Swap in… |
358
383
  |---|---|---|
359
- | Provider | Echo (offline), Anthropic, OpenAI | Any `ModelProvider` (local, Bedrock, Vertex, …) |
384
+ | Provider | Echo (offline), Anthropic, OpenAI, Ollama | Any `ModelProvider` (Bedrock, Vertex, …) |
360
385
  | Routing | `balanced` strategy | Your own strategy / `fixed` model |
361
- | Memory | In-memory vector store | Any `Memory` backend (pgvector, Pinecone, …) |
386
+ | Memory | InMemoryVectorStore (default), SQLiteMemoryStore | Any `Memory` backend (pgvector, Redis, …) |
362
387
  | Tools | `calculator`, `utc_now` | Any `@tool` function |
363
388
  | Audit | Hash-chained JSONL | Forward events to your SIEM via the event bus |
364
389
 
@@ -449,7 +474,7 @@ ruff check . # lint
449
474
  mypy forge # strict type-check
450
475
  ```
451
476
 
452
- The entire 47-test suite runs offline against the deterministic provider — fast,
477
+ The entire 77-test suite runs offline against the deterministic provider — fast,
453
478
  hermetic, and free. CI runs the same checks (ruff, ruff format, mypy strict, pytest)
454
479
  on Python 3.11, 3.12, and 3.13.
455
480
 
@@ -12,7 +12,7 @@ with cost-awareness, security, and compliance built in from line one.**
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
13
13
  [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)
14
14
  [![Typed](https://img.shields.io/badge/typed-mypy%20strict-blue.svg)](pyproject.toml)
15
- [![Status: Beta](https://img.shields.io/badge/status-beta-orange.svg)](#whats-shipped-v020)
15
+ [![Status: Beta](https://img.shields.io/badge/status-beta-orange.svg)](#whats-shipped-v030)
16
16
 
17
17
  </div>
18
18
 
@@ -55,7 +55,7 @@ asyncio.run(main())
55
55
 
56
56
  ---
57
57
 
58
- ## What's shipped (v0.2.0)
58
+ ## What's shipped (v0.3.0)
59
59
 
60
60
  An honest snapshot of what works today versus what is on the way. Everything marked
61
61
  **Shipped** is implemented, typed, and covered by the test suite.
@@ -67,6 +67,7 @@ An honest snapshot of what works today versus what is on the way. Everything mar
67
67
  | Anthropic provider (Claude Haiku 4.5, Sonnet 4.6, Opus 4.8, Fable 5) | Shipped |
68
68
  | OpenAI provider (gpt-4o-mini, gpt-4o, gpt-4.1, o3) | Shipped |
69
69
  | Offline deterministic provider (zero config, no API key) | Shipped |
70
+ | Ollama provider (local models, zero cost, no API key, auto-detected) | Shipped |
70
71
  | Pre-flight + per-step budget caps | Shipped |
71
72
  | Tool sandboxing (allowlist/denylist, timeouts, dangerous-denied-by-default) | Shipped |
72
73
  | RBAC (admin / operator / developer / viewer) | Shipped |
@@ -76,12 +77,13 @@ An honest snapshot of what works today versus what is on the way. Everything mar
76
77
  | Event bus (21 lifecycle event types) | Shipped |
77
78
  | Streaming token output through the event bus (`stream=True`) | Shipped |
78
79
  | Per-run cost reporting (tokens + USD, per model, per agent) | Shipped |
80
+ | OpenTelemetry export (traces + metrics: console or OTLP to Jaeger/Grafana/Datadog) | Shipped |
79
81
  | Conversation memory + in-memory RAG vector store | Shipped |
82
+ | Durable memory backend (SQLite, persistent RAG, no vector extension) | Shipped |
80
83
  | CLI (`forge run`, `forge models`, `forge audit`) | Shipped |
81
- | 47 tests, mypy strict, ruff clean, CI on 3.11 / 3.12 / 3.13 | Shipped |
82
- | Durable memory backends (pgvector, SQLite-VSS) | Planned |
83
- | OpenTelemetry export for traces and metrics | Planned |
84
- | Ollama / Bedrock / Vertex providers | Planned |
84
+ | 77 tests, mypy strict, ruff clean, CI on 3.11 / 3.12 / 3.13 | Shipped |
85
+ | Durable memory backends (pgvector, Redis) | Planned |
86
+ | Bedrock / Vertex providers | Planned |
85
87
  | Policy-as-code for tool governance | Planned |
86
88
  | Hosted SaaS control plane (TypeScript / Next.js) | Future |
87
89
 
@@ -131,9 +133,9 @@ An honest snapshot of what works today versus what is on the way. Everything mar
131
133
  JSON-Schema generated automatically from your type hints and docstring.
132
134
  - **Pluggable memory.** Short-term conversation memory plus a dependency-free
133
135
  in-memory vector store for RAG — swap in any backend behind one tiny interface.
134
- - **Provider-agnostic core.** Anthropic (Claude) and OpenAI (GPT / o-series) ship in
135
- the box alongside a deterministic offline echo provider; add any provider by
136
- implementing one method.
136
+ - **Provider-agnostic core.** Anthropic (Claude), OpenAI (GPT / o-series), and Ollama
137
+ (local models, zero cost) ship in the box alongside a deterministic offline echo
138
+ provider; add any provider by implementing one method.
137
139
 
138
140
  ### Security from the start
139
141
  - **Tool sandboxing** with allowlists/denylists, per-tool timeouts, and
@@ -158,6 +160,10 @@ An honest snapshot of what works today versus what is on the way. Everything mar
158
160
  `TOKEN_STREAM_START` / `TOKEN_CHUNK` / `TOKEN_STREAM_END` events — each tagged with
159
161
  the agent that produced it, so you can render live output even across parallel
160
162
  workers. `forge run "..." --stream` gives the classic live-typing terminal feel.
163
+ - **OpenTelemetry export.** Every run becomes a tree of spans (`forge.run` →
164
+ `forge.agent` → `forge.model_call` → `forge.tool_call`) exportable to any
165
+ OTel-compatible backend — Jaeger, Grafana, Datadog, Honeycomb, New Relic. Console
166
+ exporter by default (zero infra); set an OTLP endpoint for production.
161
167
  - **Structured logging** (human or JSON) and a per-run **usage/cost report** broken
162
168
  down per model and per agent.
163
169
 
@@ -183,6 +189,15 @@ pip install "agentforge-oss[all,dev]" # everything + test/lint toolin
183
189
  > to route to GPT. Both keys can be set at once; Forge prefers Anthropic by default
184
190
  > (configurable).
185
191
 
192
+ > **Ollama support is built in** — no extra install needed (it uses `httpx`, already a
193
+ > core dependency, so there is no `[ollama]` extra). Just run Ollama locally and Forge
194
+ > auto-detects it (or set `OLLAMA_BASE_URL` for a custom/remote server):
195
+
196
+ ```bash
197
+ ollama serve
198
+ ollama pull llama3.1:8b
199
+ ```
200
+
186
201
  ---
187
202
 
188
203
  ## Quickstart (CLI)
@@ -295,10 +310,10 @@ governance (RBAC + budgets + audit verification).
295
310
  │ │
296
311
  │ ┌───────────────────┴───────────────┐
297
312
  ▼ ▼ ▼
298
- ┌────────────────┐ ┌───────────────────────────┐
299
- │ Model Router │ picks model by │ Model Providers
300
- │ cost / quality │ strategy + budget ───▶│ Anthropic · OpenAI · Echo │
301
- └───────┬────────┘ └───────────────────────────┘
313
+ ┌────────────────┐ ┌────────────────────────────────────┐
314
+ │ Model Router │ picks model by │ Model Providers
315
+ │ cost / quality │ strategy + budget ───▶│ Anthropic · OpenAI · Ollama · Echo │
316
+ └───────┬────────┘ └────────────────────────────────────┘
302
317
  │ pricing
303
318
 
304
319
  ┌────────────────┐ cross-cutting, on every step:
@@ -310,9 +325,9 @@ Every layer is swappable:
310
325
 
311
326
  | Layer | Default | Swap in… |
312
327
  |---|---|---|
313
- | Provider | Echo (offline), Anthropic, OpenAI | Any `ModelProvider` (local, Bedrock, Vertex, …) |
328
+ | Provider | Echo (offline), Anthropic, OpenAI, Ollama | Any `ModelProvider` (Bedrock, Vertex, …) |
314
329
  | Routing | `balanced` strategy | Your own strategy / `fixed` model |
315
- | Memory | In-memory vector store | Any `Memory` backend (pgvector, Pinecone, …) |
330
+ | Memory | InMemoryVectorStore (default), SQLiteMemoryStore | Any `Memory` backend (pgvector, Redis, …) |
316
331
  | Tools | `calculator`, `utc_now` | Any `@tool` function |
317
332
  | Audit | Hash-chained JSONL | Forward events to your SIEM via the event bus |
318
333
 
@@ -403,7 +418,7 @@ ruff check . # lint
403
418
  mypy forge # strict type-check
404
419
  ```
405
420
 
406
- The entire 47-test suite runs offline against the deterministic provider — fast,
421
+ The entire 77-test suite runs offline against the deterministic provider — fast,
407
422
  hermetic, and free. CI runs the same checks (ruff, ruff format, mypy strict, pytest)
408
423
  on Python 3.11, 3.12, and 3.13.
409
424
 
@@ -0,0 +1,34 @@
1
+ """
2
+ OpenTelemetry tracing example — exports spans to the console.
3
+
4
+ pip install "agentforge-oss[otel]"
5
+ python examples/otel_tracing.py
6
+
7
+ To send traces to Jaeger, Grafana Tempo, or any OTel collector instead:
8
+
9
+ FORGE_OTEL_ENDPOINT=http://localhost:4317 python examples/otel_tracing.py
10
+ """
11
+
12
+ import asyncio
13
+ import os
14
+
15
+ from forge import ForgeConfig, Orchestrator
16
+
17
+
18
+ async def main() -> None:
19
+ config = ForgeConfig(
20
+ otel_enabled=True,
21
+ otel_service_name="forge-demo",
22
+ otel_endpoint=os.environ.get("FORGE_OTEL_ENDPOINT"),
23
+ )
24
+ async with Orchestrator(config) as forge:
25
+ result = await forge.run(
26
+ "Calculate 15% of 4200 and summarize the result",
27
+ mode="single",
28
+ )
29
+ print(result.output)
30
+ print(result.usage.format_table())
31
+ print("\nSpans exported above (ConsoleSpanExporter).")
32
+
33
+
34
+ asyncio.run(main())
@@ -0,0 +1,31 @@
1
+ """
2
+ Persistent RAG example — memories survive process restarts.
3
+
4
+ Run this script twice. The second run finds the facts stored by the first.
5
+
6
+ pip install "agentforge-oss[sqlite]"
7
+ python examples/rag_persistent.py
8
+ """
9
+
10
+ import asyncio
11
+
12
+ from forge.memory.sqlite import SQLiteMemoryStore
13
+
14
+
15
+ async def main() -> None:
16
+ async with SQLiteMemoryStore("demo_memory.db") as store:
17
+ count_before = len(await store.search("forge", k=100))
18
+ if count_before == 0:
19
+ print("First run — storing facts...")
20
+ await store.add("Forge routes cheap tasks to small models to save cost.")
21
+ await store.add("The audit log is hash-chained and tamper-evident.")
22
+ await store.add("Ollama runs local LLMs with zero API cost.")
23
+ print("Stored 3 facts. Run again to retrieve them.")
24
+ else:
25
+ print(f"Second run — found {count_before} stored facts.")
26
+ hits = await store.search("how does Forge reduce cost?", k=2)
27
+ for hit in hits:
28
+ print(f" [{hit.score:.3f}] {hit.text}")
29
+
30
+
31
+ asyncio.run(main())
@@ -51,7 +51,13 @@ from forge.exceptions import (
51
51
  ToolTimeoutError,
52
52
  ToolValidationError,
53
53
  )
54
- from forge.memory import ConversationMemory, InMemoryVectorStore, Memory, MemoryItem
54
+ from forge.memory import (
55
+ ConversationMemory,
56
+ InMemoryVectorStore,
57
+ Memory,
58
+ MemoryItem,
59
+ SQLiteMemoryStore,
60
+ )
55
61
  from forge.models import (
56
62
  Complexity,
57
63
  ModelInfo,
@@ -61,7 +67,12 @@ from forge.models import (
61
67
  ModelTier,
62
68
  RoutingDecision,
63
69
  )
64
- from forge.models.providers import AnthropicProvider, EchoProvider, OpenAIProvider
70
+ from forge.models.providers import (
71
+ AnthropicProvider,
72
+ EchoProvider,
73
+ OllamaProvider,
74
+ OpenAIProvider,
75
+ )
65
76
  from forge.observability import (
66
77
  Event,
67
78
  EventBus,
@@ -93,23 +104,19 @@ from forge.types import (
93
104
 
94
105
  __all__ = [
95
106
  "__version__",
96
- # Orchestration
97
107
  "Orchestrator",
98
108
  "RunResult",
99
109
  "RunContext",
100
- # Agents
101
110
  "Agent",
102
111
  "Supervisor",
103
112
  "BaseAgent",
104
113
  "AgentResult",
105
- # Config
106
114
  "ForgeConfig",
107
115
  "RoutingConfig",
108
116
  "BudgetConfig",
109
117
  "SecurityConfig",
110
118
  "ComplianceConfig",
111
119
  "ObservabilityConfig",
112
- # Types
113
120
  "Role",
114
121
  "Message",
115
122
  "ToolCall",
@@ -118,7 +125,6 @@ __all__ = [
118
125
  "Usage",
119
126
  "ModelResponse",
120
127
  "FinishReason",
121
- # Models
122
128
  "ModelProvider",
123
129
  "ModelRegistry",
124
130
  "ModelInfo",
@@ -129,7 +135,7 @@ __all__ = [
129
135
  "EchoProvider",
130
136
  "AnthropicProvider",
131
137
  "OpenAIProvider",
132
- # Tools
138
+ "OllamaProvider",
133
139
  "Tool",
134
140
  "tool",
135
141
  "ToolRegistry",
@@ -137,12 +143,11 @@ __all__ = [
137
143
  "calculator",
138
144
  "http_get",
139
145
  "utc_now",
140
- # Memory
141
146
  "Memory",
142
147
  "MemoryItem",
143
148
  "ConversationMemory",
144
149
  "InMemoryVectorStore",
145
- # Observability
150
+ "SQLiteMemoryStore",
146
151
  "Event",
147
152
  "EventBus",
148
153
  "EventType",
@@ -150,17 +155,14 @@ __all__ = [
150
155
  "UsageReport",
151
156
  "get_logger",
152
157
  "configure_logging",
153
- # Security
154
158
  "InputSanitizer",
155
159
  "ToolSandbox",
156
160
  "AccessController",
157
161
  "Principal",
158
162
  "Permission",
159
- # Compliance
160
163
  "AuditLogger",
161
164
  "AuditEntry",
162
165
  "PIIRedactor",
163
- # Exceptions
164
166
  "ForgeError",
165
167
  "ConfigurationError",
166
168
  "ProviderError",
@@ -5,4 +5,4 @@ Hatchling reads ``__version__`` from this file at build time (see
5
5
  it for runtime access via ``forge.__version__``.
6
6
  """
7
7
 
8
- __version__ = "0.2.0"
8
+ __version__ = "0.3.0"
@@ -52,7 +52,6 @@ class Agent(BaseAgent):
52
52
  agent=self.name, output=response.content, usage=self._own_usage(), steps=steps
53
53
  )
54
54
 
55
- # Loop exhausted without a final answer.
56
55
  ctx.events.emit(
57
56
  EventType.AGENT_FAILED, run_id=ctx.run_id, agent=self.name, reason="max_steps_exceeded"
58
57
  )
@@ -105,7 +105,11 @@ class BaseAgent(abc.ABC):
105
105
  max_tokens = min(self.max_tokens, info.max_output_tokens)
106
106
 
107
107
  ctx.events.emit(
108
- EventType.MODEL_CALL_STARTED, run_id=ctx.run_id, agent=self.name, model=decision.model
108
+ EventType.MODEL_CALL_STARTED,
109
+ run_id=ctx.run_id,
110
+ agent=self.name,
111
+ model=decision.model,
112
+ provider=decision.provider,
109
113
  )
110
114
  try:
111
115
  if ctx.stream:
@@ -67,7 +67,6 @@ class Supervisor(BaseAgent):
67
67
  )
68
68
  ctx.audit.record("agent.start", actor=self.name, run_id=ctx.run_id, resource=goal[:200])
69
69
 
70
- # 1) Plan.
71
70
  plan = await self._plan(goal)
72
71
  ctx.events.emit(EventType.PLAN_CREATED, run_id=ctx.run_id, agent=self.name, subtasks=plan)
73
72
  ctx.audit.record(
@@ -78,10 +77,8 @@ class Supervisor(BaseAgent):
78
77
  subtasks=plan,
79
78
  )
80
79
 
81
- # 2) Delegate subtasks to workers, running each batch concurrently.
82
80
  children = await self._run_workers(plan)
83
81
 
84
- # 3) Synthesise a final answer and assemble the report.
85
82
  summary = await self._synthesize(goal, children)
86
83
  report = self._format_report(goal, plan, children, summary)
87
84
 
@@ -102,9 +99,6 @@ class Supervisor(BaseAgent):
102
99
  children=children,
103
100
  )
104
101
 
105
- # ------------------------------------------------------------------ #
106
- # Parallel worker execution
107
- # ------------------------------------------------------------------ #
108
102
  async def _run_workers(self, plan: list[str]) -> list[AgentResult]:
109
103
  """Run the planned subtasks as workers, concurrently and in bounded batches.
110
104
 
@@ -194,9 +188,6 @@ class Supervisor(BaseAgent):
194
188
  success=False,
195
189
  )
196
190
 
197
- # ------------------------------------------------------------------ #
198
- # Planning & synthesis
199
- # ------------------------------------------------------------------ #
200
191
  async def _plan(self, goal: str) -> list[str]:
201
192
  prompt = (
202
193
  f"{PLAN_MARKER} You are a planning supervisor. Decompose the goal into a minimal, "
@@ -225,9 +216,6 @@ class Supervisor(BaseAgent):
225
216
  response = await self._invoke_model(self._with_system(prompt), complexity=Complexity.MEDIUM)
226
217
  return response.content
227
218
 
228
- # ------------------------------------------------------------------ #
229
- # Helpers
230
- # ------------------------------------------------------------------ #
231
219
  def _with_system(self, user_content: str) -> list[Message]:
232
220
  messages: list[Message] = []
233
221
  if self.system_prompt:
@@ -118,7 +118,13 @@ class AuditLogger:
118
118
  line = line.strip()
119
119
  if not line:
120
120
  continue
121
- entry = AuditEntry.model_validate_json(line)
121
+ try:
122
+ entry = AuditEntry.model_validate_json(line)
123
+ except ValueError:
124
+ # A line that no longer parses is itself tampering: fail closed
125
+ # (return False) rather than raising out of a bool-returning check.
126
+ _log.error("audit chain broken at line %d (unparseable record)", line_no)
127
+ return False
122
128
  if entry.previous_hash != prev:
123
129
  _log.error("audit chain broken at line %d (prev_hash mismatch)", line_no)
124
130
  return False
@@ -128,7 +134,6 @@ class AuditLogger:
128
134
  prev = entry.hash
129
135
  return True
130
136
 
131
- # ------------------------------------------------------------------ #
132
137
  @staticmethod
133
138
  def _compute_hash(entry: AuditEntry) -> str:
134
139
  digest = hashlib.sha256()
@@ -23,6 +23,7 @@ from pydantic import BaseModel, Field
23
23
  from forge.exceptions import ConfigurationError
24
24
 
25
25
  RoutingStrategy = Literal["balanced", "cost_optimized", "quality_first", "fixed"]
26
+ MemoryBackend = Literal["inmemory", "sqlite"]
26
27
 
27
28
 
28
29
  def _env_bool(name: str, default: bool) -> bool:
@@ -61,13 +62,13 @@ class RoutingConfig(BaseModel):
61
62
  class BudgetConfig(BaseModel):
62
63
  """Hard limits that keep an autonomous run from running away with spend."""
63
64
 
64
- max_usd_per_run: float | None = None
65
- max_tokens_per_run: int | None = None
65
+ max_usd_per_run: float | None = Field(default=None, ge=0)
66
+ max_tokens_per_run: int | None = Field(default=None, ge=1)
66
67
  #: Maximum reasoning/acting iterations a single agent may take.
67
- max_steps_per_agent: int = 12
68
+ max_steps_per_agent: int = Field(default=12, ge=1)
68
69
  #: Maximum number of dynamic workers a supervisor may spawn for one goal.
69
70
  #: Also bounds parallelism: subtasks run in concurrent batches of this size.
70
- max_workers: int = 5
71
+ max_workers: int = Field(default=5, ge=1)
71
72
 
72
73
 
73
74
  class SecurityConfig(BaseModel):
@@ -115,10 +116,22 @@ class ForgeConfig(BaseModel):
115
116
  observability: ObservabilityConfig = Field(default_factory=ObservabilityConfig)
116
117
  #: Provider name -> API key. Populated from the environment by default.
117
118
  api_keys: dict[str, str] = Field(default_factory=dict)
119
+ #: Base URL of a local Ollama server (overridable via ``OLLAMA_BASE_URL``). The
120
+ #: orchestrator offers the Ollama provider when this is set explicitly or when a
121
+ #: server is reachable here; see ``Orchestrator._build_default_providers``.
122
+ ollama_base_url: str = "http://localhost:11434"
123
+ #: Retrieval-memory backend (see ``forge.memory.build_memory``). ``"sqlite"``
124
+ #: persists RAG state across restarts and needs the optional ``aiosqlite`` extra.
125
+ memory_backend: MemoryBackend = "inmemory"
126
+ #: Database file path for the SQLite memory backend.
127
+ memory_path: str = "forge_memory.db"
128
+ #: Export traces + metrics via OpenTelemetry (needs the optional ``otel`` extra).
129
+ otel_enabled: bool = False
130
+ #: OTLP endpoint (e.g. ``http://localhost:4317``); ``None`` exports to the console.
131
+ otel_endpoint: str | None = None
132
+ #: ``service.name`` resource attribute reported to the OTel backend.
133
+ otel_service_name: str = "forge"
118
134
 
119
- # ------------------------------------------------------------------ #
120
- # Loaders
121
- # ------------------------------------------------------------------ #
122
135
  @classmethod
123
136
  def load(cls, path: str | Path | None = None) -> ForgeConfig:
124
137
  """Build a config from (optional) TOML file overlaid with environment.
@@ -145,7 +158,6 @@ class ForgeConfig(BaseModel):
145
158
 
146
159
  def _apply_environment(self) -> None:
147
160
  """Overlay environment variables onto this config in place."""
148
- # Provider API keys (conventional names take priority).
149
161
  for provider, env_name in (
150
162
  ("anthropic", "ANTHROPIC_API_KEY"),
151
163
  ("openai", "OPENAI_API_KEY"),
@@ -154,6 +166,20 @@ class ForgeConfig(BaseModel):
154
166
  if value:
155
167
  self.api_keys[provider] = value
156
168
 
169
+ if (ollama_url := os.environ.get("OLLAMA_BASE_URL")) is not None:
170
+ self.ollama_base_url = ollama_url
171
+
172
+ if (backend := os.environ.get("FORGE_MEMORY_BACKEND")) is not None:
173
+ self.memory_backend = backend # type: ignore[assignment]
174
+ if (mem_path := os.environ.get("FORGE_MEMORY_PATH")) is not None:
175
+ self.memory_path = mem_path
176
+
177
+ self.otel_enabled = _env_bool("FORGE_OTEL_ENABLED", self.otel_enabled)
178
+ if (otel_ep := os.environ.get("FORGE_OTEL_ENDPOINT")) is not None:
179
+ self.otel_endpoint = otel_ep
180
+ if (otel_sn := os.environ.get("FORGE_OTEL_SERVICE_NAME")) is not None:
181
+ self.otel_service_name = otel_sn
182
+
157
183
  if (level := os.environ.get("FORGE_LOG_LEVEL")) is not None:
158
184
  self.observability.log_level = level
159
185
  self.observability.json_logs = _env_bool("FORGE_JSON_LOGS", self.observability.json_logs)
@@ -34,16 +34,10 @@ class ForgeError(Exception):
34
34
  return self.message
35
35
 
36
36
 
37
- # --------------------------------------------------------------------------- #
38
- # Configuration
39
- # --------------------------------------------------------------------------- #
40
37
  class ConfigurationError(ForgeError):
41
38
  """Invalid, missing, or inconsistent configuration."""
42
39
 
43
40
 
44
- # --------------------------------------------------------------------------- #
45
- # Model providers & routing
46
- # --------------------------------------------------------------------------- #
47
41
  class ProviderError(ForgeError):
48
42
  """Base class for errors originating from a model provider."""
49
43
 
@@ -64,9 +58,6 @@ class ModelRoutingError(ForgeError):
64
58
  """No model could be selected that satisfies the routing constraints."""
65
59
 
66
60
 
67
- # --------------------------------------------------------------------------- #
68
- # Tools
69
- # --------------------------------------------------------------------------- #
70
61
  class ToolError(ForgeError):
71
62
  """Base class for tool-related errors."""
72
63
 
@@ -87,9 +78,6 @@ class ToolTimeoutError(ToolError):
87
78
  """A tool exceeded its execution time budget."""
88
79
 
89
80
 
90
- # --------------------------------------------------------------------------- #
91
- # Agents & orchestration
92
- # --------------------------------------------------------------------------- #
93
81
  class AgentError(ForgeError):
94
82
  """Base class for agent execution errors."""
95
83
 
@@ -102,9 +90,6 @@ class OrchestrationError(ForgeError):
102
90
  """An error occurred while coordinating multiple agents."""
103
91
 
104
92
 
105
- # --------------------------------------------------------------------------- #
106
- # Governance: budgets, security, compliance
107
- # --------------------------------------------------------------------------- #
108
93
  class BudgetExceededError(ForgeError):
109
94
  """A run exceeded its configured cost or token budget."""
110
95