agentseek-api 0.1.0__tar.gz → 0.2.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 (228) hide show
  1. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/.env.example +4 -0
  2. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/.github/workflows/ci.yml +3 -0
  3. agentseek_api-0.2.0/CHANGELOG.md +22 -0
  4. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/PKG-INFO +14 -6
  5. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/README.md +11 -3
  6. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/README.zh-CN.md +10 -3
  7. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/pyproject.toml +3 -3
  8. agentseek_api-0.2.0/src/agentseek_api/__init__.py +1 -0
  9. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/a2a_server.py +3 -3
  10. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/api/crons.py +20 -11
  11. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/api/runs.py +9 -32
  12. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/api/threads.py +8 -28
  13. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/database.py +55 -1
  14. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/oceanbase_checkpointer.py +0 -1
  15. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/orm.py +5 -1
  16. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/models/api.py +60 -6
  17. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/cron_models.py +6 -0
  18. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/cron_scheduler.py +39 -1
  19. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/cron_service.py +142 -26
  20. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/run_executor.py +32 -4
  21. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/run_jobs.py +39 -5
  22. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/sse.py +3 -1
  23. agentseek_api-0.2.0/src/agentseek_api/services/stream_modes.py +60 -0
  24. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/stream_persistence.py +152 -18
  25. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/thread_protocol.py +51 -17
  26. agentseek_api-0.2.0/src/agentseek_api/services/thread_service.py +116 -0
  27. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/settings.py +2 -0
  28. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/conftest.py +35 -1
  29. agentseek_api-0.2.0/tests/e2e/test_cron_api_live.py +206 -0
  30. agentseek_api-0.2.0/tests/e2e/test_cron_scheduler_live.py +162 -0
  31. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_auth_coverage_gaps.py +0 -2
  32. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_auth_handlers.py +18 -1
  33. agentseek_api-0.2.0/tests/integration/test_cron_api.py +659 -0
  34. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_langsmith_compat_coverage.py +11 -0
  35. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_runs_compat.py +17 -0
  36. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_scheduler_runtime.py +335 -1
  37. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_stream_persistence.py +46 -0
  38. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_a2a_server.py +20 -0
  39. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_auth_internals.py +3 -3
  40. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_cli.py +15 -0
  41. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_content_type_fix.py +0 -1
  42. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_cron_service.py +48 -2
  43. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_database_manager.py +169 -0
  44. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_default_assistants.py +0 -1
  45. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_run_executor.py +32 -0
  46. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_run_jobs.py +91 -0
  47. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_scheduler.py +89 -0
  48. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_sse_keepalive.py +6 -0
  49. agentseek_api-0.2.0/tests/unit/test_stream_modes.py +42 -0
  50. agentseek_api-0.2.0/tests/unit/test_stream_persistence_helpers.py +438 -0
  51. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_thread_protocol.py +145 -0
  52. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_thread_routes_unit.py +22 -8
  53. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/uv.lock +6 -6
  54. agentseek_api-0.1.0/src/agentseek_api/__init__.py +0 -1
  55. agentseek_api-0.1.0/src/agentseek_api/services/thread_service.py +0 -64
  56. agentseek_api-0.1.0/tests/integration/test_cron_api.py +0 -223
  57. agentseek_api-0.1.0/tests/unit/test_stream_persistence_helpers.py +0 -194
  58. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/.dockerignore +0 -0
  59. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  60. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/.github/workflows/live-provider-streaming.yml +0 -0
  61. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/.github/workflows/release.yml +0 -0
  62. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/.gitignore +0 -0
  63. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/AGENTS.md +0 -0
  64. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/CONTRIBUTING.md +0 -0
  65. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/LICENSE +0 -0
  66. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/Makefile +0 -0
  67. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/README.md +0 -0
  68. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/__init__.py +0 -0
  69. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/assistant_config/agentseek.json +0 -0
  70. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/assistant_config/graph.py +0 -0
  71. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/auth/api_key_auth.py +0 -0
  72. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/auth/bearer_token_auth.py +0 -0
  73. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/auth/custom_backend.py +0 -0
  74. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/auth/jwt.md +0 -0
  75. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/auth/jwt_auth.py +0 -0
  76. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/custom_routes/app.py +0 -0
  77. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/docker_ci_auth/auth_backend.py +0 -0
  78. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/docker_ci_auth/manifest.json +0 -0
  79. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/external_graph/__init__.py +0 -0
  80. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/external_graph/graph.py +0 -0
  81. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/external_graph/manifest.json +0 -0
  82. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/external_graph/run.py +0 -0
  83. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/graphs/__init__.py +0 -0
  84. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/graphs/react_agent/__init__.py +0 -0
  85. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/graphs/react_agent/graph.py +0 -0
  86. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/graphs/store_memory/graph.py +0 -0
  87. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/graphs/stress_test/__init__.py +0 -0
  88. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/graphs/stress_test/graph.py +0 -0
  89. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/graphs/stress_tool_agent/__init__.py +0 -0
  90. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/graphs/stress_tool_agent/graph.py +0 -0
  91. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/graphs/subgraph_agent/__init__.py +0 -0
  92. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/graphs/subgraph_agent/graph.py +0 -0
  93. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/graphs/subgraph_hitl_agent/__init__.py +0 -0
  94. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/graphs/subgraph_hitl_agent/graph.py +0 -0
  95. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/live_provider_graphs/__init__.py +0 -0
  96. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/live_provider_graphs/graph.py +0 -0
  97. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/live_provider_graphs/manifest.json +0 -0
  98. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/minimal_agentseek/agentseek.json +0 -0
  99. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/minimal_agentseek/graph.py +0 -0
  100. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/run_sample_graphs.py +0 -0
  101. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/examples/sample_graphs_manifest.json +0 -0
  102. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/build_live_provider_matrix.py +0 -0
  103. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/seekdb_checkpoint_smoke.py +0 -0
  104. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/seekdb_embed_launcher.py +0 -0
  105. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/test-checkpoints.sh +0 -0
  106. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/test-cli-dev-samples.sh +0 -0
  107. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/test-cli-docker.sh +0 -0
  108. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/test-live-provider.sh +0 -0
  109. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/test-redis-runtime.sh +0 -0
  110. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/test-seekdb.sh +0 -0
  111. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/test_cli_config_autodiscovery.py +0 -0
  112. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/test_cli_dev_studio_smoke.py +0 -0
  113. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/test_cli_embed_serve_smoke.py +0 -0
  114. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/test_cli_serve_smoke.py +0 -0
  115. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/scripts/verify_docker_api.py +0 -0
  116. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/api/__init__.py +0 -0
  117. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/api/assistants.py +0 -0
  118. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/api/stateless_runs.py +0 -0
  119. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/api/store.py +0 -0
  120. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/api/streaming.py +0 -0
  121. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/cli.py +0 -0
  122. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/__init__.py +0 -0
  123. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/a2a_config.py +0 -0
  124. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/app_loader.py +0 -0
  125. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/auth_deps.py +0 -0
  126. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/auth_middleware.py +0 -0
  127. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/config_file.py +0 -0
  128. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/content_type_fix.py +0 -0
  129. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/cors_config.py +0 -0
  130. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/http_config.py +0 -0
  131. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/mcp_config.py +0 -0
  132. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/runtime_store.py +0 -0
  133. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/core/store_config.py +0 -0
  134. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/main.py +0 -0
  135. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/mcp_server.py +0 -0
  136. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/models/__init__.py +0 -0
  137. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/models/auth.py +0 -0
  138. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/models/protocol.py +0 -0
  139. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/scheduler.py +0 -0
  140. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/__init__.py +0 -0
  141. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/cron_rrule.py +0 -0
  142. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/cron_webhooks.py +0 -0
  143. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/default_assistants.py +0 -0
  144. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/executor.py +0 -0
  145. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/langgraph_service.py +0 -0
  146. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/redis_queue.py +0 -0
  147. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/run_preparation.py +0 -0
  148. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/run_state.py +0 -0
  149. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/sample_graphs.py +0 -0
  150. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/services/thread_checkpoint_store.py +0 -0
  151. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/src/agentseek_api/worker.py +0 -0
  152. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/__init__.py +0 -0
  153. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/conftest.py +0 -0
  154. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/__init__.py +0 -0
  155. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/e2e_inprocess_flow.py +0 -0
  156. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/e2e_live_http_flow.py +0 -0
  157. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/e2e_live_http_multi_graph.py +0 -0
  158. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/e2e_live_http_resume_flow.py +0 -0
  159. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/fixtures/langgraph.store-e2e.json +0 -0
  160. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/live_provider_helpers.py +0 -0
  161. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/test_a2a_live.py +0 -0
  162. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/test_langsmith_compat_live.py +0 -0
  163. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/test_live_provider_api.py +0 -0
  164. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/test_live_provider_mcp.py +0 -0
  165. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/test_mcp_live.py +0 -0
  166. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/e2e/test_real_api_flow.py +0 -0
  167. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_a2a_endpoint.py +0 -0
  168. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_agent_card.py +0 -0
  169. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_agents_aliases.py +0 -0
  170. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_assistants_compat.py +0 -0
  171. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_assistants_crud.py +0 -0
  172. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_assistants_ordering.py +0 -0
  173. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_auth_route_enforcement.py +0 -0
  174. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_auth_scope_matrix.py +0 -0
  175. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_core_api_flow.py +0 -0
  176. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_langsmith_compat_extra.py +0 -0
  177. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_live_provider_streaming.py +0 -0
  178. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_mcp_endpoint.py +0 -0
  179. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_metadata_db_config.py +0 -0
  180. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_protocol_v2_streaming.py +0 -0
  181. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_run_cancellation_async.py +0 -0
  182. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_runs_crud.py +0 -0
  183. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_runs_ordering.py +0 -0
  184. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_runs_resume.py +0 -0
  185. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_runs_streaming.py +0 -0
  186. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_runs_streaming_errors.py +0 -0
  187. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_runs_validation.py +0 -0
  188. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_runs_wait_and_get.py +0 -0
  189. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_runs_wait_timeout.py +0 -0
  190. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_stateless_runs_crud.py +0 -0
  191. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_store_api.py +0 -0
  192. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_system_endpoints.py +0 -0
  193. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_threads_compat.py +0 -0
  194. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_threads_crud.py +0 -0
  195. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/integration/test_threads_ordering.py +0 -0
  196. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_a2a_config.py +0 -0
  197. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_app_loader.py +0 -0
  198. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_assistant_routes_unit.py +0 -0
  199. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_auth.py +0 -0
  200. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_auth_deps.py +0 -0
  201. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_checkpoint_guards.py +0 -0
  202. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_cron_rrule.py +0 -0
  203. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_cron_webhooks.py +0 -0
  204. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_custom_routers.py +0 -0
  205. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_examples.py +0 -0
  206. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_executor.py +0 -0
  207. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_graph_manifest.py +0 -0
  208. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_http_config.py +0 -0
  209. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_langgraph_service.py +0 -0
  210. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_live_provider_e2e_config.py +0 -0
  211. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_live_provider_graphs.py +0 -0
  212. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_live_provider_matrix.py +0 -0
  213. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_mcp_config.py +0 -0
  214. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_mcp_server.py +0 -0
  215. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_oceanbase_checkpointer.py +0 -0
  216. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_openapi_auth_config.py +0 -0
  217. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_redis_queue.py +0 -0
  218. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_run_preparation.py +0 -0
  219. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_run_state.py +0 -0
  220. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_runtime_store.py +0 -0
  221. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_sample_graphs.py +0 -0
  222. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_scheduler_process.py +0 -0
  223. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_seekdb_checkpoint_smoke.py +0 -0
  224. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_seekdb_embed_launcher.py +0 -0
  225. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_store_config.py +0 -0
  226. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_thread_checkpoint_store.py +0 -0
  227. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_thread_service.py +0 -0
  228. {agentseek_api-0.1.0 → agentseek_api-0.2.0}/tests/unit/test_worker.py +0 -0
