agentexec 0.2.0rc2__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 (149) hide show
  1. {agentexec-0.2.0rc2 → agentexec-0.3.0}/CHANGELOG.md +61 -67
  2. {agentexec-0.2.0rc2 → agentexec-0.3.0}/PKG-INFO +1 -1
  3. agentexec-0.3.0/docs/api-reference/activity.md +401 -0
  4. agentexec-0.3.0/docs/api-reference/core.md +532 -0
  5. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/api-reference/pipeline.md +8 -8
  6. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/api-reference/runner.md +5 -5
  7. agentexec-0.3.0/docs/concepts/activity-tracking.md +431 -0
  8. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/concepts/architecture.md +63 -44
  9. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/concepts/task-lifecycle.md +33 -27
  10. agentexec-0.3.0/docs/concepts/worker-pool.md +363 -0
  11. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/contributing.md +19 -9
  12. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/deployment/docker.md +23 -18
  13. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/deployment/production.md +49 -62
  14. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/getting-started/configuration.md +30 -22
  15. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/getting-started/installation.md +2 -2
  16. agentexec-0.3.0/docs/getting-started/quickstart.md +253 -0
  17. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/guides/basic-usage.md +112 -114
  18. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/guides/custom-runners.md +34 -34
  19. agentexec-0.3.0/docs/guides/fastapi-integration.md +296 -0
  20. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/guides/openai-runner.md +5 -5
  21. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/guides/pipelines.md +6 -6
  22. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docs/index.md +23 -10
  23. {agentexec-0.2.0rc2 → agentexec-0.3.0}/pyproject.toml +9 -1
  24. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/__init__.py +6 -3
  25. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/cli.py +8 -2
  26. agentexec-0.3.0/src/agentexec/core/results.py +80 -0
  27. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/schedule.py +0 -1
  28. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/worker/pool.py +33 -8
  29. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_results.py +55 -1
  30. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_schedule.py +5 -0
  31. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_state.py +1 -0
  32. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_state_backend.py +7 -4
  33. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_task_locking.py +0 -25
  34. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_worker_logging.py +1 -0
  35. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_worker_pool.py +18 -1
  36. agentexec-0.3.0/tests/test_worker_resilience.py +198 -0
  37. {agentexec-0.2.0rc2 → agentexec-0.3.0}/ui/package.json +1 -1
  38. agentexec-0.2.0rc2/.claude/skills/prepare-release/SKILL.md +0 -57
  39. agentexec-0.2.0rc2/.python-version +0 -1
  40. agentexec-0.2.0rc2/docs/api-reference/activity.md +0 -455
  41. agentexec-0.2.0rc2/docs/api-reference/core.md +0 -476
  42. agentexec-0.2.0rc2/docs/concepts/activity-tracking.md +0 -520
  43. agentexec-0.2.0rc2/docs/concepts/worker-pool.md +0 -499
  44. agentexec-0.2.0rc2/docs/getting-started/quickstart.md +0 -262
  45. agentexec-0.2.0rc2/docs/guides/fastapi-integration.md +0 -301
  46. agentexec-0.2.0rc2/examples/basic/debug_spawn.py +0 -30
  47. agentexec-0.2.0rc2/examples/basic/enqueue.py +0 -26
  48. agentexec-0.2.0rc2/examples/basic/entrypoint.sh +0 -2
  49. agentexec-0.2.0rc2/examples/basic/worker.py +0 -50
  50. agentexec-0.2.0rc2/examples/multi-tenancy/README.md +0 -92
  51. agentexec-0.2.0rc2/examples/multi-tenancy/example.py +0 -188
  52. agentexec-0.2.0rc2/examples/openai-agents-fastapi/README.md +0 -147
  53. agentexec-0.2.0rc2/examples/openai-agents-fastapi/alembic/README +0 -1
  54. agentexec-0.2.0rc2/examples/openai-agents-fastapi/alembic/env.py +0 -82
  55. agentexec-0.2.0rc2/examples/openai-agents-fastapi/alembic/script.py.mako +0 -28
  56. agentexec-0.2.0rc2/examples/openai-agents-fastapi/alembic.ini +0 -148
  57. agentexec-0.2.0rc2/examples/openai-agents-fastapi/compose.yml +0 -10
  58. agentexec-0.2.0rc2/examples/openai-agents-fastapi/context.py +0 -21
  59. agentexec-0.2.0rc2/examples/openai-agents-fastapi/db.py +0 -27
  60. agentexec-0.2.0rc2/examples/openai-agents-fastapi/main.py +0 -45
  61. agentexec-0.2.0rc2/examples/openai-agents-fastapi/models.py +0 -9
  62. agentexec-0.2.0rc2/examples/openai-agents-fastapi/pipeline.py +0 -246
  63. agentexec-0.2.0rc2/examples/openai-agents-fastapi/pyproject.toml +0 -35
  64. agentexec-0.2.0rc2/examples/openai-agents-fastapi/tools.py +0 -46
  65. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/bun.lock +0 -422
  66. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/index.html +0 -13
  67. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/package.json +0 -27
  68. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/public/vite.svg +0 -4
  69. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/src/App.tsx +0 -35
  70. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/src/api/agents.ts +0 -37
  71. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/src/api/queries.ts +0 -51
  72. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/src/components/Layout.tsx +0 -38
  73. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/src/index.css +0 -263
  74. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/src/main.tsx +0 -10
  75. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/src/pages/AgentDetailPage.tsx +0 -45
  76. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/src/pages/AgentListPage.tsx +0 -68
  77. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/src/styles/github-dark.css +0 -617
  78. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/tsconfig.json +0 -21
  79. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/tsconfig.node.json +0 -11
  80. agentexec-0.2.0rc2/examples/openai-agents-fastapi/ui/vite.config.ts +0 -19
  81. agentexec-0.2.0rc2/examples/openai-agents-fastapi/views.py +0 -118
  82. agentexec-0.2.0rc2/examples/openai-agents-fastapi/worker.py +0 -86
  83. agentexec-0.2.0rc2/examples/queue-fairness/README.md +0 -75
  84. agentexec-0.2.0rc2/examples/queue-fairness/run.py +0 -215
  85. agentexec-0.2.0rc2/research/multiprocessing.md +0 -778
  86. agentexec-0.2.0rc2/src/agentexec/core/results.py +0 -41
  87. agentexec-0.2.0rc2/ui/.gitignore +0 -3
  88. {agentexec-0.2.0rc2 → agentexec-0.3.0}/.github/workflows/ci.yml +0 -0
  89. {agentexec-0.2.0rc2 → agentexec-0.3.0}/.github/workflows/docker-publish.yml +0 -0
  90. {agentexec-0.2.0rc2 → agentexec-0.3.0}/.github/workflows/npm-publish.yml +0 -0
  91. {agentexec-0.2.0rc2 → agentexec-0.3.0}/.github/workflows/publish.yml +0 -0
  92. {agentexec-0.2.0rc2 → agentexec-0.3.0}/.gitignore +0 -0
  93. {agentexec-0.2.0rc2 → agentexec-0.3.0}/README.md +0 -0
  94. {agentexec-0.2.0rc2 → agentexec-0.3.0}/RELEASE.md +0 -0
  95. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docker/Dockerfile +0 -0
  96. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docker/README.md +0 -0
  97. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docker/entrypoint.py +0 -0
  98. {agentexec-0.2.0rc2 → agentexec-0.3.0}/docker-compose.kafka.yml +0 -0
  99. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/activity/__init__.py +0 -0
  100. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/activity/events.py +0 -0
  101. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/activity/handlers.py +0 -0
  102. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/activity/models.py +0 -0
  103. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/activity/producer.py +0 -0
  104. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/activity/schemas.py +0 -0
  105. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/activity/status.py +0 -0
  106. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/config.py +0 -0
  107. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/core/__init__.py +0 -0
  108. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/core/db.py +0 -0
  109. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/core/queue.py +0 -0
  110. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/core/task.py +0 -0
  111. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/pipeline.py +0 -0
  112. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/runners/__init__.py +0 -0
  113. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/runners/base.py +0 -0
  114. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/runners/openai.py +0 -0
  115. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/state/__init__.py +0 -0
  116. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/state/base.py +0 -0
  117. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/state/kafka.py +0 -0
  118. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/state/redis.py +0 -0
  119. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/tracker.py +0 -0
  120. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/worker/__init__.py +0 -0
  121. {agentexec-0.2.0rc2 → agentexec-0.3.0}/src/agentexec/worker/event.py +0 -0
  122. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_activity_schemas.py +0 -0
  123. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_activity_tracking.py +0 -0
  124. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_config.py +0 -0
  125. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_db.py +0 -0
  126. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_kafka_integration.py +0 -0
  127. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_pipeline.py +0 -0
  128. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_pipeline_flow.py +0 -0
  129. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_public_api.py +0 -0
  130. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_queue.py +0 -0
  131. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_queue_partitions.py +0 -0
  132. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_runners.py +0 -0
  133. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_self_describing_results.py +0 -0
  134. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_task.py +0 -0
  135. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_task_types.py +0 -0
  136. {agentexec-0.2.0rc2 → agentexec-0.3.0}/tests/test_worker_event.py +0 -0
  137. {agentexec-0.2.0rc2/examples/openai-agents-fastapi → agentexec-0.3.0}/ui/.gitignore +0 -0
  138. {agentexec-0.2.0rc2 → agentexec-0.3.0}/ui/README.md +0 -0
  139. {agentexec-0.2.0rc2 → agentexec-0.3.0}/ui/bun.lock +0 -0
  140. {agentexec-0.2.0rc2 → agentexec-0.3.0}/ui/src/components/ActiveAgentsBadge.tsx +0 -0
  141. {agentexec-0.2.0rc2 → agentexec-0.3.0}/ui/src/components/ProgressBar.tsx +0 -0
  142. {agentexec-0.2.0rc2 → agentexec-0.3.0}/ui/src/components/StatusBadge.tsx +0 -0
  143. {agentexec-0.2.0rc2 → agentexec-0.3.0}/ui/src/components/TaskDetail.tsx +0 -0
  144. {agentexec-0.2.0rc2 → agentexec-0.3.0}/ui/src/components/TaskList.tsx +0 -0
  145. {agentexec-0.2.0rc2 → agentexec-0.3.0}/ui/src/components/index.ts +0 -0
  146. {agentexec-0.2.0rc2 → agentexec-0.3.0}/ui/src/index.ts +0 -0
  147. {agentexec-0.2.0rc2 → agentexec-0.3.0}/ui/src/types.ts +0 -0
  148. {agentexec-0.2.0rc2 → agentexec-0.3.0}/ui/tsconfig.json +0 -0
  149. {agentexec-0.2.0rc2 → agentexec-0.3.0}/ui/vite.config.ts +0 -0
