django-agent-runtime 0.3.6__py3-none-any.whl

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 (55) hide show
  1. django_agent_runtime/__init__.py +25 -0
  2. django_agent_runtime/admin.py +155 -0
  3. django_agent_runtime/api/__init__.py +26 -0
  4. django_agent_runtime/api/permissions.py +109 -0
  5. django_agent_runtime/api/serializers.py +114 -0
  6. django_agent_runtime/api/views.py +472 -0
  7. django_agent_runtime/apps.py +26 -0
  8. django_agent_runtime/conf.py +241 -0
  9. django_agent_runtime/examples/__init__.py +10 -0
  10. django_agent_runtime/examples/langgraph_adapter.py +164 -0
  11. django_agent_runtime/examples/langgraph_tools.py +179 -0
  12. django_agent_runtime/examples/simple_chat.py +69 -0
  13. django_agent_runtime/examples/tool_agent.py +157 -0
  14. django_agent_runtime/management/__init__.py +2 -0
  15. django_agent_runtime/management/commands/__init__.py +2 -0
  16. django_agent_runtime/management/commands/runagent.py +419 -0
  17. django_agent_runtime/migrations/0001_initial.py +117 -0
  18. django_agent_runtime/migrations/0002_persistence_models.py +129 -0
  19. django_agent_runtime/migrations/0003_persistenceconversation_active_branch_id_and_more.py +212 -0
  20. django_agent_runtime/migrations/0004_add_anonymous_session_id.py +18 -0
  21. django_agent_runtime/migrations/__init__.py +2 -0
  22. django_agent_runtime/models/__init__.py +54 -0
  23. django_agent_runtime/models/base.py +450 -0
  24. django_agent_runtime/models/concrete.py +146 -0
  25. django_agent_runtime/persistence/__init__.py +60 -0
  26. django_agent_runtime/persistence/helpers.py +148 -0
  27. django_agent_runtime/persistence/models.py +506 -0
  28. django_agent_runtime/persistence/stores.py +1191 -0
  29. django_agent_runtime/runtime/__init__.py +23 -0
  30. django_agent_runtime/runtime/events/__init__.py +65 -0
  31. django_agent_runtime/runtime/events/base.py +135 -0
  32. django_agent_runtime/runtime/events/db.py +129 -0
  33. django_agent_runtime/runtime/events/redis.py +228 -0
  34. django_agent_runtime/runtime/events/sync.py +140 -0
  35. django_agent_runtime/runtime/interfaces.py +475 -0
  36. django_agent_runtime/runtime/llm/__init__.py +91 -0
  37. django_agent_runtime/runtime/llm/anthropic.py +249 -0
  38. django_agent_runtime/runtime/llm/litellm_adapter.py +173 -0
  39. django_agent_runtime/runtime/llm/openai.py +230 -0
  40. django_agent_runtime/runtime/queue/__init__.py +75 -0
  41. django_agent_runtime/runtime/queue/base.py +158 -0
  42. django_agent_runtime/runtime/queue/postgres.py +248 -0
  43. django_agent_runtime/runtime/queue/redis_streams.py +336 -0
  44. django_agent_runtime/runtime/queue/sync.py +277 -0
  45. django_agent_runtime/runtime/registry.py +186 -0
  46. django_agent_runtime/runtime/runner.py +540 -0
  47. django_agent_runtime/runtime/tracing/__init__.py +48 -0
  48. django_agent_runtime/runtime/tracing/langfuse.py +117 -0
  49. django_agent_runtime/runtime/tracing/noop.py +36 -0
  50. django_agent_runtime/urls.py +39 -0
  51. django_agent_runtime-0.3.6.dist-info/METADATA +723 -0
  52. django_agent_runtime-0.3.6.dist-info/RECORD +55 -0
  53. django_agent_runtime-0.3.6.dist-info/WHEEL +5 -0
  54. django_agent_runtime-0.3.6.dist-info/licenses/LICENSE +22 -0
  55. django_agent_runtime-0.3.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,723 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-agent-runtime