@@ -7,6 +7,10 @@ AGENTSEEK_GRAPHS=
7
7
  # If not set, all requests pass through as default_user (noop).
8
8
  AUTH_MODULE_PATH=
9
9
 
10
+ # Redis worker stream replay buffer.
11
+ # REDIS_STREAM_MAXLEN=10000
12
+ # REDIS_STREAM_TTL_SECONDS=3600
13
+
10
14
  # Embedded SeekDB — set to true for a zero-config local backend.
11
15
  # Data stored in SEEKDB_EMBED_DIR (default: ~/.agentseek/seekdb_data).
12
16
  # Requires: uv sync --dev --extra embedded
@@ -37,6 +37,9 @@ jobs:
37
37
  cache-python: true
38
38
  python-version: "3.12"
39
39
 
40
+ - name: Check lockfile
41
+ run: uv lock --check
42
+
40
43
  - name: Sync dependencies
41
44
  run: uv sync --dev
42
45
 
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ Notable changes to AgentSeek API are documented in this file.
4
+
5
+ ## 0.2.0 - 2026-07-12
6
+
7
+ ### Highlights
8
+
9
+ - Expanded the Crons API to align its request, response, filtering, sorting,
10
+ selection, and lifecycle behavior with the LangGraph Platform contract.
11
+ - Moved Redis-worker run and protocol event persistence from SQL to bounded
12
+ Redis Streams, removing metadata-database writes from the streaming hot path.
13
+ - Preserved UTF-8 output across SSE, wait, and A2A JSON responses while keeping
14
+ active thread event retention bounded without dropping live events.
15
+ - Upgraded checkpoint integration to `langchain-oceanbase` 0.6.0.
16
+
17
+ ### Upgrade notes
18
+
19
+ - Drain active Redis-worker runs before upgrading. Stream-event rows written to
20
+ SQL by earlier versions are not imported into Redis Streams.
21
+ - Redis stream replay is bounded by `REDIS_STREAM_MAXLEN` and
22
+ `REDIS_STREAM_TTL_SECONDS`; review these settings for long-running workloads.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentseek-api
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: AgentSeek API core runtime with OceanBase checkpoints.
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -12,7 +12,7 @@ Requires-Dist: fastapi>=0.110.1
12
12
  Requires-Dist: greenlet>=3.1.0