@@ -1,93 +1,60 @@
1
1
  # Changelog
2
2
 
3
- ## v0.2.0rc2
4
-
5
- Second release candidate for 0.2.0. Completes the async migration and
6
- stabilizes the worker pool architecture.
3
+ ## v0.3.0
7
4
 
8
5
  ### Breaking Changes
9
6
 
10
- **Fully async database layer**
11
- - `configure_engine()` and `get_session()` now require an async SQLAlchemy engine (`AsyncEngine`) and return `AsyncSession`
12
- - Database URLs must use async drivers (e.g. `sqlite+aiosqlite://`, `postgresql+asyncpg://`)
13
- - `sqlalchemy[asyncio]` is now a core dependency; `aiosqlite` added as dev dependency
14
-
15
- **Async activity query API**
16
- - `activity.list()`, `activity.detail()`, and `activity.count_active()` are now async and accept `AsyncSession`
17
- - Activity handlers are now async (`async def __call__`)
18
-
19
- **Removed `session` parameter from activity mutations**
20
- - `activity.create()`, `activity.update()`, `activity.complete()`, and `activity.error()` no longer accept a `session` parameter — the handler owns its own session lifecycle
21
-
22
- **Queue priority parameter**
23
- - `BaseQueueBackend.push()` signature changed from `high_priority: bool` to `priority: Priority | None`
24
- - Affects Redis, Kafka, and any custom queue backend implementations
7
+ **Permanent task failure raises `TaskFailedError`**
8
+ - Callers that previously relied on `TimeoutError` to detect a task that exhausted its retries now receive `ax.TaskFailedError` as soon as the failure is recorded
25
9
 