3
+ Version: 0.3.6
4
+ Summary: Production-grade AI agent runtime for Django - framework and model agnostic
5
+ Author: Chris Barry
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/makemore/django-agent-runtime
8
+ Project-URL: Documentation, https://github.com/makemore/django-agent-runtime#readme
9
+ Project-URL: Repository, https://github.com/makemore/django-agent-runtime
10
+ Project-URL: Issues, https://github.com/makemore/django-agent-runtime/issues
11
+ Keywords: django,ai,agent,llm,runtime,async
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Framework :: Django
15
+ Classifier: Framework :: Django :: 4.2
16
+ Classifier: Framework :: Django :: 5.0
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: Django>=4.2
28
+ Requires-Dist: djangorestframework>=3.14
29
+ Requires-Dist: agent-runtime-core>=0.4.0
30
+ Provides-Extra: redis
31
+ Requires-Dist: redis>=4.5.0; extra == "redis"
32
+ Provides-Extra: openai
33
+ Requires-Dist: openai>=1.0.0; extra == "openai"
34
+ Provides-Extra: anthropic
35
+ Requires-Dist: anthropic>=0.18.0; extra == "anthropic"
36
+ Provides-Extra: litellm
37
+ Requires-Dist: litellm>=1.0.0; extra == "litellm"
38
+ Provides-Extra: langfuse
39
+ Requires-Dist: langfuse>=2.0.0; extra == "langfuse"
40
+ Provides-Extra: all
41
+ Requires-Dist: django-agent-runtime[anthropic,langfuse,litellm,openai,redis]; extra == "all"
42
+ Provides-Extra: dev
43
+ Requires-Dist: pytest>=7.0; extra == "dev"
44
+ Requires-Dist: pytest-django>=4.5; extra == "dev"
45
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
46
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
47
+ Requires-Dist: black>=23.0; extra == "dev"
48
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
49
+ Requires-Dist: mypy>=1.0; extra == "dev"
50
+ Dynamic: license-file
51
+
52
+ # django-agent-runtime
53
+
54
+ [![PyPI version](https://badge.fury.io/py/django-agent-runtime.svg)](https://badge.fury.io/py/django-agent-runtime)
55
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
56
+ [![Django 4.2+](https://img.shields.io/badge/django-4.2+-green.svg)](https://www.djangoproject.com/)
57
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
58
+
59
+ A production-ready Django app for AI agent execution. Provides everything you need to run AI agents in production: database models, REST API, real-time streaming, background workers, and more.
60
+
61
+ ## Recent Updates
62
+
63
+ | Version | Date | Changes |
64
+ |---------|------|---------|
65
+ | **0.3.6** | 2025-01-13 | Auto-reload for `runagent` in DEBUG mode (like Django's runserver) |
66
+ | **0.3.5** | 2025-01-13 | Added Recent Updates changelog to README |
67
+ | **0.3.4** | 2025-01-13 | Documentation updates for message history |
68
+ | **0.3.3** | 2025-01-13 | Added `conversation.get_message_history()` for retrieving full message sequences |
69
+ | **0.3.2** | 2025-01-13 | Event visibility system - filter events by `internal`/`debug`/`user` levels |
70
+ | **0.3.1** | 2025-01-12 | Anonymous session support for unauthenticated users |
71
+ | **0.3.0** | 2025-01-11 | ViewSet refactor - base classes for custom auth/permissions |
72
+
73
+ ## Features
74
+
75
+ - 🔌 **Framework Agnostic** - Works with LangGraph, CrewAI, OpenAI Agents, or custom loops
76
+ - 🤖 **Model Agnostic** - OpenAI, Anthropic, or any provider via LiteLLM
77
+ - ⚡ **Production-Grade Concurrency** - Multi-process + async workers with `./manage.py runagent`
78
+ - 📊 **PostgreSQL Queue** - Reliable, lease-based job queue with automatic retries
79
+ - 🔄 **Real-Time Streaming** - Server-Sent Events (SSE) for live UI updates
80
+ - 🛡️ **Resilient** - Retries, cancellation, timeouts, and heartbeats built-in
81
+ - 📈 **Observable** - Optional Langfuse integration for tracing
82
+ - 🧩 **Installable** - Drop-in Django app, ready in minutes
83
+
84
+ ## Installation
85
+
86
+ ```bash
87
+ pip install django-agent-runtime
88
+
89
+ # With LLM providers
90
+ pip install django-agent-runtime[openai]
91
+ pip install django-agent-runtime[anthropic]
92
+
93
+ # With Redis support (recommended for production)
94
+ pip install django-agent-runtime[redis]
95
+
96
+ # Everything
97
+ pip install django-agent-runtime[all]
98
+ ```
99
+
100
+ ## Quick Start
101
+
102
+ ### 1. Add to Django Settings
103
+
104
+ ```python
105
+ # settings.py
106
+ INSTALLED_APPS = [
107
+ ...
108
+ 'rest_framework',
109
+ 'django_agent_runtime',
110
+ ]
111
+
112
+ DJANGO_AGENT_RUNTIME = {
113
+ # Queue & Events
114
+ 'QUEUE_BACKEND': 'postgres', # or 'redis_streams'
115
+ 'EVENT_BUS_BACKEND': 'db', # or 'redis'
116
+
117
+ # LLM Configuration
118
+ 'MODEL_PROVIDER': 'openai', # or 'anthropic', 'litellm'
119
+ 'DEFAULT_MODEL': 'gpt-4o',
120
+
121
+ # Timeouts
122
+ 'LEASE_TTL_SECONDS': 30,
123
+ 'RUN_TIMEOUT_SECONDS': 900,
124
+
125
+ # Agent Discovery
126
+ 'RUNTIME_REGISTRY': [
127
+ 'myapp.agents:register_agents',
128
+ ],
129
+ }
130
+ ```
131
+
132
+ ### 2. Run Migrations
133
+
134
+ ```bash
135
+ python manage.py migrate django_agent_runtime
136
+ ```
137
+
138
+ ### 3. Set Up API ViewSets and URLs
139
+
140
+ Create your own ViewSets by inheriting from the base classes and configure authentication:
141
+
142
+ ```python
143
+ # myapp/api/views.py
144
+ from django_agent_runtime.api.views import BaseAgentRunViewSet, BaseAgentConversationViewSet
145
+ from rest_framework.permissions import IsAuthenticated
146
+
147
+ class AgentRunViewSet(BaseAgentRunViewSet):
148
+ permission_classes = [IsAuthenticated]
149
+
150
+ class AgentConversationViewSet(BaseAgentConversationViewSet):
151
+ permission_classes = [IsAuthenticated]
152
+ ```
153
+
154
+ Then wire up your URLs:
155
+
156
+ ```python
157
+ # myapp/api/urls.py
158
+ from django.urls import path, include
159
+ from rest_framework.routers import DefaultRouter
160
+ from django_agent_runtime.api.views import sync_event_stream, async_event_stream
161
+ from .views import AgentRunViewSet, AgentConversationViewSet
162
+
163
+ router = DefaultRouter()
164
+ router.register(r"conversations", AgentConversationViewSet, basename="conversation")
165
+ router.register(r"runs", AgentRunViewSet, basename="run")
166
+
167
+ urlpatterns = [
168
+ path("", include(router.urls)),
169
+ path("runs/<str:run_id>/events/", sync_event_stream, name="run-events"),
170
+ path("runs/<str:run_id>/events/stream/", async_event_stream, name="run-stream"),
171
+ ]
172
+ ```
173
+
174
+ Include in your main urls.py:
175
+
176
+ ```python
177
+ # urls.py
178
+ from django.urls import path, include
179
+
180
+ urlpatterns = [
181
+ ...
182
+ path('api/agents/', include('myapp.api.urls')),
183
+ ]
184
+ ```
185
+
186
+ ### 4. Create an Agent
187
+
188
+ ```python
189
+ # myapp/agents.py
190
+ from django_agent_runtime.runtime.interfaces import (
191
+ AgentRuntime,
192
+ RunContext,
193
+ RunResult,
194
+ EventType,
195
+ )
196
+ from django_agent_runtime.runtime.registry import register_runtime
197
+ from django_agent_runtime.runtime.llm import get_llm_client
198
+
199
+
200
+ class ChatAgent(AgentRuntime):
201
+ """A simple conversational agent."""
202
+
203
+ @property
204
+ def key(self) -> str:
205
+ return "chat-agent"
206
+
207
+ async def run(self, ctx: RunContext) -> RunResult:
208
+ # Get the LLM client
209
+ llm = get_llm_client()
210
+
211
+ # Generate a response
212
+ response = await llm.generate(ctx.input_messages)
213
+
214
+ # Emit event for real-time streaming
215
+ await ctx.emit(EventType.ASSISTANT_MESSAGE, {
216
+ "content": response.message["content"],
217
+ })
218
+
219
+ return RunResult(
220
+ final_output={"response": response.message["content"]},
221
+ final_messages=[response.message],
222
+ )
223
+
224
+
225
+ def register_agents():
226
+ """Called by django-agent-runtime on startup."""
227
+ register_runtime(ChatAgent())
228
+ ```
229
+
230
+ ### 5. Start Workers
231
+
232
+ ```bash
233
+ # Start agent workers (4 processes, 20 concurrent runs each)
234
+ python manage.py runagent --processes 4 --concurrency 20
235
+ ```
236
+
237
+ ## API Endpoints
238
+
239
+ ### Create a Run
240
+
241
+ ```http
242
+ POST /api/agents/runs/
243
+ Content-Type: application/json
244
+ Authorization: Token <your-token>
245
+
246
+ {
247
+ "agent_key": "chat-agent",
248
+ "messages": [
249
+ {"role": "user", "content": "Hello! How are you?"}
250
+ ]
251
+ }
252
+ ```
253
+
254
+ **Response:**
255
+ ```json
256
+ {
257
+ "id": "550e8400-e29b-41d4-a716-446655440000",
258
+ "agent_key": "chat-agent",
259
+ "status": "queued",
260
+ "created_at": "2024-01-15T10:30:00Z"
261
+ }
262
+ ```
263
+
264
+ ### Stream Events (SSE)
265
+
266
+ ```http
267
+ GET /api/agents/runs/{id}/events/
268
+ Accept: text/event-stream
269
+ ```
270
+
271
+ **Event Stream:**
272
+ ```
273
+ event: run.started
274
+ data: {"run_id": "550e8400...", "ts": "2024-01-15T10:30:01Z"}
275
+
276
+ event: assistant.message
277
+ data: {"content": "Hello! I'm doing well, thank you for asking!"}
278
+
279
+ event: run.succeeded
280
+ data: {"run_id": "550e8400...", "output": {...}}
281
+ ```
282
+
283
+ ### Get Run Status
284
+
285
+ ```http
286
+ GET /api/agents/runs/{id}/
287
+ ```
288
+
289
+ ### Cancel a Run
290
+
291
+ ```http
292
+ POST /api/agents/runs/{id}/cancel/
293
+ ```
294
+
295
+ ### List Conversations
296
+
297
+ ```http
298
+ GET /api/agents/conversations/
299
+ ```
300
+
301
+ ## Architecture
302
+
303
+ ```
304
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
305
+ │ Django API │────▶│ PostgreSQL │────▶│ Workers │
306
+ │ (REST/SSE) │ │ Queue │ │ (runagent) │
307
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
308
+ │ │
309
+ │ ▼
310
+ │ ┌─────────────────┐
311
+ │ │ Your Agent │
312
+ │ │ (AgentRuntime)│
313
+ │ └─────────────────┘
314
+ │ │
315
+ ▼ ▼
316
+ ┌─────────────────┐ ┌─────────────────┐
317
+ │ Frontend │◀────────────────────────────│ Event Bus │
318
+ │ (SSE Client) │ Real-time │ (DB/Redis) │
319
+ └─────────────────┘ └─────────────────┘
320
+ ```
321
+
322
+ ## Models
323
+
324
+ ### Conversation
325
+
326
+ Groups related agent runs together:
327
+
328
+ ```python
329
+ from django_agent_runtime.models import AgentConversation
330
+
331
+ conversation = AgentConversation.objects.create(
332
+ user=request.user,
333
+ agent_key="chat-agent",
334
+ title="My Chat",
335
+ metadata={"source": "web"},
336
+ )
337
+ ```
338
+
339
+ #### Message History
340
+
341
+ Get the full message history across all runs in a conversation:
342
+
343
+ ```python
344
+ # Get all messages (user, assistant, tool calls, tool results)
345
+ messages = conversation.get_message_history()
346
+
347
+ # Include messages from failed runs
348
+ messages = conversation.get_message_history(include_failed_runs=True)
349
+
350
+ # Get just the last assistant message
351
+ last_msg = conversation.get_last_assistant_message()
352
+ ```
353
+
354
+ Returns messages in the framework-neutral format:
355
+ ```python
356
+ [
357
+ {"role": "user", "content": "What's the weather?"},
358
+ {"role": "assistant", "content": None, "tool_calls": [...]},
359
+ {"role": "tool", "content": "72°F sunny", "tool_call_id": "call_123"},
360
+ {"role": "assistant", "content": "The weather is 72°F and sunny."},
361
+ ]
362
+ ```
363
+
364
+ ### AgentRun
365
+
366
+ Represents a single agent execution:
367
+
368
+ ```python
369
+ from django_agent_runtime.models import AgentRun
370
+
371
+ run = AgentRun.objects.create(
372
+ conversation=conversation,
373
+ agent_key="chat-agent",
374
+ input={"messages": [...]},
375
+ )
376
+
377
+ # After completion, output contains final_messages
378
+ messages = run.output.get("final_messages", [])
379
+ ```
380
+
381
+ ### AgentEvent
382
+
383
+ Stores events emitted during runs:
384
+
385
+ ```python
386
+ from django_agent_runtime.models import AgentEvent
387
+
388
+ events = AgentEvent.objects.filter(run=run).order_by('seq')
389
+ for event in events:
390
+ print(f"{event.event_type}: {event.payload}")
391
+ ```
392
+
393
+ ## Building Agents with Tools
394
+
395
+ ```python
396
+ from django_agent_runtime.runtime.interfaces import (
397
+ AgentRuntime, RunContext, RunResult, EventType,
398
+ Tool, ToolRegistry,
399
+ )
400
+ from django_agent_runtime.runtime.llm import get_llm_client
401
+
402
+
403
+ def get_weather(location: str) -> str:
404
+ """Get current weather for a location."""
405
+ # Your weather API call here
406
+ return f"Sunny, 72°F in {location}"
407
+
408
+
409
+ def search_database(query: str) -> list:
410
+ """Search the database for relevant information."""
411
+ # Your database search here
412
+ return [{"title": "Result 1", "content": "..."}]
413
+
414
+
415
+ class ToolAgent(AgentRuntime):
416
+ @property
417
+ def key(self) -> str:
418
+ return "tool-agent"
419
+
420
+ def __init__(self):
421
+ self.tools = ToolRegistry()
422
+ self.tools.register(Tool.from_function(get_weather))
423
+ self.tools.register(Tool.from_function(search_database))
424
+
425
+ async def run(self, ctx: RunContext) -> RunResult:
426
+ llm = get_llm_client()
427
+ messages = list(ctx.input_messages)
428
+
429
+ while True:
430
+ response = await llm.generate(
431
+ messages,
432
+ tools=self.tools.to_openai_format(),
433
+ )
434
+ messages.append(response.message)
435
+
436
+ if not response.tool_calls:
437
+ break
438
+
439
+ for tool_call in response.tool_calls:
440
+ # Emit tool call event
441
+ await ctx.emit(EventType.TOOL_CALL, {
442
+ "tool": tool_call["function"]["name"],
443
+ "arguments": tool_call["function"]["arguments"],
444
+ })
445
+
446
+ # Execute tool
447
+ result = await self.tools.execute(
448
+ tool_call["function"]["name"],
449
+ tool_call["function"]["arguments"],
450
+ )
451
+
452
+ # Emit result event
453
+ await ctx.emit(EventType.TOOL_RESULT, {
454
+ "tool_call_id": tool_call["id"],
455
+ "result": result,
456
+ })
457
+
458
+ messages.append({
459
+ "role": "tool",
460
+ "tool_call_id": tool_call["id"],
461
+ "content": str(result),
462
+ })
463
+
464
+ return RunResult(
465
+ final_output={"response": response.message["content"]},
466
+ final_messages=messages,
467
+ )
468
+ ```
469
+
470
+ ## Anonymous Sessions
471
+
472
+ django-agent-runtime supports anonymous sessions for unauthenticated users who have a session token. This is useful for public-facing chat interfaces.
473
+
474
+ ### Setup
475
+
476
+ 1. **Configure the anonymous session model** in your settings:
477
+
478
+ ```python
479
+ DJANGO_AGENT_RUNTIME = {
480
+ # ... other settings ...
481
+ 'ANONYMOUS_SESSION_MODEL': 'accounts.AnonymousSession',
482
+ }
483
+ ```
484
+
485
+ 2. **Create your anonymous session model** with required fields:
486
+
487
+ ```python
488
+ # accounts/models.py
489
+ import uuid
490
+ from django.db import models
491
+ from django.utils import timezone
492
+
493
+ class AnonymousSession(models.Model):
494
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
495
+ token = models.CharField(max_length=64, unique=True, db_index=True)
496
+ created_at = models.DateTimeField(auto_now_add=True)
497
+ expires_at = models.DateTimeField()
498
+
499
+ @property
500
+ def is_expired(self):
501
+ return timezone.now() > self.expires_at
502
+ ```
503
+
504
+ 3. **Set up authentication** in your ViewSets:
505
+
506
+ ```python
507
+ from rest_framework.authentication import TokenAuthentication
508
+ from django_agent_runtime.api.views import BaseAgentRunViewSet, BaseAgentConversationViewSet
509
+ from django_agent_runtime.api.permissions import (
510
+ AnonymousSessionAuthentication,
511
+ IsAuthenticatedOrAnonymousSession,
512
+ )
513
+
514
+ class AgentConversationViewSet(BaseAgentConversationViewSet):
515
+ authentication_classes = [TokenAuthentication, AnonymousSessionAuthentication]
516
+ permission_classes = [IsAuthenticatedOrAnonymousSession]
517
+
518
+ class AgentRunViewSet(BaseAgentRunViewSet):
519
+ authentication_classes = [TokenAuthentication, AnonymousSessionAuthentication]
520
+ permission_classes = [IsAuthenticatedOrAnonymousSession]
521
+ ```
522
+
523
+ ### Client Usage
524
+
525
+ Pass the session token via the `X-Anonymous-Token` header:
526
+
527
+ ```bash
528
+ curl -X POST https://api.example.com/agent/runs/ \
529
+ -H "X-Anonymous-Token: your-session-token" \
530
+ -H "Content-Type: application/json" \
531
+ -d '{"agent_key": "chat-agent", "messages": [{"role": "user", "content": "Hello!"}]}'
532
+ ```
533
+
534
+ For SSE streaming (where headers can't be set), use a query parameter:
535
+
536
+ ```javascript
537
+ const eventSource = new EventSource(
538
+ `/api/agents/runs/${runId}/events/?anonymous_token=your-session-token`
539
+ );
540
+ ```
541
+
542
+ ## Event Visibility
543
+
544
+ Events have visibility levels that control what's shown to users in the UI:
545
+
546
+ | Level | Description |
547
+ |-------|-------------|
548
+ | `internal` | Never shown to UI (heartbeats, checkpoints) |
549
+ | `debug` | Shown only in debug mode (tool calls, tool results) |
550
+ | `user` | Always shown to users (messages, errors) |
551
+
552
+ ### Configuration
553
+
554
+ ```python
555
+ DJANGO_AGENT_RUNTIME = {
556
+ 'EVENT_VISIBILITY': {
557
+ 'run.started': 'internal',
558
+ 'run.failed': 'user',
559
+ 'assistant.message': 'user',
560
+ 'tool.call': 'debug',
561
+ 'tool.result': 'debug',
562
+ 'state.checkpoint': 'internal',
563
+ 'error': 'user',
564
+ },
565
+ 'DEBUG_MODE': False, # When True, 'debug' events become visible
566
+ }
567
+ ```
568
+
569
+ ### SSE Filtering
570
+
571
+ The SSE endpoint filters events by visibility:
572
+
573
+ ```javascript
574
+ // Only user-visible events (default)
575
+ new EventSource(`/api/agents/runs/${runId}/events/`);
576
+
577
+ // Include debug events
578
+ new EventSource(`/api/agents/runs/${runId}/events/?include_debug=true`);
579
+
580
+ // Include all events (for debugging)
581
+ new EventSource(`/api/agents/runs/${runId}/events/?include_all=true`);
582
+ ```
583
+
584
+ ### Helper Methods
585
+
586
+ Agent runtimes can use convenience methods:
587
+
588
+ ```python
589
+ async def run(self, ctx: RunContext) -> RunResult:
590
+ # Emit a message always shown to users
591
+ await ctx.emit_user_message("Processing your request...")
592
+
593
+ # Emit an error shown to users
594
+ await ctx.emit_error("Something went wrong", {"code": "ERR_001"})
595
+ ```
596
+
597
+ ## Configuration Reference
598
+
599
+ | Setting | Type | Default | Description |
600
+ |---------|------|---------|-------------|
601
+ | `QUEUE_BACKEND` | str | `"postgres"` | Queue backend: `postgres`, `redis_streams` |
602
+ | `EVENT_BUS_BACKEND` | str | `"db"` | Event bus: `db`, `redis` |
603
+ | `REDIS_URL` | str | `None` | Redis connection URL |
604
+ | `MODEL_PROVIDER` | str | `"openai"` | LLM provider: `openai`, `anthropic`, `litellm` |
605
+ | `DEFAULT_MODEL` | str | `"gpt-4o"` | Default model name |
606
+ | `LEASE_TTL_SECONDS` | int | `30` | Worker lease duration |
607
+ | `RUN_TIMEOUT_SECONDS` | int | `900` | Maximum run duration |
608
+ | `MAX_RETRIES` | int | `3` | Retry attempts on failure |
609
+ | `RUNTIME_REGISTRY` | list | `[]` | Agent registration functions |
610
+ | `ANONYMOUS_SESSION_MODEL` | str | `None` | Path to anonymous session model |
611
+ | `EVENT_VISIBILITY` | dict | See above | Event visibility configuration |
612
+ | `DEBUG_MODE` | bool | `False` | Show debug-level events in UI |
613
+ | `LANGFUSE_ENABLED` | bool | `False` | Enable Langfuse tracing |
614
+
615
+ ## Event Types
616
+
617
+ | Event | Visibility | Description |
618
+ |-------|------------|-------------|
619
+ | `run.started` | internal | Run execution began |
620
+ | `run.succeeded` | internal | Run completed successfully |
621
+ | `run.failed` | user | Run failed with error |
622
+ | `run.cancelled` | user | Run was cancelled |
623
+ | `run.timed_out` | user | Run exceeded timeout |
624
+ | `run.heartbeat` | internal | Worker heartbeat |
625
+ | `tool.call` | debug | Tool was invoked |
626
+ | `tool.result` | debug | Tool returned result |
627
+ | `assistant.message` | user | LLM generated message |
628
+ | `assistant.delta` | user | Token streaming delta |
629
+ | `state.checkpoint` | internal | State checkpoint saved |
630
+ | `error` | user | Runtime error (distinct from run.failed) |
631
+
632
+ ## Management Commands
633
+
634
+ ### runagent
635
+
636
+ Start agent workers:
637
+
638
+ ```bash
639
+ # Basic usage
640
+ python manage.py runagent
641
+
642
+ # With options
643
+ python manage.py runagent \
644
+ --processes 4 \
645
+ --concurrency 20 \
646
+ --agent-keys chat-agent,tool-agent \
647
+ --queue-poll-interval 1.0
648
+ ```
649
+
650
+ #### Auto-Reload (Development)
651
+
652
+ In `DEBUG=True` mode, `runagent` automatically reloads when Python files change—just like Django's `runserver`:
653
+
654
+ ```bash
655
+ # Auto-reload enabled by default in DEBUG mode
656
+ python manage.py runagent
657
+
658
+ # Disable auto-reload
659
+ python manage.py runagent --noreload
660
+ ```
661
+
662
+ **Note:** Auto-reload only works in single-process mode. Multi-process mode (`--processes > 1`) automatically disables auto-reload.
663
+
664
+ ## Frontend Integration
665
+
666
+ ### JavaScript SSE Client
667
+
668
+ ```javascript
669
+ const eventSource = new EventSource('/api/agents/runs/550e8400.../events/');
670
+
671
+ eventSource.addEventListener('assistant.message', (event) => {
672
+ const data = JSON.parse(event.data);
673
+ appendMessage(data.content);
674
+ });
675
+
676
+ eventSource.addEventListener('run.succeeded', (event) => {
677
+ eventSource.close();
678
+ showComplete();
679
+ });
680
+
681
+ eventSource.addEventListener('run.failed', (event) => {
682
+ const data = JSON.parse(event.data);
683
+ showError(data.error);
684
+ eventSource.close();
685
+ });
686
+ ```
687
+
688
+ ### React Hook Example
689
+
690
+ ```typescript
691
+ function useAgentRun(runId: string) {
692
+ const [events, setEvents] = useState<AgentEvent[]>([]);
693
+ const [status, setStatus] = useState<'running' | 'complete' | 'error'>('running');
694
+
695
+ useEffect(() => {
696
+ const es = new EventSource(`/api/agents/runs/${runId}/events/`);
697
+
698
+ es.onmessage = (event) => {
699
+ const data = JSON.parse(event.data);
700
+ setEvents(prev => [...prev, data]);
701
+
702
+ if (data.type === 'run.succeeded') setStatus('complete');
703
+ if (data.type === 'run.failed') setStatus('error');
704
+ };
705
+
706
+ return () => es.close();
707
+ }, [runId]);
708
+
709
+ return { events, status };
710
+ }
711
+ ```
712
+
713
+ ## Related Packages
714
+
715
+ - [agent-runtime-core](https://pypi.org/project/agent-runtime-core/) - The framework-agnostic core library (used internally)
716
+
717
+ ## Contributing
718
+
719
+ Contributions are welcome! Please feel free to submit a Pull Request.
720
+
721
+ ## License
722
+
723
+ MIT License - see [LICENSE](LICENSE) for details.