13
13
  Requires-Dist: langchain-anthropic>=1.0.0
14
14
  Requires-Dist: langchain-core>=1.0.0
15
- Requires-Dist: langchain-oceanbase==0.5.2
15
+ Requires-Dist: langchain-oceanbase==0.6.0
16
16
  Requires-Dist: langchain-openai>=1.0.0
17
17
  Requires-Dist: langchain>=0.3.9
18
18
  Requires-Dist: langgraph-sdk>=0.3.5
@@ -26,13 +26,15 @@ Requires-Dist: scalar-fastapi>=1.0.3
26
26
  Requires-Dist: sqlalchemy>=2.0.0
27
27
  Requires-Dist: uvicorn>=0.30.0
28
28
  Provides-Extra: embedded
29
- Requires-Dist: langchain-oceanbase[pyseekdb]==0.5.2; extra == 'embedded'
29
+ Requires-Dist: langchain-oceanbase[pyseekdb]==0.6.0; extra == 'embedded'
30
30
  Description-Content-Type: text/markdown
31
31
 
32
32
  # AgentSeek API
33
33
 
34
34
  **English** | [中文](README.zh-CN.md)
35
35
 
36
+ [![PyPI version](https://img.shields.io/pypi/v/agentseek-api.svg)](https://pypi.org/project/agentseek-api/) [![Python >=3.12](https://img.shields.io/badge/python-%3E%3D3.12-blue.svg)](https://pypi.org/project/agentseek-api/) [![CI](https://github.com/ob-labs/agentseek-api/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/ob-labs/agentseek-api/actions/workflows/ci.yml?query=branch%3Amain)
37
+
36
38
  > [!WARNING]
37
39
  > This project is under active development and is **not production-ready**.
38
40
  > Pull requests for bug fixes and enhancements are warmly welcomed!
@@ -214,7 +216,7 @@ When the server starts it prints the banner and local URLs:
214
216
  ╠═╣│ ┬├┤ │││ │ ╚═╗├┤ ├┤ ├┴┐
215
217
  ╩ ╩└─┘└─┘┘└┘ ┴ ╚═╝└─┘└─┘┴ ┴
216
218
 
217
- AgentSeek v0.1.0
219
+ AgentSeek v0.2.0
218
220
 
219
221
  - 🚀 API: http://localhost:2024
220
222
  - 📚 Docs: http://localhost:2024/docs
@@ -653,6 +655,8 @@ parent api build --config ./langgraph.json -t my-api:dev
653
655
  - `REDIS_RUN_PROCESSING_KEY=agentseek:runs:processing`
654
656
  - `REDIS_WORKER_LOCK_KEY=agentseek:worker:active`
655
657
  - `REDIS_WORKER_LOCK_TTL_SECONDS=30`
658
+ - `REDIS_STREAM_MAXLEN=10000`
659
+ - `REDIS_STREAM_TTL_SECONDS=3600`
656
660
  - `METADATA_DB_BACKEND=auto` normalizes drivers:
657
661
  - PostgreSQL: `postgresql+asyncpg://...`
658
662
  - OceanBase / MySQL: `mysql+aiomysql://...`
@@ -665,8 +669,12 @@ parent api build --config ./langgraph.json -t my-api:dev
665
669
 
666
670
  ### Durable execution
667
671
 
668
- - Redis mode persists run stream events and protocol stream events into the
669
- metadata database so stream replay does not depend on API-process memory.
672
+ - Redis mode stores run stream events and protocol stream events in bounded
673
+ Redis Streams, so replay does not depend on API-process memory and streaming
674
+ writes do not put the metadata database on the hot path.
675
+ - When upgrading from a version that stored Redis-worker stream events in the
676
+ metadata database, drain active runs first. Existing SQL stream-event rows
677
+ are not imported into Redis.
670
678
  - Interrupted runs can be resumed after worker restart as long as Redis and the
671
679
  metadata database stay available.
672
680
  - The worker owns a renewable Redis lease and exits if that lease is lost,
@@ -2,6 +2,8 @@
2
2
 
3
3
  **English** | [中文](README.zh-CN.md)
4
4
 
5
+ [![PyPI version](https://img.shields.io/pypi/v/agentseek-api.svg)](https://pypi.org/project/agentseek-api/) [![Python >=3.12](https://img.shields.io/badge/python-%3E%3D3.12-blue.svg)](https://pypi.org/project/agentseek-api/) [![CI](https://github.com/ob-labs/agentseek-api/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/ob-labs/agentseek-api/actions/workflows/ci.yml?query=branch%3Amain)
6
+
5
7
  > [!WARNING]
6
8
  > This project is under active development and is **not production-ready**.
7
9
  > Pull requests for bug fixes and enhancements are warmly welcomed!
@@ -183,7 +185,7 @@ When the server starts it prints the banner and local URLs:
183
185
  ╠═╣│ ┬├┤ │││ │ ╚═╗├┤ ├┤ ├┴┐
184
186
  ╩ ╩└─┘└─┘┘└┘ ┴ ╚═╝└─┘└─┘┴ ┴
185
187
 
186
- AgentSeek v0.1.0
188
+ AgentSeek v0.2.0
187
189
 
188
190
  - 🚀 API: http://localhost:2024
189
191
  - 📚 Docs: http://localhost:2024/docs
@@ -622,6 +624,8 @@ parent api build --config ./langgraph.json -t my-api:dev
622
624
  - `REDIS_RUN_PROCESSING_KEY=agentseek:runs:processing`
623
625
  - `REDIS_WORKER_LOCK_KEY=agentseek:worker:active`
624
626
  - `REDIS_WORKER_LOCK_TTL_SECONDS=30`
627
+ - `REDIS_STREAM_MAXLEN=10000`
628
+ - `REDIS_STREAM_TTL_SECONDS=3600`
625
629
  - `METADATA_DB_BACKEND=auto` normalizes drivers:
626
630
  - PostgreSQL: `postgresql+asyncpg://...`
627
631
  - OceanBase / MySQL: `mysql+aiomysql://...`
@@ -634,8 +638,12 @@ parent api build --config ./langgraph.json -t my-api:dev
634
638
 
635
639
  ### Durable execution
636
640
 
637
- - Redis mode persists run stream events and protocol stream events into the
638
- metadata database so stream replay does not depend on API-process memory.
641
+ - Redis mode stores run stream events and protocol stream events in bounded
642
+ Redis Streams, so replay does not depend on API-process memory and streaming
643
+ writes do not put the metadata database on the hot path.
644
+ - When upgrading from a version that stored Redis-worker stream events in the
645
+ metadata database, drain active runs first. Existing SQL stream-event rows
646
+ are not imported into Redis.
639
647
  - Interrupted runs can be resumed after worker restart as long as Redis and the
640
648
  metadata database stay available.
641
649
  - The worker owns a renewable Redis lease and exits if that lease is lost,
@@ -2,6 +2,8 @@
2
2
 
3
3
  [English](README.md) | **中文**
4
4
 
5
+ [![PyPI version](https://img.shields.io/pypi/v/agentseek-api.svg)](https://pypi.org/project/agentseek-api/) [![Python >=3.12](https://img.shields.io/badge/python-%3E%3D3.12-blue.svg)](https://pypi.org/project/agentseek-api/) [![CI](https://github.com/ob-labs/agentseek-api/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/ob-labs/agentseek-api/actions/workflows/ci.yml?query=branch%3Amain)
6
+
5
7
  > [!WARNING]
6
8
  > 本项目正在积极开发中,**尚未达到生产可用状态**。
7
9
  > 欢迎提交 Pull Request 来修复 Bug 或贡献增强功能!
@@ -179,7 +181,7 @@ uv run agentseek-api dev --config ./langgraph.json
179
181
  ╠═╣│ ┬├┤ │││ │ ╚═╗├┤ ├┤ ├┴┐
180
182
  ╩ ╩└─┘└─┘┘└┘ ┴ ╚═╝└─┘└─┘┴ ┴
181
183
 
182
- AgentSeek v0.1.0
184
+ AgentSeek v0.2.0
183
185
 
184
186
  - 🚀 API: http://localhost:2024
185
187
  - 📚 Docs: http://localhost:2024/docs
@@ -616,6 +618,8 @@ parent api build --config ./langgraph.json -t my-api:dev
616
618
  - `REDIS_RUN_PROCESSING_KEY=agentseek:runs:processing`
617
619
  - `REDIS_WORKER_LOCK_KEY=agentseek:worker:active`
618
620
  - `REDIS_WORKER_LOCK_TTL_SECONDS=30`
621
+ - `REDIS_STREAM_MAXLEN=10000`
622
+ - `REDIS_STREAM_TTL_SECONDS=3600`
619
623
  - `METADATA_DB_BACKEND=auto` 会对驱动进行归一化:
620
624
  - PostgreSQL:`postgresql+asyncpg://...`
621
625
  - OceanBase / MySQL:`mysql+aiomysql://...`
@@ -628,8 +632,11 @@ parent api build --config ./langgraph.json -t my-api:dev
628
632
 
629
633
  ### 持久化执行
630
634
 
631
- - Redis 模式会把 run 流事件与 protocol 流事件持久化到元数据数据库,
632
- 这样流回放就不再依赖 API 进程的内存。
635
+ - Redis 模式会把 run 流事件与 protocol 流事件存入有界 Redis Stream,
636
+ 这样流回放既不依赖 API 进程内存,也不会让流式写入占用元数据数据库
637
+ 的关键路径。
638
+ - 从使用元数据数据库保存 Redis worker 流事件的旧版本升级时,请先等待
639
+ 活跃 run 执行完成;已有的 SQL 流事件不会导入 Redis。
633
640
  - 只要 Redis 与元数据数据库仍然可用,被中断的 run 可以在 worker 重启
634
641
  后恢复。
635
642
  - Worker 持有一个可续期的 Redis lease,在 lease 丢失时会主动退出,从而
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentseek-api"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "AgentSeek API core runtime with OceanBase checkpoints."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -20,7 +20,7 @@ dependencies = [
20
20
  "langchain-core>=1.0.0",
21
21
  "langchain-openai>=1.0.0",
22
22
  "langchain-anthropic>=1.0.0",
23
- "langchain-oceanbase==0.5.2",
23
+ "langchain-oceanbase==0.6.0",
24
24
  "cryptography>=45.0.0",
25
25
  "pymysql>=1.1.0",
26
26
  "langchain>=0.3.9",
@@ -30,7 +30,7 @@ dependencies = [
30
30
 
31
31
  [project.optional-dependencies]
32
32
  embedded = [
33
- "langchain-oceanbase[pyseekdb]==0.5.2",
33
+ "langchain-oceanbase[pyseekdb]==0.6.0",
34
34
  ]
35
35
 
36
36
  [project.scripts]
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -282,7 +282,7 @@ def _message_content(message: BaseMessage) -> str:
282
282
  content = getattr(message, "content", "")
283
283
  if isinstance(content, str):
284
284
  return content
285
- return json.dumps(content, default=str)
285
+ return json.dumps(content, ensure_ascii=False, default=str)
286
286
 
287
287
 
288
288
  def _extract_chunk_messages(chunk: Any) -> list[BaseMessage]:
@@ -366,7 +366,7 @@ def _extract_output_text(extracted: Any) -> str:
366
366
  message_texts = [_message_content(message) for message in messages if isinstance(message, BaseMessage)]
367
367
  if message_texts:
368
368
  return message_texts[-1]
369
- return json.dumps(extracted, default=str)
369
+ return json.dumps(extracted, ensure_ascii=False, default=str)
370
370
 
371
371
 
372
372
  async def _invoke_a2a_graph(
@@ -558,7 +558,7 @@ def _canonical_a2a_method(method: str) -> tuple[str, bool]:
558
558
 
559
559
 
560
560
  def _sse_jsonrpc_event(*, request_id: Any, result: dict[str, Any]) -> str:
561
- return f"event: message\ndata: {json.dumps(_jsonrpc_result(request_id, result))}\n\n"
561
+ return f"event: message\ndata: {json.dumps(_jsonrpc_result(request_id, result), ensure_ascii=False)}\n\n"
562
562
 
563
563
 
564
564
  async def handle_a2a_request(
@@ -1,9 +1,9 @@
1
1
  from sqlalchemy import select
2
2
 
3
3
  from fastapi import APIRouter, Depends, HTTPException
4
- from fastapi.responses import Response
4
+ from fastapi.responses import JSONResponse, Response
5
5
 
6
- from agentseek_api.core.auth_deps import authorize, apply_metadata_filters, get_current_user
6
+ from agentseek_api.core.auth_deps import authorize, get_current_user
7
7
  from agentseek_api.core.database import db_manager
8
8
  from agentseek_api.core.orm import Assistant, Thread
9
9
  from agentseek_api.models.api import (
@@ -14,6 +14,7 @@ from agentseek_api.models.api import (
14
14
  CronRead,
15
15
  CronSearchRequest,
16
16
  CronSearchResponse,
17
+ ThreadCronCreate,
17
18
  )
18
19
  from agentseek_api.models.auth import User
19
20
  from agentseek_api.services import cron_service
@@ -36,7 +37,7 @@ async def _create_cron(
36
37
  *,
37
38
  assistant_id: str,
38
39
  thread_id: str | None,
39
- payload: CronCreate,
40
+ payload: CronCreate | ThreadCronCreate,
40
41
  user: User,
41
42
  ) -> CronRead:
42
43
  try:
@@ -45,7 +46,7 @@ async def _create_cron(
45
46
  raise HTTPException(status_code=400, detail=str(exc)) from exc
46
47
 
47
48
 
48
- @router.post("/runs/crons", response_model=CronRead)
49
+ @router.post("/runs/crons", response_model=CronRead, response_model_exclude_none=True)
49
50
  async def create_stateless_cron(payload: CronCreate, user: User = Depends(get_current_user)) -> CronRead:
50
51
  value: dict = {
51
52
  "metadata": dict(payload.metadata) if payload.metadata else {},
@@ -57,8 +58,8 @@ async def create_stateless_cron(payload: CronCreate, user: User = Depends(get_cu
57
58
  return await _create_cron(assistant_id=resolved_id, thread_id=None, payload=payload, user=user)
58
59
 
59
60
 
60
- @router.post("/threads/{thread_id}/runs/crons", response_model=CronRead)
61
- async def create_thread_cron(thread_id: str, payload: CronCreate, user: User = Depends(get_current_user)) -> CronRead:
61
+ @router.post("/threads/{thread_id}/runs/crons", response_model=CronRead, response_model_exclude_none=True)
62
+ async def create_thread_cron(thread_id: str, payload: ThreadCronCreate, user: User = Depends(get_current_user)) -> CronRead:
62
63
  value: dict = {
63
64
  "metadata": dict(payload.metadata) if payload.metadata else {},
64
65
  "assistant_id": payload.assistant_id,
@@ -75,10 +76,18 @@ async def create_thread_cron(thread_id: str, payload: CronCreate, user: User = D
75
76
  return await _create_cron(assistant_id=resolved_id, thread_id=thread_id, payload=payload, user=user)
76
77
 
77
78
 
78
- @router.post("/runs/crons/search", response_model=CronSearchResponse)
79
- async def search_crons(payload: CronSearchRequest, user: User = Depends(get_current_user)) -> CronSearchResponse:
79
+ @router.post("/runs/crons/search", response_model=CronSearchResponse, response_model_exclude_none=True)
80
+ async def search_crons(payload: CronSearchRequest, user: User = Depends(get_current_user)) -> CronSearchResponse | JSONResponse:
80
81
  filters = await authorize(user, "crons", "search", {})
81
- return await cron_service.search_crons(payload=payload, user=user, filters=filters)
82
+ result = await cron_service.search_crons(payload=payload, user=user, filters=filters)
83
+ if payload.select is not None:
84
+ fields = set(payload.select)
85
+ # exclude_none keeps the select path's null handling consistent with the
86
+ # default path (response_model_exclude_none) and the single-cron routes.
87
+ return JSONResponse(
88
+ content={"items": [item.model_dump(mode="json", include=fields, exclude_none=True) for item in result.items]}
89
+ )
90
+ return result
82
91
 
83
92
 
84
93
  @router.post("/runs/crons/count", response_model=CronCountResponse)
@@ -87,13 +96,13 @@ async def count_crons(payload: CronCountRequest, user: User = Depends(get_curren
87
96
  return await cron_service.count_crons(payload=payload, user=user, filters=filters)
88
97
 
89
98
 
90
- @router.get("/runs/crons/{cron_id}", response_model=CronRead)
99
+ @router.get("/runs/crons/{cron_id}", response_model=CronRead, response_model_exclude_none=True)
91
100
  async def get_cron(cron_id: str, user: User = Depends(get_current_user)) -> CronRead:
92
101
  filters = await authorize(user, "crons", "read", {"cron_id": cron_id})
93
102
  return await cron_service.get_cron(cron_id=cron_id, user=user, filters=filters)
94
103
 
95
104
 
96
- @router.patch("/runs/crons/{cron_id}", response_model=CronRead)
105
+ @router.patch("/runs/crons/{cron_id}", response_model=CronRead, response_model_exclude_none=True)
97
106
  async def patch_cron(cron_id: str, payload: CronPatch, user: User = Depends(get_current_user)) -> CronRead:
98
107
  filters = await authorize(user, "crons", "update", {"cron_id": cron_id})
99
108
  try:
@@ -31,6 +31,10 @@ from agentseek_api.services.stream_persistence import (
31
31
  parse_last_event_id,
32
32
  )
33
33
  from agentseek_api.services.sse import iter_with_sse_keepalives, safe_json_dumps, sse_keepalive_comment
34
+ from agentseek_api.services.stream_modes import (
35
+ SUPPORTED_RUN_STREAM_MODES,
36
+ normalize_stream_modes as _normalize_stream_modes_shared,
37
+ )
34
38
  from agentseek_api.services.thread_protocol import thread_protocol_broker
35
39
  from agentseek_api.settings import settings
36
40
 
@@ -39,8 +43,6 @@ router = APIRouter(prefix="/threads/{thread_id}/runs", tags=["Thread Runs"])
39
43
  TERMINAL_RUN_STATUSES = ("success", "error", "interrupted")
40
44
  REDIS_STREAM_POLL_INTERVAL_SECONDS = 0.05
41
45
  REDIS_STREAM_TERMINAL_IDLE_POLLS = 2
42
- SUPPORTED_RUN_STREAM_MODES = {"values", "updates", "messages", "messages-tuple", "debug", "events", "tasks", "checkpoints", "custom"}
43
- RUN_STREAM_MODE_ALIASES: dict[str, str] = {}
44
46
  RUN_CHECKPOINT_ID_METADATA_KEY = "__agentseek_checkpoint_id"
45
47
 
46
48
 
@@ -107,35 +109,10 @@ def _uses_redis_executor() -> bool:
107
109
 
108
110
 
109
111
  def _normalize_stream_modes(stream_mode: str | list[str] | None) -> list[str]:
110
- if stream_mode is None:
111
- return ["values"]
112
-
113
- raw_modes = [stream_mode] if isinstance(stream_mode, str) else list(stream_mode)
114
- modes: list[str] = []
115
- invalid_modes: list[str] = []
116
-
117
- if not raw_modes:
118
- invalid_modes.append("<empty>")
119
-
120
- for mode in raw_modes:
121
- raw_mode = str(mode).strip()
122
- if not raw_mode:
123
- invalid_modes.append("<empty>")
124
- continue
125
- normalized = RUN_STREAM_MODE_ALIASES.get(raw_mode, raw_mode)
126
- if normalized not in modes:
127
- modes.append(normalized)
128
-
129
- unsupported = invalid_modes + [mode for mode in modes if mode not in SUPPORTED_RUN_STREAM_MODES]
130
- if unsupported:
131
- raise HTTPException(
132
- status_code=422,
133
- detail=(
134
- "Unsupported stream_mode value(s): "
135
- f"{', '.join(unsupported)}. Supported values: {', '.join(sorted(SUPPORTED_RUN_STREAM_MODES))}."
136
- ),
137
- )
138
- return modes
112
+ try:
113
+ return _normalize_stream_modes_shared(stream_mode)
114
+ except ValueError as exc:
115
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
139
116
 
140
117
 
141
118
  def _parse_stream_mode_query_param(stream_mode: list[str] | None) -> list[str] | None:
@@ -275,7 +252,7 @@ def _wait_json_stream_response(
275
252
  raise
276
253
  yield b"\n"
277
254
  payload = await _wait_response_payload(current_run, user=user)
278
- yield json.dumps(jsonable_encoder(payload), separators=(",", ":")).encode()
255
+ yield safe_json_dumps(jsonable_encoder(payload), separators=(",", ":")).encode("utf-8")
279
256
  finally:
280
257
  if cancel_on_disconnect:
281
258
  try:
@@ -48,7 +48,11 @@ from agentseek_api.services.thread_checkpoint_store import (
48
48
  snapshot_to_payload,
49
49
  )
50
50
  from agentseek_api.services.thread_protocol import thread_protocol_broker
51
- from agentseek_api.services.thread_service import create_thread_for_user, to_read_model
51
+ from agentseek_api.services.thread_service import (
52
+ create_thread_for_user,
53
+ delete_threads_cascade,
54
+ to_read_model,
55
+ )
52
56
  from agentseek_api.settings import settings
53
57
 
54
58
  router = APIRouter(prefix="/threads", tags=["Threads"])
@@ -59,18 +63,6 @@ REDIS_STREAM_TERMINAL_IDLE_POLLS = 2
59
63
  THREAD_STREAM_CHANNELS = ["input", "lifecycle", "messages", "tools", "values"]
60
64
 
61
65
 
62
- async def _best_effort_checkpointer_call(method_name: str, *args: object, **kwargs: object) -> None:
63
- method = getattr(db_manager.get_langgraph_checkpointer(), method_name, None)
64
- if method is None:
65
- return
66
- try:
67
- result = method(*args, **kwargs)
68
- if hasattr(result, "__await__"):
69
- await result
70
- except NotImplementedError:
71
- return
72
-
73
-
74
66
  logger = logging.getLogger(__name__)
75
67
 
76
68
  @functools.lru_cache(maxsize=64)
@@ -461,23 +453,11 @@ async def delete_thread(thread_id: str, user: User = Depends(get_current_user))
461
453
 
462
454
  session_factory = db_manager.get_session_factory()
463
455
  async with session_factory() as session:
464
- stmt = select(Thread).where(Thread.thread_id == thread_id)
456
+ stmt = select(Thread.thread_id).where(Thread.thread_id == thread_id)
465
457
  stmt = apply_metadata_filters(stmt, Thread, filters)
466
- row = await session.scalar(stmt)
467
- if row is None:
458
+ if await session.scalar(stmt) is None:
468
459
  raise HTTPException(status_code=404, detail="Thread not found")
469
- run_ids = (
470
- await session.scalars(select(Run.run_id).where(Run.thread_id == thread_id))
471
- ).all()
472
- await session.execute(delete(Run).where(Run.thread_id == thread_id))
473
- await session.delete(row)
474
- await session.commit()
475
- await _best_effort_checkpointer_call("adelete_thread", thread_id)
476
- if run_ids:
477
- await _best_effort_checkpointer_call("adelete_for_runs", list(run_ids))
478
- await delete_run_stream_events(list(run_ids))
479
- thread_protocol_broker.delete_thread(thread_id)
480
- await delete_thread_stream_events(thread_id)
460
+ await delete_threads_cascade([thread_id])
481
461
  return Response(status_code=204)
482
462
 
483
463
 
@@ -172,6 +172,7 @@ class DatabaseManager:
172
172
  self.session_factory = async_sessionmaker(self.engine, expire_on_commit=False)
173
173
  async with self.engine.begin() as conn:
174
174
  await conn.run_sync(Base.metadata.create_all)
175
+ await conn.run_sync(self._apply_additive_migrations)
175
176
  embed_conn_args = {"path": embed_dir, "db_name": settings.OCEANBASE_DB_NAME}
176
177
  self._checkpointer = OceanBaseCheckpointSaver(
177
178
  connection_args=embed_conn_args
@@ -195,6 +196,7 @@ class DatabaseManager:
195
196
  self.session_factory = async_sessionmaker(self.engine, expire_on_commit=False)
196
197
  async with self.engine.begin() as conn:
197
198
  await conn.run_sync(Base.metadata.create_all)
199
+ await conn.run_sync(self._apply_additive_migrations)
198
200
  conn_args = _resolve_connection_args()
199
201
  self._checkpointer = OceanBaseCheckpointSaver(
200
202
  connection_args=conn_args
@@ -220,6 +222,51 @@ class DatabaseManager:
220
222
  await self._setup_langgraph_checkpointer_once()
221
223
  await self._setup_store_once()
222
224
 
225
+ @staticmethod
226
+ def _apply_additive_migrations(connection) -> None:
227
+ """Add columns introduced after a table was first created.
228
+
229
+ ``Base.metadata.create_all`` only creates *missing* tables; it never
230
+ alters an existing one. Deployments whose tables predate a new ORM
231
+ column would therefore be missing it, breaking queries that reference
232
+ it. This inspects each mapped table and issues ``ALTER TABLE ADD
233
+ COLUMN`` for any column present in the model but absent in the DB.
234
+ Idempotent and safe to run on every startup.
235
+
236
+ Invariant for new columns: they must be either nullable or carry a
237
+ ``server_default``. ``ADD COLUMN ... NOT NULL`` without a server-side
238
+ default fails on a populated table (there is no value for existing
239
+ rows). Such a column cannot be added automatically here — it requires a
240
+ deliberate data migration — so we skip it with a loud error rather than
241
+ crash startup or emit failing DDL.
242
+ """
243
+ from sqlalchemy import inspect
244
+ from sqlalchemy.schema import CreateColumn
245
+
246
+ inspector = inspect(connection)
247
+ existing_tables = set(inspector.get_table_names())
248
+ dialect = connection.dialect
249
+ preparer = dialect.identifier_preparer
250
+ for table in Base.metadata.sorted_tables:
251
+ if table.name not in existing_tables:
252
+ continue # create_all already built it with all columns
253
+ db_columns = {col["name"] for col in inspector.get_columns(table.name)}
254
+ for column in table.columns:
255
+ if column.name in db_columns:
256
+ continue
257
+ if not column.nullable and column.server_default is None:
258
+ logger.error(
259
+ "Cannot auto-add NOT NULL column %s.%s without a server_default; "
260
+ "a manual data migration is required. Skipping.",
261
+ table.name,
262
+ column.name,
263
+ )
264
+ continue
265
+ column_spec = CreateColumn(column).compile(dialect=dialect)
266
+ table_name = preparer.format_table(table)
267
+ logger.info("Adding missing column %s.%s", table.name, column.name)
268
+ connection.exec_driver_sql(f"ALTER TABLE {table_name} ADD COLUMN {column_spec}")
269
+
223
270
  async def _setup_checkpointer_once(self) -> None:
224
271
  if self._checkpointer_setup_done:
225
272
  return
@@ -262,6 +309,14 @@ class DatabaseManager:
262
309
  self._store_setup_done = True
263
310
 
264
311
  async def close(self) -> None:
312
+ langgraph_checkpointer = self._langgraph_checkpointer
313
+ self._langgraph_checkpointer = None
314
+ close_checkpointer = getattr(langgraph_checkpointer, "close", None)
315
+ if callable(close_checkpointer):
316
+ try:
317
+ await asyncio.to_thread(close_checkpointer)
318
+ except Exception: # noqa: BLE001
319
+ logger.warning("Failed to close LangGraph checkpointer", exc_info=True)
265
320
  if self.engine is not None:
266
321
  await self.engine.dispose()
267
322
  store_engine = getattr(getattr(self._store, "obvector", None), "engine", None)
@@ -271,7 +326,6 @@ class DatabaseManager:
271
326
  self.engine = None
272
327
  self.session_factory = None
273
328
  self._checkpointer = None
274
- self._langgraph_checkpointer = None
275
329
  self._store = None
276
330
  self._checkpointer_setup_done = False
277
331
  self._langgraph_checkpointer_setup_done = False
@@ -3,7 +3,6 @@ from datetime import UTC, datetime
3
3
  from typing import Any
4
4
 
5
5
  import pymysql
6
- from pymysql.connections import Connection
7
6
  from sqlalchemy import text
8
7
  from sqlalchemy.engine import Engine
9
8
 
@@ -3,7 +3,7 @@ from typing import Any
3
3
  from datetime import UTC, datetime
4
4
  from uuid import uuid4
5
5
 
6
- from sqlalchemy import JSON, Boolean, DateTime, Index, Integer, String, Text, UniqueConstraint
6
+ from sqlalchemy import JSON, Boolean, DateTime, Index, Integer, String, Text, UniqueConstraint, text
7
7
  from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
8
8
  from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
9
9
 
@@ -74,6 +74,10 @@ class CronJob(Base):
74
74
  metadata_json: Mapped[dict] = mapped_column("metadata", JSON, default=dict, nullable=False)
75
75
  kwargs_json: Mapped[dict] = mapped_column("kwargs", JSON, default=dict, nullable=False)
76
76
  webhook: Mapped[str | None] = mapped_column(Text, nullable=True)
77
+ end_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
78
+ on_run_completed: Mapped[str] = mapped_column(
79
+ String(16), nullable=False, default="delete", server_default=text("'delete'")
80
+ )
77
81
  max_webhook_attempts: Mapped[int] = mapped_column(nullable=False, default=3)
78
82
  next_run_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
79
83
  last_run_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)