26
10
  ### New Features
27
11
 
28
- **CLI entrypoint**
29
- - New `agentexec` CLI command via `[project.scripts]`
30
-
31
- **Activity model `create()` classmethod**
32
- - `Activity.create()` encapsulates record + initial log entry creation in one async call
33
-
34
- **Async engine disposal**
35
- - `dispose_engine()` ensures the async engine's background threads exit cleanly on shutdown
36
-
37
- ### Architecture Changes
38
-
39
- **Worker pool refactor**
40
- - Workers now use the `spawn` multiprocessing start method with explicit context — no inherited state
41
- - `StateEvent` replaced with stdlib `multiprocessing.Event` — removes dependency on the state backend for shutdown coordination
42
- - Event handling and scheduling extracted into `_EventHandler` and `_Scheduler` classes
43
- - Worker processes are now daemonic with `SIGINT` ignored — clean shutdown driven by the event
44
- - `pool.start()` handles `CancelledError` directly for Ctrl+C shutdown
45
-
46
- **Logging overhaul**
47
- - Removed `worker/logging.py` and `core/logging.py` — all modules use stdlib `logging.getLogger(__name__)`
48
- - Spawned workers bootstrap a `StreamHandler` on the root logger so logs reach stderr
49
- - Pool messages use `logger.info`/`logger.error` instead of `print()`
50
-
51
- ### Bug Fixes
52
-
53
- - Fixed crash when worker receives a task for an unregistered task name (now logs error and skips)
54
- - Failed tasks now log full tracebacks via `logger.exception` instead of `logger.error`
55
- - Kafka consumer handles `None` message values without crashing
56
- - `ActivityUpdated.status` is now `Status` enum instead of raw string
57
- - `raise e` instead of bare `raise` in task execution for clearer tracebacks
12
+ **Terminal failure contract for `ax.gather` / `ax.get_result`**
13
+ - When a task exhausts its retries, the pool stores a `TaskFailure` record at the task's result key (same convention and TTL as success results)
14
+ - `ax.get_result` raises `ax.TaskFailedError` — carrying `task_name`, `agent_id`, `error`, and `attempts` — on its next poll (~0.5s) instead of polling until the timeout
15
+ - `ax.gather` fails fast on the first permanent failure; remaining tasks keep running and their results stay retrievable, so re-gathering survivors resolves instantly
16
+ - `TaskFailedError` is exported on the `ax` namespace
58
17
 
