agentexec 0.1.6__tar.gz → 0.2.0rc1__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.
- agentexec-0.2.0rc1/.github/workflows/ci.yml +125 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/CHANGELOG.md +21 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/PKG-INFO +133 -52
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/README.md +129 -51
- agentexec-0.2.0rc1/docker-compose.kafka.yml +48 -0
- agentexec-0.2.0rc1/examples/queue-fairness/README.md +75 -0
- agentexec-0.2.0rc1/examples/queue-fairness/run.py +215 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/pyproject.toml +7 -1
- agentexec-0.2.0rc1/src/agentexec/activity/__init__.py +103 -0
- agentexec-0.2.0rc1/src/agentexec/activity/events.py +25 -0
- agentexec-0.2.0rc1/src/agentexec/activity/handlers.py +103 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/src/agentexec/activity/models.py +2 -11
- agentexec-0.2.0rc1/src/agentexec/activity/producer.py +179 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/src/agentexec/activity/schemas.py +9 -5
- agentexec-0.2.0rc1/src/agentexec/activity/status.py +11 -0
- agentexec-0.2.0rc1/src/agentexec/config.py +167 -0
- agentexec-0.2.0rc1/src/agentexec/core/db.py +52 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/src/agentexec/core/logging.py +0 -6
- agentexec-0.2.0rc1/src/agentexec/core/queue.py +65 -0
- agentexec-0.2.0rc1/src/agentexec/core/results.py +40 -0
- agentexec-0.2.0rc1/src/agentexec/core/task.py +213 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/src/agentexec/pipeline.py +1 -1
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/src/agentexec/runners/base.py +2 -2
- agentexec-0.2.0rc1/src/agentexec/schedule.py +102 -0
- agentexec-0.2.0rc1/src/agentexec/state/__init__.py +38 -0
- agentexec-0.2.0rc1/src/agentexec/state/base.py +105 -0
- agentexec-0.2.0rc1/src/agentexec/state/kafka.py +298 -0
- agentexec-0.2.0rc1/src/agentexec/state/redis.py +264 -0
- agentexec-0.2.0rc1/src/agentexec/tracker.py +48 -0
- agentexec-0.2.0rc1/src/agentexec/worker/event.py +31 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/src/agentexec/worker/logging.py +12 -32
- agentexec-0.2.0rc1/src/agentexec/worker/pool.py +517 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_activity_schemas.py +0 -2
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_activity_tracking.py +85 -90
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_config.py +7 -9
- agentexec-0.2.0rc1/tests/test_db.py +38 -0
- agentexec-0.2.0rc1/tests/test_kafka_integration.py +158 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_pipeline.py +0 -2
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_pipeline_flow.py +0 -49
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_public_api.py +0 -2
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_queue.py +20 -60
- agentexec-0.2.0rc1/tests/test_queue_partitions.py +173 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_results.py +29 -52
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_runners.py +3 -5
- agentexec-0.2.0rc1/tests/test_schedule.py +385 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_self_describing_results.py +17 -42
- agentexec-0.2.0rc1/tests/test_state.py +59 -0
- agentexec-0.2.0rc1/tests/test_state_backend.py +120 -0
- agentexec-0.2.0rc1/tests/test_task.py +378 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_task_locking.py +31 -113
- agentexec-0.2.0rc1/tests/test_worker_event.py +71 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_worker_logging.py +30 -62
- agentexec-0.2.0rc1/tests/test_worker_pool.py +536 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/package.json +1 -1
- agentexec-0.1.6/.claude/settings.local.json +0 -7
- agentexec-0.1.6/src/agentexec/activity/__init__.py +0 -39
- agentexec-0.1.6/src/agentexec/activity/tracker.py +0 -286
- agentexec-0.1.6/src/agentexec/config.py +0 -102
- agentexec-0.1.6/src/agentexec/core/db.py +0 -62
- agentexec-0.1.6/src/agentexec/core/queue.py +0 -132
- agentexec-0.1.6/src/agentexec/core/results.py +0 -64
- agentexec-0.1.6/src/agentexec/core/task.py +0 -336
- agentexec-0.1.6/src/agentexec/state/__init__.py +0 -264
- agentexec-0.1.6/src/agentexec/state/backend.py +0 -320
- agentexec-0.1.6/src/agentexec/state/redis_backend.py +0 -443
- agentexec-0.1.6/src/agentexec/tracker.py +0 -67
- agentexec-0.1.6/src/agentexec/worker/event.py +0 -48
- agentexec-0.1.6/src/agentexec/worker/pool.py +0 -385
- agentexec-0.1.6/tests/test_db.py +0 -134
- agentexec-0.1.6/tests/test_state.py +0 -185
- agentexec-0.1.6/tests/test_state_backend.py +0 -292
- agentexec-0.1.6/tests/test_task.py +0 -316
- agentexec-0.1.6/tests/test_worker_event.py +0 -133
- agentexec-0.1.6/tests/test_worker_pool.py +0 -284
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/.claude/skills/prepare-release/SKILL.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/.github/workflows/docker-publish.yml +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/.github/workflows/npm-publish.yml +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/.github/workflows/publish.yml +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/.gitignore +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docker/Dockerfile +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docker/README.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docker/entrypoint.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/api-reference/activity.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/api-reference/core.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/api-reference/pipeline.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/api-reference/runner.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/concepts/activity-tracking.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/concepts/architecture.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/concepts/task-lifecycle.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/concepts/worker-pool.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/contributing.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/deployment/docker.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/deployment/production.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/getting-started/configuration.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/getting-started/installation.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/getting-started/quickstart.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/guides/basic-usage.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/guides/custom-runners.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/guides/fastapi-integration.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/guides/openai-runner.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/guides/pipelines.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/docs/index.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/multi-tenancy/README.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/multi-tenancy/example.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/README.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/alembic/README +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/alembic/env.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/alembic/script.py.mako +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/alembic.ini +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/compose.yml +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/context.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/db.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/main.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/models.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/pipeline.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/pyproject.toml +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/tools.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/.gitignore +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/bun.lock +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/index.html +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/package.json +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/public/vite.svg +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/src/App.tsx +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/src/api/agents.ts +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/src/api/queries.ts +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/src/components/Layout.tsx +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/src/index.css +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/src/main.tsx +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/src/pages/AgentDetailPage.tsx +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/src/pages/AgentListPage.tsx +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/src/styles/github-dark.css +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/tsconfig.json +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/tsconfig.node.json +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/ui/vite.config.ts +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/views.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/examples/openai-agents-fastapi/worker.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/src/agentexec/__init__.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/src/agentexec/core/__init__.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/src/agentexec/runners/__init__.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/src/agentexec/runners/openai.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/src/agentexec/worker/__init__.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_activity_tracking.py.bak +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/tests/test_task_types.py +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/.gitignore +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/README.md +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/bun.lock +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/src/components/ActiveAgentsBadge.tsx +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/src/components/ProgressBar.tsx +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/src/components/StatusBadge.tsx +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/src/components/TaskDetail.tsx +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/src/components/TaskList.tsx +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/src/components/index.ts +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/src/index.ts +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/src/types.ts +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/tsconfig.json +0 -0
- {agentexec-0.1.6 → agentexec-0.2.0rc1}/ui/vite.config.ts +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
# -----------------------------------------------------------------------
|
|
11
|
+
# Unit tests — no external services (fakeredis + SQLite)
|
|
12
|
+
# -----------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
python-version: ["3.12", "3.13"]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v6
|
|
26
|
+
with:
|
|
27
|
+
enable-cache: true
|
|
28
|
+
|
|
29
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
30
|
+
run: uv python install ${{ matrix.python-version }}
|
|
31
|
+
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: uv sync --dev
|
|
34
|
+
|
|
35
|
+
- name: Run unit tests
|
|
36
|
+
run: |
|
|
37
|
+
uv run pytest tests/ \
|
|
38
|
+
--ignore=tests/test_kafka_integration.py \
|
|
39
|
+
-o "addopts=" \
|
|
40
|
+
-v --tb=long
|
|
41
|
+
|
|
42
|
+
# -----------------------------------------------------------------------
|
|
43
|
+
# Kafka integration tests — real broker via docker run
|
|
44
|
+
# -----------------------------------------------------------------------
|
|
45
|
+
test-kafka:
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
|
|
51
|
+
- name: Start Kafka broker
|
|
52
|
+
run: |
|
|
53
|
+
docker run -d --name kafka \
|
|
54
|
+
-p 9092:9092 \
|
|
55
|
+
-e KAFKA_NODE_ID=1 \
|
|
56
|
+
-e KAFKA_PROCESS_ROLES=broker,controller \
|
|
57
|
+
-e KAFKA_CONTROLLER_QUORUM_VOTERS=1@localhost:9093 \
|
|
58
|
+
-e KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER \
|
|
59
|
+
-e KAFKA_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \
|
|
60
|
+
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 \
|
|
61
|
+
-e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT \
|
|
62
|
+
-e KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT \
|
|
63
|
+
-e KAFKA_LOG_CLEANER_MIN_COMPACTION_LAG_MS=0 \
|
|
64
|
+
-e KAFKA_LOG_CLEANER_MIN_CLEANABLE_RATIO=0.01 \
|
|
65
|
+
-e KAFKA_LOG_RETENTION_MS=60000 \
|
|
66
|
+
-e KAFKA_NUM_PARTITIONS=1 \
|
|
67
|
+
-e KAFKA_AUTO_CREATE_TOPICS_ENABLE=true \
|
|
68
|
+
-e KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS=0 \
|
|
69
|
+
-e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
|
|
70
|
+
-e CLUSTER_ID=ciTestCluster0001 \
|
|
71
|
+
apache/kafka:3.9.0
|
|
72
|
+
|
|
73
|
+
- name: Install uv
|
|
74
|
+
uses: astral-sh/setup-uv@v6
|
|
75
|
+
with:
|
|
76
|
+
enable-cache: true
|
|
77
|
+
|
|
78
|
+
- name: Set up Python
|
|
79
|
+
run: uv python install 3.12
|
|
80
|
+
|
|
81
|
+
- name: Install dependencies
|
|
82
|
+
run: uv sync --dev --extra kafka
|
|
83
|
+
|
|
84
|
+
- name: Wait for Kafka to be ready
|
|
85
|
+
run: |
|
|
86
|
+
echo "Waiting for Kafka..."
|
|
87
|
+
for i in $(seq 1 30); do
|
|
88
|
+
if nc -z localhost 9092 2>/dev/null; then
|
|
89
|
+
echo "Kafka port is open"
|
|
90
|
+
sleep 5
|
|
91
|
+
echo "Kafka is ready"
|
|
92
|
+
exit 0
|
|
93
|
+
fi
|
|
94
|
+
echo " attempt $i/30..."
|
|
95
|
+
sleep 2
|
|
96
|
+
done
|
|
97
|
+
echo "Kafka failed to start"
|
|
98
|
+
docker logs kafka
|
|
99
|
+
exit 1
|
|
100
|
+
|
|
101
|
+
- name: Run Kafka integration tests
|
|
102
|
+
timeout-minutes: 2
|
|
103
|
+
run: |
|
|
104
|
+
uv run pytest tests/test_kafka_integration.py \
|
|
105
|
+
-o "addopts=" \
|
|
106
|
+
-v --tb=long 2>&1 | tee /tmp/kafka_test_output.txt
|
|
107
|
+
exit ${PIPESTATUS[0]}
|
|
108
|
+
env:
|
|
109
|
+
AGENTEXEC_STATE_BACKEND: agentexec.state.kafka
|
|
110
|
+
KAFKA_BOOTSTRAP_SERVERS: localhost:9092
|
|
111
|
+
AGENTEXEC_KAFKA_DEFAULT_PARTITIONS: "2"
|
|
112
|
+
AGENTEXEC_KAFKA_REPLICATION_FACTOR: "1"
|
|
113
|
+
|
|
114
|
+
- name: Show Kafka logs on failure
|
|
115
|
+
if: failure()
|
|
116
|
+
run: docker logs kafka 2>&1 | tail -50
|
|
117
|
+
|
|
118
|
+
- name: Create failure check annotation with output
|
|
119
|
+
if: failure()
|
|
120
|
+
run: |
|
|
121
|
+
if [ -f /tmp/kafka_test_output.txt ]; then
|
|
122
|
+
grep -E '\[queue_|FAILED|ERROR|AssertionError|TIMEOUT|short test summary' /tmp/kafka_test_output.txt | tail -9 | while IFS= read -r line; do
|
|
123
|
+
echo "::warning::$line"
|
|
124
|
+
done
|
|
125
|
+
fi
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.1.7
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
**Scheduled tasks with cron expressions**
|
|
8
|
+
- `@pool.schedule("task_name", "*/5 * * * *")` decorator registers and schedules a task in one step
|
|
9
|
+
- `pool.add_schedule()` for imperative scheduling of already-registered tasks
|
|
10
|
+
- Cron expressions evaluated in configurable timezone (`AGENTEXEC_SCHEDULER_TIMEZONE`, default UTC)
|
|
11
|
+
- Repeat budget: `-1` for forever (default), `0` for one-shot, `N` for N more executions
|
|
12
|
+
- Scheduler runs automatically inside `pool.run()` — no extra setup needed
|
|
13
|
+
- Idempotent registration: keyed by task name, so restarts and multiple pool instances overwrite instead of duplicating
|
|
14
|
+
- Clock-drift resilient: next run computed from intended anchor time, not wall clock
|
|
15
|
+
- Skips missed intervals after downtime instead of enqueuing a burst of catch-up tasks
|
|
16
|
+
- New `croniter` dependency for cron expression parsing
|
|
17
|
+
|
|
18
|
+
### Improvements
|
|
19
|
+
|
|
20
|
+
**State backend sorted set operations**
|
|
21
|
+
- Added `zadd()`, `zrangebyscore()`, `zrem()` to `StateBackend` protocol and Redis implementation
|
|
22
|
+
- Used internally by the scheduler for efficient due-task polling
|
|
23
|
+
|
|
3
24
|
## v0.1.6
|
|
4
25
|
|
|
5
26
|
### New Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentexec
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0rc1
|
|
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
|
|
@@ -16,11 +16,14 @@ Classifier: Programming Language :: Python :: 3
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: croniter>=6.0.0
|
|
19
20
|
Requires-Dist: openai-agents>=0.1.0
|
|
20
21
|
Requires-Dist: pydantic-settings>=2.5.0
|
|
21
22
|
Requires-Dist: pydantic>=2.12.0
|
|
22
23
|
Requires-Dist: redis>=7.0.1
|
|
23
24
|
Requires-Dist: sqlalchemy>=2.0.44
|
|
25
|
+
Provides-Extra: kafka
|
|
26
|
+
Requires-Dist: aiokafka>=0.11.0; extra == 'kafka'
|
|
24
27
|
Description-Content-Type: text/markdown
|
|
25
28
|
|
|
26
29
|
# `agentexec`
|
|
@@ -147,8 +150,8 @@ async def start_research(company: str) -> dict:
|
|
|
147
150
|
return {"agent_id": str(task.agent_id), "status": "queued"} # Return agent_id for status polling
|
|
148
151
|
|
|
149
152
|
@router.get("/research/{agent_id}")
|
|
150
|
-
def get_status(agent_id: UUID
|
|
151
|
-
return ax.activity.detail(
|
|
153
|
+
async def get_status(agent_id: UUID) -> ax.activity.ActivityDetailSchema:
|
|
154
|
+
return await ax.activity.detail(agent_id=agent_id)
|
|
152
155
|
```
|
|
153
156
|
|
|
154
157
|
### 4. Run Workers
|
|
@@ -175,8 +178,8 @@ task = await ax.enqueue(
|
|
|
175
178
|
)
|
|
176
179
|
|
|
177
180
|
# Filter activities by metadata
|
|
178
|
-
activities = ax.activity.list(
|
|
179
|
-
detail = ax.activity.detail(
|
|
181
|
+
activities = await ax.activity.list(metadata_filter={"organization_id": "org-123"})
|
|
182
|
+
detail = await ax.activity.detail(agent_id=agent_id, metadata_filter={"organization_id": "org-123"})
|
|
180
183
|
|
|
181
184
|
# Access metadata programmatically (excluded from API serialization by default)
|
|
182
185
|
org_id = detail.metadata["organization_id"]
|
|
@@ -211,7 +214,7 @@ agent = Agent(
|
|
|
211
214
|
Update progress explicitly from your task:
|
|
212
215
|
|
|
213
216
|
```python
|
|
214
|
-
ax.activity.update(agent_id, "Processing batch 3 of 10", percentage=30)
|
|
217
|
+
await ax.activity.update(agent_id, "Processing batch 3 of 10", percentage=30)
|
|
215
218
|
```
|
|
216
219
|
|
|
217
220
|
### Task Locking
|
|
@@ -227,11 +230,34 @@ async def associate(agent_id: UUID, context: ObservationContext):
|
|
|
227
230
|
pool.add_task("associate_observation", handler, lock_key="user:{user_id}")
|
|
228
231
|
```
|
|
229
232
|
|
|
230
|
-
The `lock_key` is a string template evaluated against the task context fields.
|
|
233
|
+
The `lock_key` is a string template evaluated against the task context fields. Tasks with the same evaluated lock key are routed to a dedicated partition queue (`{prefix}:{lock_key}`) where they execute one at a time. Workers skip locked partitions and move on to the next available one — no requeuing, no wasted cycles.
|
|
231
234
|
|
|
232
|
-
The lock TTL (`AGENTEXEC_LOCK_TTL`, default 1800s) is a safety net for worker process death — locks are always explicitly released
|
|
235
|
+
The lock is released automatically when a task completes or errors. The lock TTL (`AGENTEXEC_LOCK_TTL`, default 1800s) is a safety net for worker process death (OOM, SIGKILL) — under normal operation, locks are always explicitly released. Set this higher than your longest expected task duration.
|
|
233
236
|
|
|
234
|
-
|
|
237
|
+
### Scheduled Tasks
|
|
238
|
+
|
|
239
|
+
Run tasks on a recurring interval using cron expressions:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
# Decorator — registers the task and schedules it in one step
|
|
243
|
+
@pool.schedule("refresh_cache", "*/5 * * * *")
|
|
244
|
+
async def refresh(agent_id: UUID, context: RefreshContext):
|
|
245
|
+
...
|
|
246
|
+
|
|
247
|
+
# With context and repeat limit
|
|
248
|
+
@pool.schedule("sync_users", "0 * * * *", context=SyncContext(full=True), repeat=3)
|
|
249
|
+
async def sync(agent_id: UUID, context: SyncContext):
|
|
250
|
+
...
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
For tasks registered separately, use `pool.add_schedule()`:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
pool.add_schedule("refresh_cache", "*/5 * * * *", RefreshContext(scope="all"))
|
|
257
|
+
pool.add_schedule("refresh_cache", "0 * * * *", RefreshContext(scope="users"), repeat=3)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The scheduler runs automatically inside `pool.run()`. Cron expressions are evaluated in the configured timezone (`AGENTEXEC_SCHEDULER_TIMEZONE`, default UTC) so schedules read naturally regardless of server timezone. Next-run times are computed from the intended anchor time, not wall clock, to prevent cumulative drift.
|
|
235
261
|
|
|
236
262
|
### Priority Queue
|
|
237
263
|
|
|
@@ -366,8 +392,7 @@ if __name__ == "__main__":
|
|
|
366
392
|
try:
|
|
367
393
|
pool.run()
|
|
368
394
|
except KeyboardInterrupt:
|
|
369
|
-
|
|
370
|
-
ax.activity.cancel_pending(db)
|
|
395
|
+
asyncio.run(ax.activity.cancel_pending())
|
|
371
396
|
```
|
|
372
397
|
|
|
373
398
|
### Docker Deployment
|
|
@@ -396,11 +421,10 @@ import agentexec as ax
|
|
|
396
421
|
engine = create_engine(os.environ["DATABASE_URL"])
|
|
397
422
|
pool = ax.Pool(engine=engine)
|
|
398
423
|
|
|
399
|
-
def cleanup() -> None:
|
|
400
|
-
|
|
401
|
-
ax.activity.cancel_pending(db)
|
|
424
|
+
async def cleanup() -> None:
|
|
425
|
+
await ax.activity.cancel_pending()
|
|
402
426
|
|
|
403
|
-
atexit.register(cleanup)
|
|
427
|
+
atexit.register(lambda: asyncio.run(cleanup()))
|
|
404
428
|
|
|
405
429
|
@pool.task("my_task")
|
|
406
430
|
async def my_task(agent_id: UUID, context: MyContext) -> None:
|
|
@@ -421,11 +445,13 @@ docker run -e DATABASE_URL=... -e REDIS_URL=... -e OPENAI_API_KEY=... my-worker
|
|
|
421
445
|
|
|
422
446
|
## Backend Architecture
|
|
423
447
|
|
|
424
|
-
### Redis
|
|
448
|
+
### Redis (Default)
|
|
449
|
+
|
|
450
|
+
agentexec uses Redis for task queuing, result storage, and coordination between workers. The queue uses a partitioned design where tasks with a `lock_key` go to dedicated partition queues (`{prefix}:{lock_key}`) and are serialized by a lock, while tasks without a lock key go to the default queue for concurrent processing.
|
|
425
451
|
|
|
426
|
-
|
|
452
|
+
Workers dequeue using Redis `SCAN`, which iterates keys in hash-table order — effectively random. This provides fair distribution across partitions without explicit round-robin. See `examples/queue-fairness/` for benchmarks showing uniform distribution at 1000+ partitions.
|
|
427
453
|
|
|
428
|
-
**AWS Compatible:**
|
|
454
|
+
**AWS Compatible:** Standard Redis features only — AWS ElastiCache works out of the box.
|
|
429
455
|
|
|
430
456
|
```bash
|
|
431
457
|
AGENTEXEC_REDIS_URL=redis://localhost:6379/0
|
|
@@ -433,18 +459,45 @@ AGENTEXEC_REDIS_URL=redis://localhost:6379/0
|
|
|
433
459
|
AGENTEXEC_REDIS_URL=redis://my-cluster.abc123.use1.cache.amazonaws.com:6379
|
|
434
460
|
```
|
|
435
461
|
|
|
462
|
+
### Kafka (Experimental)
|
|
463
|
+
|
|
464
|
+
Kafka can be used as an alternative backend for task queuing and schedule storage. Activity tracking always uses PostgreSQL regardless of backend — Kafka is not a KV store, so state operations (`get`/`set`, counters) are not supported and will raise `NotImplementedError`.
|
|
465
|
+
|
|
466
|
+
```bash
|
|
467
|
+
pip install agentexec[kafka]
|
|
468
|
+
|
|
469
|
+
AGENTEXEC_STATE_BACKEND=agentexec.state.kafka
|
|
470
|
+
KAFKA_BOOTSTRAP_SERVERS=localhost:9092
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Kafka uses consumer groups for work distribution instead of Redis's scan-based dequeue. Topics are auto-created on first use. Schedule storage uses a compacted topic that is replayed on each poll.
|
|
474
|
+
|
|
475
|
+
**When to consider Kafka:**
|
|
476
|
+
- You already run Kafka and want to avoid adding Redis
|
|
477
|
+
- You need durable, replayable task queues with built-in replication
|
|
478
|
+
- You want partition-level ordering guarantees (tasks with the same key go to the same partition)
|
|
479
|
+
|
|
480
|
+
**Limitations:**
|
|
481
|
+
- No KV state — `backend.state.get/set/delete` and counters raise `NotImplementedError`
|
|
482
|
+
- No partition-level locking (Kafka partition assignment handles isolation instead)
|
|
483
|
+
- Schedule `get_due()` replays the entire compacted topic on every poll
|
|
484
|
+
- `lock_key` is used as a Kafka partition key (routing), not as a mutex
|
|
485
|
+
|
|
486
|
+
See [Kafka configuration](#kafka-settings) below for all available settings.
|
|
487
|
+
|
|
436
488
|
### Extensible State Backend
|
|
437
489
|
|
|
438
|
-
The state backend is pluggable.
|
|
490
|
+
The state backend is pluggable. Implement `BaseBackend` with `state`, `queue`, and `schedule` sub-backends:
|
|
439
491
|
|
|
440
492
|
```bash
|
|
441
|
-
AGENTEXEC_STATE_BACKEND=agentexec.state.
|
|
442
|
-
AGENTEXEC_STATE_BACKEND=
|
|
493
|
+
AGENTEXEC_STATE_BACKEND=agentexec.state.redis # Default
|
|
494
|
+
AGENTEXEC_STATE_BACKEND=agentexec.state.kafka # Experimental
|
|
495
|
+
AGENTEXEC_STATE_BACKEND=myapp.state.custom # Custom (must export Backend class)
|
|
443
496
|
```
|
|
444
497
|
|
|
445
498
|
### Database
|
|
446
499
|
|
|
447
|
-
Activity tracking uses SQLAlchemy with two tables:
|
|
500
|
+
Activity tracking uses SQLAlchemy with two tables (always PostgreSQL/SQLite, independent of the state backend):
|
|
448
501
|
|
|
449
502
|
**`agentexec_activity`** - Main activity records
|
|
450
503
|
- `agent_id` - Unique identifier (UUID)
|
|
@@ -478,25 +531,23 @@ from agentexec.activity.schemas import (
|
|
|
478
531
|
**List activities:**
|
|
479
532
|
|
|
480
533
|
```python
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
# }
|
|
534
|
+
result = await ax.activity.list(page=1, page_size=20)
|
|
535
|
+
# Returns ActivityListSchema:
|
|
536
|
+
# {
|
|
537
|
+
# "items": [...], # List of ActivityListItemSchema
|
|
538
|
+
# "total": 150,
|
|
539
|
+
# "page": 1,
|
|
540
|
+
# "page_size": 20,
|
|
541
|
+
# "total_pages": 8
|
|
542
|
+
# }
|
|
491
543
|
```
|
|
492
544
|
|
|
493
545
|
**Get activity detail:**
|
|
494
546
|
|
|
495
547
|
```python
|
|
496
|
-
activity = ax.activity.detail(
|
|
548
|
+
activity = await ax.activity.detail(agent_id=agent_id)
|
|
497
549
|
# Returns ActivityDetailSchema:
|
|
498
550
|
# {
|
|
499
|
-
# "id": "...",
|
|
500
551
|
# "agent_id": "...",
|
|
501
552
|
# "agent_type": "research_company",
|
|
502
553
|
# "created_at": "2024-01-15T10:30:00Z",
|
|
@@ -512,7 +563,7 @@ activity = ax.activity.detail(db, agent_id=agent_id)
|
|
|
512
563
|
**Count active agents:**
|
|
513
564
|
|
|
514
565
|
```python
|
|
515
|
-
count = ax.activity.
|
|
566
|
+
count = await ax.activity.count_active()
|
|
516
567
|
# Returns number of agents with status QUEUED or RUNNING
|
|
517
568
|
```
|
|
518
569
|
|
|
@@ -527,13 +578,15 @@ from sqlalchemy.orm import Session
|
|
|
527
578
|
import agentexec as ax
|
|
528
579
|
|
|
529
580
|
def build_table(db: Session) -> Table:
|
|
530
|
-
|
|
581
|
+
count = asyncio.run(ax.activity.count_active())
|
|
582
|
+
table = Table(title=f"Active Agents: {count}")
|
|
531
583
|
table.add_column("Status")
|
|
532
584
|
table.add_column("Task")
|
|
533
585
|
table.add_column("Message")
|
|
534
586
|
table.add_column("Progress")
|
|
535
587
|
|
|
536
|
-
|
|
588
|
+
activities = asyncio.run(ax.activity.list(page=1, page_size=10))
|
|
589
|
+
for item in activities.items:
|
|
537
590
|
table.add_row(
|
|
538
591
|
item.status,
|
|
539
592
|
item.agent_type,
|
|
@@ -642,7 +695,12 @@ async def handler(agent_id: UUID, context: MyContext) -> None: ...
|
|
|
642
695
|
@pool.task("name", lock_key="user:{user_id}") # Sequential per user
|
|
643
696
|
async def locked(agent_id: UUID, context: MyContext) -> None: ...
|
|
644
697
|
|
|
645
|
-
pool.
|
|
698
|
+
@pool.schedule("name", "*/5 * * * *") # Register + schedule in one step
|
|
699
|
+
async def scheduled(agent_id: UUID, context: MyContext) -> None: ...
|
|
700
|
+
|
|
701
|
+
pool.add_schedule("name", "0 * * * *", MyContext(), repeat=3) # Schedule separately
|
|
702
|
+
|
|
703
|
+
pool.run() # Blocking - runs workers + scheduler + retry handling
|
|
646
704
|
pool.start() # Non-blocking - starts workers in background
|
|
647
705
|
pool.shutdown() # Graceful shutdown
|
|
648
706
|
```
|
|
@@ -653,20 +711,20 @@ pool.shutdown() # Graceful shutdown
|
|
|
653
711
|
import agentexec as ax
|
|
654
712
|
|
|
655
713
|
# Create activity (returns agent_id for tracking)
|
|
656
|
-
agent_id = ax.activity.create(task_name, message="Starting...")
|
|
714
|
+
agent_id = await ax.activity.create(task_name, message="Starting...")
|
|
657
715
|
|
|
658
716
|
# Update progress
|
|
659
|
-
ax.activity.update(agent_id, message, percentage=50)
|
|
660
|
-
ax.activity.complete(agent_id, message="Done")
|
|
661
|
-
ax.activity.error(agent_id,
|
|
717
|
+
await ax.activity.update(agent_id, message, percentage=50)
|
|
718
|
+
await ax.activity.complete(agent_id, message="Done")
|
|
719
|
+
await ax.activity.error(agent_id, message="Failed: ...")
|
|
662
720
|
|
|
663
|
-
# Query activities
|
|
664
|
-
activities = ax.activity.list(
|
|
665
|
-
activity = ax.activity.detail(
|
|
666
|
-
count = ax.activity.
|
|
721
|
+
# Query activities (uses database session)
|
|
722
|
+
activities = await ax.activity.list(page=1, page_size=20)
|
|
723
|
+
activity = await ax.activity.detail(agent_id=agent_id)
|
|
724
|
+
count = await ax.activity.count_active()
|
|
667
725
|
|
|
668
726
|
# Cleanup
|
|
669
|
-
canceled = ax.activity.cancel_pending(
|
|
727
|
+
canceled = await ax.activity.cancel_pending()
|
|
670
728
|
```
|
|
671
729
|
|
|
672
730
|
### Runners
|
|
@@ -728,13 +786,16 @@ ax.Base # SQLAlchemy declarative base for activity tables
|
|
|
728
786
|
All settings via environment variables:
|
|
729
787
|
|
|
730
788
|
```bash
|
|
731
|
-
# Redis
|
|
732
|
-
AGENTEXEC_REDIS_URL=redis://localhost:6379/0
|
|
789
|
+
# Redis
|
|
790
|
+
AGENTEXEC_REDIS_URL=redis://localhost:6379/0 # Also accepts REDIS_URL
|
|
791
|
+
AGENTEXEC_REDIS_POOL_SIZE=10
|
|
792
|
+
AGENTEXEC_REDIS_POOL_TIMEOUT=5
|
|
733
793
|
|
|
734
794
|
# Workers
|
|
735
795
|
AGENTEXEC_NUM_WORKERS=4
|
|
736
|
-
|
|
796
|
+
AGENTEXEC_QUEUE_PREFIX=agentexec_tasks # Also accepts AGENTEXEC_QUEUE_NAME
|
|
737
797
|
AGENTEXEC_GRACEFUL_SHUTDOWN_TIMEOUT=300
|
|
798
|
+
AGENTEXEC_MAX_TASK_RETRIES=3 # 0 to disable retries
|
|
738
799
|
|
|
739
800
|
# Database
|
|
740
801
|
AGENTEXEC_TABLE_PREFIX=agentexec_
|
|
@@ -742,11 +803,15 @@ AGENTEXEC_TABLE_PREFIX=agentexec_
|
|
|
742
803
|
# Results
|
|
743
804
|
AGENTEXEC_RESULT_TTL=3600
|
|
744
805
|
|
|
745
|
-
# Task locking
|
|
806
|
+
# Task locking (Redis backend only)
|
|
746
807
|
AGENTEXEC_LOCK_TTL=1800
|
|
747
808
|
|
|
809
|
+
# Scheduling
|
|
810
|
+
AGENTEXEC_SCHEDULER_TIMEZONE=UTC
|
|
811
|
+
AGENTEXEC_SCHEDULER_POLL_INTERVAL=10
|
|
812
|
+
|
|
748
813
|
# State backend
|
|
749
|
-
AGENTEXEC_STATE_BACKEND=agentexec.state.
|
|
814
|
+
AGENTEXEC_STATE_BACKEND=agentexec.state.redis # or agentexec.state.kafka
|
|
750
815
|
AGENTEXEC_KEY_PREFIX=agentexec
|
|
751
816
|
|
|
752
817
|
# Activity messages (customizable)
|
|
@@ -756,6 +821,21 @@ AGENTEXEC_ACTIVITY_MESSAGE_COMPLETE="Task completed successfully."
|
|
|
756
821
|
AGENTEXEC_ACTIVITY_MESSAGE_ERROR="Task failed with error: {error}"
|
|
757
822
|
```
|
|
758
823
|
|
|
824
|
+
### Kafka Settings
|
|
825
|
+
|
|
826
|
+
These settings only apply when using the Kafka state backend (`AGENTEXEC_STATE_BACKEND=agentexec.state.kafka`):
|
|
827
|
+
|
|
828
|
+
```bash
|
|
829
|
+
KAFKA_BOOTSTRAP_SERVERS=localhost:9092 # Also accepts AGENTEXEC_KAFKA_BOOTSTRAP_SERVERS
|
|
830
|
+
AGENTEXEC_KAFKA_DEFAULT_PARTITIONS=6 # Partitions for auto-created topics
|
|
831
|
+
AGENTEXEC_KAFKA_REPLICATION_FACTOR=1 # Replication factor for auto-created topics
|
|
832
|
+
AGENTEXEC_KAFKA_MAX_BATCH_SIZE=16384 # Producer max batch size (bytes)
|
|
833
|
+
AGENTEXEC_KAFKA_LINGER_MS=5 # Producer linger time (ms)
|
|
834
|
+
AGENTEXEC_KAFKA_RETENTION_MS=-1 # Retention for compacted topics (-1 = forever)
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
For single-node development, set `KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1` on your broker or consumer groups will hang.
|
|
838
|
+
|
|
759
839
|
---
|
|
760
840
|
|
|
761
841
|
## Development
|
|
@@ -804,4 +884,5 @@ MIT License - see [LICENSE](LICENSE) for details.
|
|
|
804
884
|
- **Documentation**: [docs/](docs/)
|
|
805
885
|
- **Example App**: [examples/openai-agents-fastapi/](examples/openai-agents-fastapi/)
|
|
806
886
|
- **Multi-Tenancy Example**: [examples/multi-tenancy/](examples/multi-tenancy/)
|
|
887
|
+
- **Queue Fairness Benchmark**: [examples/queue-fairness/](examples/queue-fairness/)
|
|
807
888
|
- **Issues**: [GitHub Issues](https://github.com/Agent-CI/agentexec/issues)
|