59
- ### Improvements
60
-
61
- - Dependency groups use PEP 735 `[dependency-groups]` instead of `[tool.uv]`
62
- - Ruff line-length increased to 110
63
- - Removed verbose docstring examples and redundant comments throughout activity models
64
- - `selectinload` for eager loading of activity logs in `get_by_agent_id`
65
- - Redis backend adds `type: ignore` annotations for redis-py async stubs
18
+ ## v0.2.0
66
19
 
67
- ## v0.2.0rc1
68
-
69
- First release candidate for 0.2.0. Major refactor of the backend, queue,
70
- activity, and worker systems.
20
+ Major refactor of the backend, queue, activity, worker, and database layers.
21
+ If you're upgrading from 0.1.x, read the **Breaking Changes** section closely.
71
22
 
72
23
  ### Breaking Changes
73
24
 
74
- **Backend module restructure**
75
- - `agentexec.state.redis_backend` renamed to `agentexec.state.redis` update `AGENTEXEC_STATE_BACKEND` if set explicitly
76
- - `AGENTEXEC_QUEUE_NAME` renamed to `AGENTEXEC_QUEUE_PREFIX` (old name still accepted as alias)
25
+ **Fully async database layer**
26
+ - `configure_engine()` and `get_session()` now require an async SQLAlchemy engine (`AsyncEngine`) and return `AsyncSession`
27
+ - Database URLs must use async drivers (e.g. `sqlite+aiosqlite://`, `postgresql+asyncpg://`)
28
+ - `sqlalchemy[asyncio]` is now a core dependency
77
29
 
78
30
  **Async activity API**
79
- - Activity functions are now async: `await ax.activity.create(...)`, `await ax.activity.update(...)`, etc.
31
+ - All activity functions are async: `await ax.activity.create(...)`, `await ax.activity.update(...)`, `await ax.activity.complete(...)`, `await ax.activity.error(...)`
32
+ - `activity.list()`, `activity.detail()`, and `activity.count_active()` are async and accept `AsyncSession`
33
+ - Activity handlers are async (`async def __call__`)
34
+ - The `session` parameter was removed from activity mutations — the handler owns its own session lifecycle
35
+
36
+ **Pool entry point**
37
+ - `pool.run()` was removed. Use `await pool.start()` in an asyncio loop, or the new `agentexec run mymodule:pool` CLI
38
+ - `AGENTEXEC_QUEUE_NAME` renamed to `AGENTEXEC_QUEUE_PREFIX` (old name still accepted as alias)
39
+ - `agentexec.state.redis_backend` renamed to `agentexec.state.redis` — update `AGENTEXEC_STATE_BACKEND` if set explicitly
80
40
 
81
41
  **Task context serialization**
82
42
  - `Task.context` is now `Mapping[str, Any]` (raw dict), not a typed BaseModel — hydration happens at execution time
83
43
  - `Task.create()` is now async
84
44
 
45
+ **Queue backend protocol**
46
+ - `BaseQueueBackend.push()` signature changed from `high_priority: bool` to `priority: Priority | None` — affects Redis, Kafka, and any custom queue backend
47
+
85
48
  **Removed APIs**
86
49
  - `set_global_session`/`get_global_session`/`remove_global_session` — use `configure_engine`/`get_session`
87
50
  - `state.backend.publish`/`subscribe` (pubsub), `index_add`/`index_range`/`index_remove`, `clear`, `configure`
51
+ - `worker/logging.py` and `core/logging.py` — all modules use stdlib `logging.getLogger(__name__)` directly
88
52
 
89
53
  ### New Features
90
54
 
55
+ **CLI entrypoint**
56
+ - New `agentexec` CLI command: `agentexec run mymodule:pool --create-tables --workers 4`
57
+
91
58
  **Partitioned Redis queues**
92
59
  - Tasks with `lock_key` route to dedicated partition queues with per-partition locking and SCAN-based fair dequeue
93
60
 
@@ -101,18 +68,45 @@ activity, and worker systems.
101
68
  - `pip install agentexec[kafka]` for queue and schedule via Kafka
102
69
 
103
70
  **Typed worker IPC**
104
- - `TaskFailed`, `LogEntry`, `ActivityUpdated` messages over `multiprocessing.Queue`
71
+ - `TaskFailed` and `ActivityEvent` messages flow over `multiprocessing.Queue` with pydantic validation
105
72
 
106
73
  **Schedule composite keys**
107
74
  - `{task_name}:{cron}:{context_hash}` for unique schedule identity
108
75
 
109
- ### Improvements
76
+ **Activity model `create()` classmethod**
77
+ - `Activity.create()` encapsulates record + initial log entry creation in one async call
78
+
79
+ **Async engine disposal**
80
+ - `dispose_engine()` ensures the async engine's background threads exit cleanly on shutdown
110
81
 
82
+ ### Architecture Changes
83
+
84
+ **Worker pool refactor**
85
+ - Workers use the `spawn` multiprocessing start method with explicit context — no inherited state
86
+ - Event handling and scheduling extracted into `_EventHandler` and `_Scheduler` classes
87
+ - `StateEvent` replaced with stdlib `multiprocessing.Event` — removes dependency on the state backend for shutdown coordination
111
88
  - Class-based backend architecture with ABCs (`BaseStateBackend`, `BaseQueueBackend`, `BaseScheduleBackend`)
112
89
  - `Task` is pure data, `TaskDefinition` owns behavior
113
- - Session management via `configure_engine`/`get_session` (Pool owns the engine)
114
90
  - Status enum extracted to `activity/status.py` (no SQLAlchemy dependency)
115
91
 
92
+ **Logging**
93
+ - All modules use stdlib `logging.getLogger(__name__)`
94
+ - Spawned workers bootstrap a `StreamHandler` on the root logger so logs reach stderr
95
+ - Pool messages use `logger.info`/`logger.error` instead of `print()`
96
+
97
+ ### Bug Fixes
98
+
99
+ - **Orphaned worker processes on shutdown.** SIGTERM (systemd/docker stop), SIGKILL, and SIGHUP were leaving worker processes running. Fixed via an asyncio SIGTERM handler in the CLI and `prctl(PR_SET_PDEATHSIG)` in each worker so the kernel terminates workers when the pool dies
100
+ - **Worker and scheduler error loops throttled.** Infra failures (e.g. Redis unreachable) were producing 100k+ log lines per second. Added a 1s sleep after outer-loop exceptions
101
+ - **Unregistered task name crash.** Worker now logs an error and skips instead of crashing when it receives a task for an unknown name
102
+ - Failed tasks now log full tracebacks via `logger.exception` instead of `logger.error`
103
+ - Kafka consumer handles `None` message values without crashing
104
+ - `ActivityUpdated.status` is a `Status` enum instead of raw string
105
+
106
+ ### Documentation
107
+
108
+ - Full documentation sweep for the async API — connection strings, CLI usage, `await` on activity calls across all guides and API references
109
+
116
110
  ## v0.1.7
117
111
 
118
112
  ### New Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentexec
3
- Version: 0.2.0rc2
3
+ Version: 0.3.0
4
4
  Summary: Production-ready orchestration for OpenAI Agents with Redis-backed coordination, activity tracking, and workflow management
5
5
  Project-URL: Homepage, https://github.com/Agent-CI/agentexec
6
6
  Project-URL: Documentation, https://github.com/Agent-CI/agentexec#readme
@@ -0,0 +1,401 @@
1
+ # Activity API Reference
2
+
3
+ This document covers the activity tracking API for monitoring task execution.
4
+
5
+ All activity functions are `async` and operate on `AsyncSession` from
6
+ `sqlalchemy.ext.asyncio`. The session parameter is optional — when omitted,
7
+ functions fall back to `get_session()` from `agentexec.core.db`, which
8
+ requires `configure_engine()` to have been called.
9
+
10
+ ## Module: agentexec.activity
11
+
12
+ ```python
13
+ import agentexec as ax
14
+
15
+ # Lifecycle (async)
16
+ await ax.activity.create(task_name, message)
17
+ await ax.activity.update(agent_id, message)
18
+ await ax.activity.complete(agent_id)
19
+ await ax.activity.error(agent_id)
20
+ await ax.activity.cancel_pending()
21
+
22
+ # Query (async)
23
+ await ax.activity.list()
24
+ await ax.activity.detail(agent_id=...)
25
+ await ax.activity.count_active()
26
+ ```
27
+
28
+ ---
29
+
30
+ ## create()
31
+
32
+ Create a new activity record with an initial `QUEUED` log entry.
33
+
34
+ ```python
35
+ async def create(
36
+ task_name: str,
37
+ message: str = "Agent queued",
38
+ agent_id: str | uuid.UUID | None = None,
39
+ metadata: dict[str, Any] | None = None,
40
+ ) -> uuid.UUID
41
+ ```
42
+
43
+ ### Parameters
44
+
45
+ | Parameter | Type | Default | Description |
46
+ |-----------|------|---------|-------------|
47
+ | `task_name` | `str` | required | Name of the task |
48
+ | `message` | `str` | `"Agent queued"` | Initial log message |
49
+ | `agent_id` | `str \| UUID \| None` | `None` | Pre-generated ID (auto-generated if omitted) |
50
+ | `metadata` | `dict \| None` | `None` | Arbitrary metadata attached to the activity |
51
+
52
+ ### Returns
53
+
54
+ `uuid.UUID` — the activity's `agent_id`.
55
+
56
+ ### Example
57
+
58
+ ```python
59
+ import agentexec as ax
60
+
61
+ agent_id = await ax.activity.create(
62
+ task_name="my_task",
63
+ message="Task created",
64
+ metadata={"organization_id": "org-123"},
65
+ )
66
+ ```
67
+
68
+ Usually called automatically by `ax.enqueue()`. Call it directly only for
69
+ advanced use cases where you need to register an activity outside the normal
70
+ enqueue flow.
71
+
72
+ ---
73
+
74
+ ## update()
75
+
76
+ Append a log entry to an existing activity.
77
+
78
+ ```python
79
+ async def update(
80
+ agent_id: str | uuid.UUID,
81
+ message: str,
82
+ percentage: int | None = None,
83
+ status: Status | None = None,
84
+ ) -> bool
85
+ ```
86
+
87
+ ### Parameters
88
+
89
+ | Parameter | Type | Default | Description |
90
+ |-----------|------|---------|-------------|
91
+ | `agent_id` | `str \| UUID` | required | Activity identifier |
92
+ | `message` | `str` | required | Log message |
93
+ | `percentage` | `int \| None` | `None` | Progress (0-100) |
94
+ | `status` | `Status \| None` | `None` | Status override (defaults to `RUNNING`) |
95
+
96
+ ### Example
97
+
98
+ ```python
99
+ # Progress update
100
+ await ax.activity.update(agent_id, "Processing data...", percentage=30)
101
+
102
+ # Explicit status
103
+ from agentexec.activity import Status
104
+ await ax.activity.update(agent_id, "Starting", status=Status.RUNNING)
105
+ ```
106
+
107
+ ---
108
+
109
+ ## complete()
110
+
111
+ Mark an activity as successfully completed.
112
+
113
+ ```python
114
+ async def complete(
115
+ agent_id: str | uuid.UUID,
116
+ message: str = "Agent completed",
117
+ percentage: int = 100,
118
+ ) -> bool
119
+ ```
120
+
121
+ ### Example
122
+
123
+ ```python
124
+ await ax.activity.complete(agent_id)
125
+ await ax.activity.complete(agent_id, "Partial completion", percentage=75)
126
+ ```
127
+
128
+ ---
129
+
130
+ ## error()
131
+
132
+ Mark an activity as failed.
133
+
134
+ ```python
135
+ async def error(
136
+ agent_id: str | uuid.UUID,
137
+ message: str = "Agent failed",
138
+ percentage: int = 100,
139
+ ) -> bool
140
+ ```
141
+
142
+ ### Example
143
+
144
+ ```python
145
+ try:
146
+ await risky_operation()
147
+ except Exception as e:
148
+ await ax.activity.error(agent_id, f"Failed: {e}")
149
+ raise
150
+ ```
151
+
152
+ ---
153
+
154
+ ## cancel_pending()
155
+
156
+ Cancel all queued and running activities. Typically called at pool startup
157
+ to reconcile state from a previous run.
158
+
159
+ ```python
160
+ async def cancel_pending(
161
+ session: AsyncSession | None = None,
162
+ ) -> int
163
+ ```
164
+
165
+ ### Returns
166
+
167
+ `int` — number of activities canceled.
168
+
169
+ ### Example
170
+
171
+ ```python
172
+ from agentexec.core.db import get_session
173
+
174
+ async with get_session() as db:
175
+ canceled = await ax.activity.cancel_pending(session=db)
176
+ print(f"Canceled {canceled} activities")
177
+ ```
178
+
179
+ ---
180
+
181
+ ## list()
182
+
183
+ Get a paginated list of activities.
184
+
185
+ ```python
186
+ async def list(
187
+ session: AsyncSession | None = None,
188
+ page: int = 1,
189
+ page_size: int = 50,
190
+ metadata_filter: dict[str, Any] | None = None,
191
+ ) -> ActivityListSchema
192
+ ```
193
+
194
+ ### Parameters
195
+
196
+ | Parameter | Type | Default | Description |
197
+ |-----------|------|---------|-------------|
198
+ | `session` | `AsyncSession \| None` | `None` | Session (falls back to `get_session()`) |
199
+ | `page` | `int` | `1` | Page number (1-indexed) |
200
+ | `page_size` | `int` | `50` | Items per page |
201
+ | `metadata_filter` | `dict \| None` | `None` | Filter by metadata fields |
202
+
203
+ ### Example
204
+
205
+ ```python
206
+ result = await ax.activity.list(page=1, page_size=20)
207
+
208
+ print(f"Total: {result.total}")
209
+ print(f"Page {result.page}, total_pages: {result.total_pages}")
210
+
211
+ for item in result.items:
212
+ print(f"{item.agent_id}: {item.status}")
213
+ ```
214
+
215
+ ### With Metadata Filter
216
+
217
+ ```python
218
+ # Only activities for org-123
219
+ result = await ax.activity.list(
220
+ metadata_filter={"organization_id": "org-123"},
221
+ )
222
+ ```
223
+
224
+ ---
225
+
226
+ ## detail()
227
+
228
+ Get detailed activity with full log history.
229
+
230
+ ```python
231
+ async def detail(
232
+ session: AsyncSession | None = None,
233
+ agent_id: str | uuid.UUID | None = None,
234
+ metadata_filter: dict[str, Any] | None = None,
235
+ ) -> ActivityDetailSchema | None
236
+ ```
237
+
238
+ ### Returns
239
+
240
+ `ActivityDetailSchema | None` — activity details, or `None` if not found (or
241
+ the metadata filter didn't match).
242
+
243
+ ### Example
244
+
245
+ ```python
246
+ activity = await ax.activity.detail(agent_id=agent_id)
247
+
248
+ if activity:
249
+ print(f"Task: {activity.agent_type}")
250
+ for log in activity.logs:
251
+ print(f" [{log.created_at}] {log.status} {log.message}")
252
+ ```
253
+
254
+ ---
255
+
256
+ ## count_active()
257
+
258
+ Return the number of queued or running activities.
259
+
260
+ ```python
261
+ async def count_active(
262
+ session: AsyncSession | None = None,
263
+ ) -> int
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Status Enum
269
+
270
+ ```python
271
+ class Status(str, Enum):
272
+ QUEUED = "QUEUED"
273
+ RUNNING = "RUNNING"
274
+ COMPLETE = "COMPLETE"
275
+ ERROR = "ERROR"
276
+ CANCELED = "CANCELED"
277
+ ```
278
+
279
+ | Status | Description |
280
+ |--------|-------------|
281
+ | `QUEUED` | Task is waiting in queue |
282
+ | `RUNNING` | Task is being executed |
283
+ | `COMPLETE` | Task finished successfully |
284
+ | `ERROR` | Task failed with error |
285
+ | `CANCELED` | Task was canceled |
286
+
287
+ ---
288
+
289
+ ## Response Schemas
290
+
291
+ ### ActivityLogSchema
292
+
293
+ ```python
294
+ class ActivityLogSchema(BaseModel):
295
+ id: uuid.UUID
296
+ message: str
297
+ status: Status
298
+ percentage: int | None = 0
299
+ created_at: datetime
300
+ ```
301
+
302
+ ### ActivityDetailSchema
303
+
304
+ ```python
305
+ class ActivityDetailSchema(BaseModel):
306
+ id: uuid.UUID | None
307
+ agent_id: uuid.UUID
308
+ agent_type: str
309
+ created_at: datetime
310
+ updated_at: datetime
311
+ logs: list[ActivityLogSchema]
312
+ ```
313
+
314
+ ### ActivityListItemSchema
315
+
316
+ ```python
317
+ class ActivityListItemSchema(BaseModel):
318
+ agent_id: uuid.UUID
319
+ agent_type: str
320
+ status: Status
321
+ latest_log_message: str | None
322
+ latest_log_timestamp: datetime | None
323
+ percentage: int | None
324
+ started_at: datetime | None
325
+ elapsed_time_seconds: int # computed
326
+ ```
327
+
328
+ ### ActivityListSchema
329
+
330
+ ```python
331
+ class ActivityListSchema(BaseModel):
332
+ items: list[ActivityListItemSchema]
333
+ total: int
334
+ page: int
335
+ page_size: int
336
+ total_pages: int # computed
337
+ ```
338
+
339
+ ---
340
+
341
+ ## ORM Models
342
+
343
+ For advanced queries, use the ORM models directly with an `AsyncSession`.
344
+
345
+ ### Activity
346
+
347
+ ```python
348
+ class Activity(Base):
349
+ __tablename__ = "{prefix}activity"
350
+
351
+ id: Mapped[UUID]
352
+ agent_id: Mapped[UUID] # Unique, indexed
353
+ agent_type: Mapped[str | None]
354
+ created_at: Mapped[datetime]
355
+ updated_at: Mapped[datetime]
356
+ metadata_: Mapped[dict | None] # Column is "metadata"
357
+ logs: Mapped[list[ActivityLog]]
358
+ ```
359
+
360
+ #### Classmethods (all async)
361
+
362
+ - `Activity.create(session, agent_id, task_name, message, metadata=None)`
363
+ - `Activity.append_log(session, agent_id, message, status, percentage=None)`
364
+ - `Activity.get_by_agent_id(session, agent_id, metadata_filter=None)`
365
+ - `Activity.get_list(session, page=1, page_size=50, metadata_filter=None)`
366
+ - `Activity.get_pending_ids(session)`
367
+ - `Activity.get_active_count(session)`
368
+
369
+ ### ActivityLog
370
+
371
+ ```python
372
+ class ActivityLog(Base):
373
+ __tablename__ = "{prefix}activity_log"
374
+
375
+ id: Mapped[UUID]
376
+ activity_id: Mapped[UUID]
377
+ message: Mapped[str]
378
+ status: Mapped[Status]
379
+ percentage: Mapped[int | None]
380
+ created_at: Mapped[datetime]
381
+ ```
382
+
383
+ ---
384
+
385
+ ## Direct ORM Usage
386
+
387
+ ```python
388
+ from sqlalchemy import select
389
+ from agentexec.activity.models import Activity, ActivityLog, Status
390
+ from agentexec.core.db import get_session
391
+
392
+ async with get_session() as db:
393
+ # Latest log for a given agent
394
+ result = await db.execute(
395
+ select(ActivityLog)
396
+ .where(ActivityLog.activity_id == activity_id)
397
+ .order_by(ActivityLog.created_at.desc())
398
+ .limit(1)
399
+ )
400
+ latest = result.scalar_one_or_none()
401
+ ```