marvin 3.2.0__tar.gz → 3.2.2__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 (310) hide show
  1. {marvin-3.2.0 → marvin-3.2.2}/PKG-INFO +1 -1
  2. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-run.mdx +2 -0
  3. marvin-3.2.2/examples/slackbot/src/slackbot/_internal/thread_status.py +94 -0
  4. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/api.py +81 -3
  5. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/core.py +3 -0
  6. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/slack.py +3 -0
  7. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/agents/actor.py +11 -1
  8. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/orchestrator.py +16 -1
  9. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/run.py +32 -7
  10. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tasks/task.py +5 -1
  11. marvin-3.2.2/tests/ai/tasks/test_tasks.py +34 -0
  12. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/tasks/test_tasks.py +7 -0
  13. marvin-3.2.2/tests/test_concurrent_execution.py +191 -0
  14. marvin-3.2.0/tests/ai/tasks/test_tasks.py +0 -14
  15. {marvin-3.2.0 → marvin-3.2.2}/.cursor/rules/docs.mdc +0 -0
  16. {marvin-3.2.0 → marvin-3.2.2}/.cursor/rules/general.mdc +0 -0
  17. {marvin-3.2.0 → marvin-3.2.2}/.cursor/rules/python.mdc +0 -0
  18. {marvin-3.2.0 → marvin-3.2.2}/.github/labeler.yml +0 -0
  19. {marvin-3.2.0 → marvin-3.2.2}/.github/release.yml +0 -0
  20. {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/deploy-cloudrun-revision-community.yaml +0 -0
  21. {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/deploy-cloudrun-revision-internal.yaml +0 -0
  22. {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/image-build-and-push-community.yaml +0 -0
  23. {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/image-build-and-push-internal.yaml +0 -0
  24. {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/labeler.yml +0 -0
  25. {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/publish.yml +0 -0
  26. {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/run-tests.yml +0 -0
  27. {marvin-3.2.0 → marvin-3.2.2}/.github/workflows/static-analysis.yml +0 -0
  28. {marvin-3.2.0 → marvin-3.2.2}/.gitignore +0 -0
  29. {marvin-3.2.0 → marvin-3.2.2}/.pre-commit-config.yaml +0 -0
  30. {marvin-3.2.0 → marvin-3.2.2}/CLAUDE.md +0 -0
  31. {marvin-3.2.0 → marvin-3.2.2}/LICENSE +0 -0
  32. {marvin-3.2.0 → marvin-3.2.2}/README.md +0 -0
  33. {marvin-3.2.0 → marvin-3.2.2}/alembic.ini +0 -0
  34. {marvin-3.2.0 → marvin-3.2.2}/docs/README.md +0 -0
  35. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-agents-actor.mdx +0 -0
  36. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-agents-agent.mdx +0 -0
  37. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-agents-names.mdx +0 -0
  38. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-agents-team.mdx +0 -0
  39. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-agents.mdx +0 -0
  40. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-beta.mdx +0 -0
  41. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-cli-config.mdx +0 -0
  42. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-cli-dev.mdx +0 -0
  43. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-cli-main.mdx +0 -0
  44. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-cli-migrations.mdx +0 -0
  45. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-cli.mdx +0 -0
  46. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-database.mdx +0 -0
  47. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-defaults.mdx +0 -0
  48. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-engine-end_turn.mdx +0 -0
  49. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-engine-events.mdx +0 -0
  50. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-engine-llm.mdx +0 -0
  51. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-engine-orchestrator.mdx +0 -0
  52. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-engine-streaming.mdx +0 -0
  53. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-engine.mdx +0 -0
  54. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-cast.mdx +0 -0
  55. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-classify.mdx +0 -0
  56. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-extract.mdx +0 -0
  57. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-fn.mdx +0 -0
  58. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-generate.mdx +0 -0
  59. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-plan.mdx +0 -0
  60. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-say.mdx +0 -0
  61. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns-summarize.mdx +0 -0
  62. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-fns.mdx +0 -0
  63. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-handlers-handlers.mdx +0 -0
  64. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-handlers-print_handler.mdx +0 -0
  65. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-handlers-queue_handler.mdx +0 -0
  66. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-handlers.mdx +0 -0
  67. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-instructions.mdx +0 -0
  68. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory-memory.mdx +0 -0
  69. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory-providers-chroma.mdx +0 -0
  70. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory-providers-lance.mdx +0 -0
  71. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory-providers-postgres.mdx +0 -0
  72. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory-providers-qdrant.mdx +0 -0
  73. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory-providers.mdx +0 -0
  74. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-memory.mdx +0 -0
  75. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-prompts.mdx +0 -0
  76. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-settings.mdx +0 -0
  77. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-tasks-task.mdx +0 -0
  78. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-tasks.mdx +0 -0
  79. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-thread.mdx +0 -0
  80. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-tools-thread.mdx +0 -0
  81. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-tools.mdx +0 -0
  82. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-asyncio.mdx +0 -0
  83. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-jinja.mdx +0 -0
  84. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-jsonschema.mdx +0 -0
  85. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-logging.mdx +0 -0
  86. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-models.mdx +0 -0
  87. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-testing.mdx +0 -0
  88. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-tools.mdx +0 -0
  89. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities-types.mdx +0 -0
  90. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin-utilities.mdx +0 -0
  91. {marvin-3.2.0 → marvin-3.2.2}/docs/api-reference/marvin.mdx +0 -0
  92. {marvin-3.2.0 → marvin-3.2.2}/docs/assets/brand/marvin_mascot.jpeg +0 -0
  93. {marvin-3.2.0 → marvin-3.2.2}/docs/assets/code.css +0 -0
  94. {marvin-3.2.0 → marvin-3.2.2}/docs/assets/img/quotes/ai_model.png +0 -0
  95. {marvin-3.2.0 → marvin-3.2.2}/docs/assets/img/quotes/dont_panic.png +0 -0
  96. {marvin-3.2.0 → marvin-3.2.2}/docs/assets/img/quotes/it_hates_me.png +0 -0
  97. {marvin-3.2.0 → marvin-3.2.2}/docs/assets/img/quotes/life_the_universe_and_ai.png +0 -0
  98. {marvin-3.2.0 → marvin-3.2.2}/docs/assets/img/quotes/pip_install_marvin_hero.png +0 -0
  99. {marvin-3.2.0 → marvin-3.2.2}/docs/assets/img/quotes/sort_of_thing_you_enjoy.png +0 -0
  100. {marvin-3.2.0 → marvin-3.2.2}/docs/community.mdx +0 -0
  101. {marvin-3.2.0 → marvin-3.2.2}/docs/concepts/agents.mdx +0 -0
  102. {marvin-3.2.0 → marvin-3.2.2}/docs/concepts/memory.mdx +0 -0
  103. {marvin-3.2.0 → marvin-3.2.2}/docs/concepts/tasks.mdx +0 -0
  104. {marvin-3.2.0 → marvin-3.2.2}/docs/concepts/teams.mdx +0 -0
  105. {marvin-3.2.0 → marvin-3.2.2}/docs/concepts/threads.mdx +0 -0
  106. {marvin-3.2.0 → marvin-3.2.2}/docs/concepts/tools-and-context.mdx +0 -0
  107. {marvin-3.2.0 → marvin-3.2.2}/docs/docs.json +0 -0
  108. {marvin-3.2.0 → marvin-3.2.2}/docs/functions/cast.mdx +0 -0
  109. {marvin-3.2.0 → marvin-3.2.2}/docs/functions/classify.mdx +0 -0
  110. {marvin-3.2.0 → marvin-3.2.2}/docs/functions/extract.mdx +0 -0
  111. {marvin-3.2.0 → marvin-3.2.2}/docs/functions/fn.mdx +0 -0
  112. {marvin-3.2.0 → marvin-3.2.2}/docs/functions/generate.mdx +0 -0
  113. {marvin-3.2.0 → marvin-3.2.2}/docs/functions/plan.mdx +0 -0
  114. {marvin-3.2.0 → marvin-3.2.2}/docs/functions/run.mdx +0 -0
  115. {marvin-3.2.0 → marvin-3.2.2}/docs/functions/say.mdx +0 -0
  116. {marvin-3.2.0 → marvin-3.2.2}/docs/functions/summarize.mdx +0 -0
  117. {marvin-3.2.0 → marvin-3.2.2}/docs/guides/building-a-chatbot.mdx +0 -0
  118. {marvin-3.2.0 → marvin-3.2.2}/docs/guides/cli.mdx +0 -0
  119. {marvin-3.2.0 → marvin-3.2.2}/docs/guides/configuration.mdx +0 -0
  120. {marvin-3.2.0 → marvin-3.2.2}/docs/guides/configure-llms.mdx +0 -0
  121. {marvin-3.2.0 → marvin-3.2.2}/docs/guides/database-migrations.mdx +0 -0
  122. {marvin-3.2.0 → marvin-3.2.2}/docs/guides/mcp.mdx +0 -0
  123. {marvin-3.2.0 → marvin-3.2.2}/docs/guides/migration-guide.mdx +0 -0
  124. {marvin-3.2.0 → marvin-3.2.2}/docs/guides/multi-step-workflow.mdx +0 -0
  125. {marvin-3.2.0 → marvin-3.2.2}/docs/installation.mdx +0 -0
  126. {marvin-3.2.0 → marvin-3.2.2}/docs/patterns/instructions.mdx +0 -0
  127. {marvin-3.2.0 → marvin-3.2.2}/docs/patterns/interactivity.mdx +0 -0
  128. {marvin-3.2.0 → marvin-3.2.2}/docs/patterns/memory.mdx +0 -0
  129. {marvin-3.2.0 → marvin-3.2.2}/docs/patterns/running-tasks.mdx +0 -0
  130. {marvin-3.2.0 → marvin-3.2.2}/docs/patterns/task-results.mdx +0 -0
  131. {marvin-3.2.0 → marvin-3.2.2}/docs/patterns/tools.mdx +0 -0
  132. {marvin-3.2.0 → marvin-3.2.2}/docs/quickstart.mdx +0 -0
  133. {marvin-3.2.0 → marvin-3.2.2}/docs/snippets/version-badge.mdx +0 -0
  134. {marvin-3.2.0 → marvin-3.2.2}/docs/style.css +0 -0
  135. {marvin-3.2.0 → marvin-3.2.2}/docs/welcome.mdx +0 -0
  136. {marvin-3.2.0 → marvin-3.2.2}/examples/agent_mcp.py +0 -0
  137. {marvin-3.2.0 → marvin-3.2.2}/examples/custom_handler.py +0 -0
  138. {marvin-3.2.0 → marvin-3.2.2}/examples/custom_prompt.py +0 -0
  139. {marvin-3.2.0 → marvin-3.2.2}/examples/deepseek_chat.py +0 -0
  140. {marvin-3.2.0 → marvin-3.2.2}/examples/english_to_schema.py +0 -0
  141. {marvin-3.2.0 → marvin-3.2.2}/examples/fastmcp/hello.py +0 -0
  142. {marvin-3.2.0 → marvin-3.2.2}/examples/hello_agent.py +0 -0
  143. {marvin-3.2.0 → marvin-3.2.2}/examples/hello_cast.py +0 -0
  144. {marvin-3.2.0 → marvin-3.2.2}/examples/hello_classify.py +0 -0
  145. {marvin-3.2.0 → marvin-3.2.2}/examples/hello_extract.py +0 -0
  146. {marvin-3.2.0 → marvin-3.2.2}/examples/hello_fn.py +0 -0
  147. {marvin-3.2.0 → marvin-3.2.2}/examples/hello_generate.py +0 -0
  148. {marvin-3.2.0 → marvin-3.2.2}/examples/hello_marvin.py +0 -0
  149. {marvin-3.2.0 → marvin-3.2.2}/examples/hello_run.py +0 -0
  150. {marvin-3.2.0 → marvin-3.2.2}/examples/hello_task.py +0 -0
  151. {marvin-3.2.0 → marvin-3.2.2}/examples/hello_team.py +0 -0
  152. {marvin-3.2.0 → marvin-3.2.2}/examples/pandas/data.csv +0 -0
  153. {marvin-3.2.0 → marvin-3.2.2}/examples/pandas/dataframe.py +0 -0
  154. {marvin-3.2.0 → marvin-3.2.2}/examples/provider_specific/azure/run_agent.py +0 -0
  155. {marvin-3.2.0 → marvin-3.2.2}/examples/provider_specific/openai/run_agent.py +0 -0
  156. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/.dockerignore +0 -0
  157. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/CLAUDE.md +0 -0
  158. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/Dockerfile.slackbot +0 -0
  159. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/README.md +0 -0
  160. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/pyproject.toml +0 -0
  161. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/__init__.py +0 -0
  162. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/__main__.py +0 -0
  163. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/_internal/__init__.py +0 -0
  164. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/_internal/constants.py +0 -0
  165. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/_internal/monkey_patch.py +0 -0
  166. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/_internal/templates.py +0 -0
  167. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/_internal/tool_tracking.py +0 -0
  168. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/assets.py +0 -0
  169. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/github/__init__.py +0 -0
  170. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/github/client.py +0 -0
  171. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/github/discussions.py +0 -0
  172. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/github/exceptions.py +0 -0
  173. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/github/issues.py +0 -0
  174. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/github/models.py +0 -0
  175. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/prompts.py +0 -0
  176. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/py.typed +0 -0
  177. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/research_agent.py +0 -0
  178. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/search.py +0 -0
  179. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/settings.py +0 -0
  180. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/strings.py +0 -0
  181. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/types.py +0 -0
  182. {marvin-3.2.0 → marvin-3.2.2}/examples/slackbot/src/slackbot/wrap.py +0 -0
  183. {marvin-3.2.0 → marvin-3.2.2}/examples/talk_to_marvin.py +0 -0
  184. {marvin-3.2.0 → marvin-3.2.2}/examples/task_retries.py +0 -0
  185. {marvin-3.2.0 → marvin-3.2.2}/examples/timeseries.py +0 -0
  186. {marvin-3.2.0 → marvin-3.2.2}/examples/you_have_been_goosed/README.md +0 -0
  187. {marvin-3.2.0 → marvin-3.2.2}/examples/you_have_been_goosed/goose_em.py +0 -0
  188. {marvin-3.2.0 → marvin-3.2.2}/justfile +0 -0
  189. {marvin-3.2.0 → marvin-3.2.2}/migrations/README.md +0 -0
  190. {marvin-3.2.0 → marvin-3.2.2}/migrations/env.py +0 -0
  191. {marvin-3.2.0 → marvin-3.2.2}/migrations/script.py.mako +0 -0
  192. {marvin-3.2.0 → marvin-3.2.2}/migrations/versions/20250301_141903_initial_schema.py +0 -0
  193. {marvin-3.2.0 → marvin-3.2.2}/migrations/versions/20250302_123716_improve_message_tracking.py +0 -0
  194. {marvin-3.2.0 → marvin-3.2.2}/migrations/versions/20250302_135101_add_llmcall_message_tracking.py +0 -0
  195. {marvin-3.2.0 → marvin-3.2.2}/pyproject.toml +0 -0
  196. {marvin-3.2.0 → marvin-3.2.2}/scripts/check_for_breakpoints.py +0 -0
  197. {marvin-3.2.0 → marvin-3.2.2}/scripts/create_mintlify_api_ref.py +0 -0
  198. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/__init__.py +0 -0
  199. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/_internal/deprecation.py +0 -0
  200. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/_internal/integrations/README.md +0 -0
  201. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/_internal/integrations/fastmcp.py +0 -0
  202. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/_internal/integrations/mcp.py +0 -0
  203. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/agents/CLAUDE.md +0 -0
  204. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/agents/__init__.py +0 -0
  205. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/agents/agent.py +0 -0
  206. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/agents/names.py +0 -0
  207. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/agents/team.py +0 -0
  208. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/beta/__init__.py +0 -0
  209. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/cli/__init__.py +0 -0
  210. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/cli/config.py +0 -0
  211. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/cli/dev.py +0 -0
  212. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/cli/main.py +0 -0
  213. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/cli/migrations.py +0 -0
  214. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/database.py +0 -0
  215. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/defaults.py +0 -0
  216. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/CLAUDE.md +0 -0
  217. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/__init__.py +0 -0
  218. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/end_turn.py +0 -0
  219. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/events.py +0 -0
  220. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/llm.py +0 -0
  221. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/engine/streaming.py +0 -0
  222. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/__init__.py +0 -0
  223. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/cast.py +0 -0
  224. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/classify.py +0 -0
  225. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/extract.py +0 -0
  226. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/fn.py +0 -0
  227. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/generate.py +0 -0
  228. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/plan.py +0 -0
  229. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/say.py +0 -0
  230. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/fns/summarize.py +0 -0
  231. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/handlers/__init__.py +0 -0
  232. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/handlers/handlers.py +0 -0
  233. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/handlers/print_handler.py +0 -0
  234. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/handlers/queue_handler.py +0 -0
  235. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/instructions.md +0 -0
  236. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/instructions.py +0 -0
  237. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/__init__.py +0 -0
  238. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/memory.py +0 -0
  239. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/providers/__init__.py +0 -0
  240. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/providers/chroma.py +0 -0
  241. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/providers/lance.py +0 -0
  242. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/providers/postgres.py +0 -0
  243. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/memory/providers/qdrant.py +0 -0
  244. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/prompts.py +0 -0
  245. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/py.typed +0 -0
  246. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/settings.py +0 -0
  247. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tasks/CLAUDE.md +0 -0
  248. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tasks/__init__.py +0 -0
  249. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/templates/agent.jinja +0 -0
  250. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/templates/memory.jinja +0 -0
  251. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/templates/system.jinja +0 -0
  252. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/templates/task.jinja +0 -0
  253. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/templates/team.jinja +0 -0
  254. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/thread.py +0 -0
  255. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tools/CLAUDE.md +0 -0
  256. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tools/__init__.py +0 -0
  257. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tools/interactive/cli.py +0 -0
  258. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/tools/thread.py +0 -0
  259. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/__init__.py +0 -0
  260. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/asyncio.py +0 -0
  261. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/jinja.py +0 -0
  262. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/jsonschema.py +0 -0
  263. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/logging.py +0 -0
  264. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/models.py +0 -0
  265. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/testing.py +0 -0
  266. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/tools.py +0 -0
  267. {marvin-3.2.0 → marvin-3.2.2}/src/marvin/utilities/types.py +0 -0
  268. {marvin-3.2.0 → marvin-3.2.2}/tests/__init__.py +0 -0
  269. {marvin-3.2.0 → marvin-3.2.2}/tests/agents/test_mcp_integration.py +0 -0
  270. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/README.md +0 -0
  271. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/__init__.py +0 -0
  272. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/__init__.py +0 -0
  273. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_cast.py +0 -0
  274. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_classify.py +0 -0
  275. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_customization.py +0 -0
  276. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_extract.py +0 -0
  277. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_fn.py +0 -0
  278. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_generate.py +0 -0
  279. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_run.py +0 -0
  280. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_say.py +0 -0
  281. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/fns/test_summarize.py +0 -0
  282. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/memory/__init__.py +0 -0
  283. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/memory/test_memory.py +0 -0
  284. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/tasks/__init__.py +0 -0
  285. {marvin-3.2.0 → marvin-3.2.2}/tests/ai/test_mcp.py +0 -0
  286. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/README.md +0 -0
  287. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/__init__.py +0 -0
  288. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/actors/__init__.py +0 -0
  289. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/actors/test_agent.py +0 -0
  290. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/actors/test_team.py +0 -0
  291. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/conftest.py +0 -0
  292. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/engine/__init__.py +0 -0
  293. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/engine/test_database.py +0 -0
  294. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/engine/test_events.py +0 -0
  295. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/engine/test_orchestrator.py +0 -0
  296. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/engine/test_streaming.py +0 -0
  297. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/engine/test_threads.py +0 -0
  298. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/memory/__init__.py +0 -0
  299. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/memory/test_memory.py +0 -0
  300. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/tasks/__init__.py +0 -0
  301. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/test_instructions.py +0 -0
  302. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/test_run.py +0 -0
  303. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/utilities/__init__.py +0 -0
  304. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/utilities/test_jsonschema.py +0 -0
  305. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/utilities/test_tools.py +0 -0
  306. {marvin-3.2.0 → marvin-3.2.2}/tests/basic/utilities/test_types.py +0 -0
  307. {marvin-3.2.0 → marvin-3.2.2}/tests/conftest.py +0 -0
  308. {marvin-3.2.0 → marvin-3.2.2}/tests/data/youre-funny.wav +0 -0
  309. {marvin-3.2.0 → marvin-3.2.2}/tests/settings/test_settings_object.py +0 -0
  310. {marvin-3.2.0 → marvin-3.2.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marvin
3
- Version: 3.2.0
3
+ Version: 3.2.2
4
4
  Summary: a simple and powerful tool to get things done with AI
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -32,11 +32,13 @@ def run_stream(instructions: str | Sequence[UserContent], result_type: type[T] =
32
32
  ```python
33
33
  def run_tasks(tasks: list[Task[Any]], thread: Thread | str | None = None, raise_on_failure: bool = True, handlers: list[Handler | AsyncHandler] | None = None) -> list[Task[Any]]
34
34
  ```
35
+ Run tasks either concurrently (if independent) or sequentially.
35
36
 
36
37
  ### `run_tasks_async`
37
38
  ```python
38
39
  def run_tasks_async(tasks: list[Task[Any]], thread: Thread | str | None = None, raise_on_failure: bool = True, handlers: list[Handler | AsyncHandler] | None = None) -> list[Task[Any]] | AsyncGenerator[Event, None]
39
40
  ```
41
+ Run tasks either concurrently (if independent) or sequentially via orchestrator.
40
42
 
41
43
  ### `run_tasks_stream`
42
44
  ```python
@@ -0,0 +1,94 @@
1
+ """Thread status tracking for Slack events (cross-process safe).
2
+
3
+ This module encapsulates a tiny SQLite-backed status mechanism to avoid duplicate
4
+ processing of the same Slack thread when users edit their original post or when
5
+ Slack delivers multiple events. It keeps the rest of the app simple and avoids
6
+ sprawling status logic across files.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Literal
12
+
13
+ from slackbot.core import Database
14
+
15
+ Status = Literal["in_progress", "completed"]
16
+
17
+
18
+ async def ensure_schema(db: Database) -> None:
19
+ """Ensure the status table exists.
20
+
21
+ Using CREATE TABLE IF NOT EXISTS is idempotent and cheap enough to call
22
+ whenever we touch the status table, avoiding module-level state or locks.
23
+ """
24
+
25
+ def _create() -> None:
26
+ db.con.execute(
27
+ """
28
+ CREATE TABLE IF NOT EXISTS slack_thread_status (
29
+ thread_ts TEXT PRIMARY KEY,
30
+ status TEXT NOT NULL CHECK (status IN ('in_progress','completed')),
31
+ updated_at REAL NOT NULL
32
+ );
33
+ """
34
+ )
35
+ db.con.commit()
36
+
37
+ await db.loop.run_in_executor(db.executor, _create)
38
+
39
+
40
+ async def try_acquire(db: Database, thread_ts: str) -> bool:
41
+ """Attempt to mark a thread as in_progress; returns True if acquired.
42
+
43
+ Uses an atomic INSERT OR IGNORE on a PRIMARY KEY to prevent duplicates across
44
+ concurrent processes or tasks.
45
+ """
46
+ await ensure_schema(db)
47
+
48
+ def _insert() -> int:
49
+ cur = db.con.cursor()
50
+ cur.execute(
51
+ """
52
+ INSERT OR IGNORE INTO slack_thread_status (thread_ts, status, updated_at)
53
+ VALUES (?, 'in_progress', strftime('%s','now'))
54
+ """,
55
+ (thread_ts,),
56
+ )
57
+ db.con.commit()
58
+ return cur.rowcount
59
+
60
+ rowcount: int = await db.loop.run_in_executor(db.executor, _insert)
61
+ return rowcount == 1
62
+
63
+
64
+ async def get_status(db: Database, thread_ts: str) -> Status | None:
65
+ await ensure_schema(db)
66
+
67
+ def _query() -> Status | None:
68
+ cur = db.con.cursor()
69
+ cur.execute(
70
+ "SELECT status FROM slack_thread_status WHERE thread_ts = ?",
71
+ (thread_ts,),
72
+ )
73
+ row = cur.fetchone()
74
+ return row[0] if row else None # type: ignore[return-value]
75
+
76
+ return await db.loop.run_in_executor(db.executor, _query)
77
+
78
+
79
+ async def mark_completed(db: Database, thread_ts: str) -> None:
80
+ await ensure_schema(db)
81
+
82
+ def _update() -> None:
83
+ cur = db.con.cursor()
84
+ cur.execute(
85
+ """
86
+ UPDATE slack_thread_status
87
+ SET status = 'completed', updated_at = strftime('%s','now')
88
+ WHERE thread_ts = ?
89
+ """,
90
+ (thread_ts,),
91
+ )
92
+ db.con.commit()
93
+
94
+ await db.loop.run_in_executor(db.executor, _update)
@@ -17,6 +17,15 @@ from pydantic_ai.messages import ModelMessage
17
17
 
18
18
  from slackbot._internal.constants import WORKSPACE_TO_CHANNEL_ID
19
19
  from slackbot._internal.templates import CHANNEL_REDIRECT_MESSAGE, WELCOME_MESSAGE
20
+ from slackbot._internal.thread_status import (
21
+ get_status as get_thread_status,
22
+ )
23
+ from slackbot._internal.thread_status import (
24
+ mark_completed as mark_thread_completed,
25
+ )
26
+ from slackbot._internal.thread_status import (
27
+ try_acquire as try_acquire_thread,
28
+ )
20
29
  from slackbot.assets import summarize_thread
21
30
  from slackbot.core import (
22
31
  Database,
@@ -40,6 +49,9 @@ BOT_MENTION = r"<@(\w+)>"
40
49
 
41
50
  logger = get_logger(__name__)
42
51
 
52
+ # Duplicate handling is coordinated via a small SQLite table in
53
+ # _internal.thread_status to work across processes.
54
+
43
55
 
44
56
  def get_designated_channel_for_workspace(team_id: str) -> str | None:
45
57
  """Get the designated channel ID for a given workspace team ID."""
@@ -106,6 +118,33 @@ async def run_agent(
106
118
  raise
107
119
 
108
120
 
121
+ def _extract_message_context(event: Any) -> tuple[bool, str | None, str | None, str]:
122
+ """Return (is_edit, message_ts, thread_ts, text) for Slack events.
123
+
124
+ - For `message_changed` events, Slack nests the edited message under `event.message`.
125
+ - For normal app_mention events, fields are at the top level.
126
+ """
127
+ is_edit = getattr(event, "subtype", None) == "message_changed"
128
+ msg = (getattr(event, "message", None) or {}) if is_edit else {}
129
+
130
+ # Prefer the message ts for idempotency; fall back to event_ts if needed
131
+ message_ts = (
132
+ msg.get("ts")
133
+ if is_edit
134
+ else (getattr(event, "ts", None) or getattr(event, "event_ts", None))
135
+ )
136
+ # Thread anchor where we should post replies
137
+ thread_ts = (
138
+ (msg.get("thread_ts") or msg.get("ts"))
139
+ if is_edit
140
+ else (getattr(event, "thread_ts", None) or getattr(event, "ts", None))
141
+ )
142
+ # Text used for bot mention detection
143
+ text = (msg.get("text") if is_edit else (getattr(event, "text", None) or "")) or ""
144
+
145
+ return is_edit, message_ts, thread_ts, text
146
+
147
+
109
148
  @flow(name="Handle Slack Message", retries=1)
110
149
  async def handle_message(payload: SlackPayload, db: Database):
111
150
  logger = get_run_logger()
@@ -115,9 +154,10 @@ async def handle_message(payload: SlackPayload, db: Database):
115
154
  return Completed(message="Invalid event", name="SKIPPED")
116
155
 
117
156
  USER_MESSAGE_MAX_TOKENS = settings.user_message_max_tokens
118
- user_message = event.text or ""
119
- thread_ts = event.thread_ts or event.ts
157
+ # Determine message context accommodating edit events
158
+ is_edit, message_ts, thread_ts, user_message = _extract_message_context(event)
120
159
  assert thread_ts is not None, "No thread_ts found"
160
+ assert message_ts is not None, "No message_ts found"
121
161
  cleaned_message = re.sub(BOT_MENTION, "", user_message).strip()
122
162
  msg_len = count_tokens(cleaned_message)
123
163
 
@@ -137,6 +177,34 @@ async def handle_message(payload: SlackPayload, db: Database):
137
177
  return Completed(message="Message too long", name="SKIPPED")
138
178
 
139
179
  if re.search(BOT_MENTION, user_message) and payload.authorizations:
180
+ # Per-message acquire: prevent duplicate handling for this specific message
181
+ acquired = await try_acquire_thread(db, message_ts)
182
+ if not acquired:
183
+ status = await get_thread_status(db, message_ts)
184
+ if status == "in_progress" and is_edit:
185
+ assert event.channel is not None, (
186
+ "Event channel is None when posting edit-ignored notice"
187
+ )
188
+ await post_slack_message(
189
+ message=(
190
+ "✋ I noticed you edited your original message. "
191
+ "I'm already working on your first version — please add any "
192
+ "clarifications as new messages in this thread so I don't lose track."
193
+ ),
194
+ channel_id=event.channel,
195
+ thread_ts=thread_ts,
196
+ )
197
+ return Completed(
198
+ message="Ignored edit while in progress",
199
+ name="IGNORED_EDIT",
200
+ data=dict(message_ts=message_ts, thread_ts=thread_ts),
201
+ )
202
+ return Completed(
203
+ message="Duplicate event for message",
204
+ name="SKIPPED_DUPLICATE",
205
+ data=dict(message_ts=message_ts, thread_ts=thread_ts),
206
+ )
207
+
140
208
  # Check if this is the designated channel
141
209
  team_id = payload.team_id or ""
142
210
  is_designated = check_if_designated_channel(event.channel, team_id)
@@ -188,7 +256,11 @@ async def handle_message(payload: SlackPayload, db: Database):
188
256
 
189
257
  try:
190
258
  result = await run_agent(
191
- cleaned_message, conversation, user_context, event.channel, thread_ts
259
+ cleaned_message,
260
+ conversation,
261
+ user_context,
262
+ event.channel,
263
+ thread_ts,
192
264
  ) # type: ignore
193
265
 
194
266
  await db.add_thread_messages(thread_ts, result.new_messages())
@@ -213,6 +285,12 @@ async def handle_message(payload: SlackPayload, db: Database):
213
285
  name="ERROR_HANDLED",
214
286
  data=dict(error=str(e), user_context=user_context),
215
287
  )
288
+ finally:
289
+ try:
290
+ await mark_thread_completed(db, message_ts)
291
+ except Exception:
292
+ logger.warning("Failed to mark message as completed")
293
+
216
294
  return Completed(
217
295
  message="Responded to mention",
218
296
  data=dict(user_context=user_context, conversation=conversation),
@@ -129,6 +129,9 @@ class Database:
129
129
 
130
130
  await self.loop.run_in_executor(self.executor, _insert)
131
131
 
132
+ # Note: Thread status tracking is handled in-process within api.py to keep
133
+ # persistence minimal for the examples package.
134
+
132
135
 
133
136
  @task(task_run_name="build user context for {user_id}")
134
137
  def build_user_context(
@@ -29,7 +29,10 @@ class EventBlock(BaseModel):
29
29
  class SlackEvent(BaseModel):
30
30
  client_msg_id: str | None = None
31
31
  type: str
32
+ subtype: str | None = None
32
33
  text: str | None = None
34
+ # For message_changed edit events, Slack nests the edited message here
35
+ message: dict[str, Any] | None = None
33
36
  user: str | dict[str, Any] | None = None
34
37
  ts: str | None = None
35
38
  team: str | None = None
@@ -76,7 +76,17 @@ class Actor(ABC):
76
76
  def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any):
77
77
  """Reset the current actor in context."""
78
78
  if self._tokens: # Only reset if we have tokens
79
- _current_actor.reset(self._tokens.pop())
79
+ try:
80
+ _current_actor.reset(self._tokens.pop())
81
+ except ValueError as e:
82
+ # Token was created in a different async context (e.g., asyncio.gather)
83
+ # This happens when tasks run concurrently and is expected behavior
84
+ if "was created in a different Context" in str(e):
85
+ # This is the expected concurrent execution case - ignore safely
86
+ pass
87
+ else:
88
+ # Some other ValueError - re-raise it
89
+ raise
80
90
 
81
91
  @classmethod
82
92
  def get_current(cls) -> "Actor | None":
@@ -143,7 +143,22 @@ class Orchestrator:
143
143
  if actor is None:
144
144
  actor = tasks[0].get_actor()
145
145
 
146
- assigned_tasks = [t for t in tasks if actor is t.get_actor()]
146
+ # Get tasks assigned to this actor
147
+ potential_tasks = [t for t in tasks if actor is t.get_actor()]
148
+
149
+ # For independent tasks, only assign one per turn to avoid EndTurn conflicts
150
+ if len(potential_tasks) > 1:
151
+ # Check if any tasks depend on each other
152
+ has_deps = any(
153
+ t2 in t1.depends_on or t1 in t2.depends_on
154
+ for t1 in potential_tasks
155
+ for t2 in potential_tasks
156
+ if t1 != t2
157
+ )
158
+ # If independent, process one at a time
159
+ assigned_tasks = [potential_tasks[0]] if not has_deps else potential_tasks
160
+ else:
161
+ assigned_tasks = potential_tasks
147
162
 
148
163
  # Mark tasks as running if they're pending
149
164
  for task in assigned_tasks:
@@ -15,19 +15,43 @@ from marvin.handlers.handlers import AsyncHandler, Handler
15
15
  T = TypeVar("T")
16
16
 
17
17
 
18
+ def _tasks_are_independent(tasks: list[Task[Any]]) -> bool:
19
+ """Check if tasks have no dependencies between each other."""
20
+ for i, task1 in enumerate(tasks):
21
+ for j, task2 in enumerate(tasks):
22
+ if i != j:
23
+ # Check if task1 depends on task2 or vice versa
24
+ if task2 in task1.depends_on or task1 in task2.depends_on:
25
+ return False
26
+ # Check if they share subtasks or parent relationships
27
+ if task1.parent == task2 or task2.parent == task1:
28
+ return False
29
+ if task1 in task2.subtasks or task2 in task1.subtasks:
30
+ return False
31
+ return True
32
+
33
+
18
34
  async def run_tasks_async(
19
35
  tasks: list[Task[Any]],
20
36
  thread: Thread | str | None = None,
21
37
  raise_on_failure: bool = True,
22
38
  handlers: list[Handler | AsyncHandler] | None = None,
23
39
  ) -> list[Task[Any]] | AsyncGenerator[Event, None]:
24
- orchestrator = Orchestrator(
25
- tasks=tasks,
26
- thread=thread,
27
- handlers=handlers,
28
- )
29
- await orchestrator.run(raise_on_failure=raise_on_failure)
30
- return tasks
40
+ """Run tasks either concurrently (if independent) or sequentially via orchestrator."""
41
+ # If we have multiple independent tasks, run them concurrently
42
+ if len(tasks) > 1 and _tasks_are_independent(tasks):
43
+ # Run independent tasks concurrently using asyncio.gather
44
+ await asyncio.gather(*[task.run_async() for task in tasks])
45
+ return tasks
46
+ else:
47
+ # Use orchestrator for dependent tasks or single tasks
48
+ orchestrator = Orchestrator(
49
+ tasks=tasks,
50
+ thread=thread,
51
+ handlers=handlers,
52
+ )
53
+ await orchestrator.run(raise_on_failure=raise_on_failure)
54
+ return tasks
31
55
 
32
56
 
33
57
  async def run_tasks_stream(
@@ -79,6 +103,7 @@ def run_tasks(
79
103
  raise_on_failure: bool = True,
80
104
  handlers: list[Handler | AsyncHandler] | None = None,
81
105
  ) -> list[Task[Any]]:
106
+ """Run tasks either concurrently (if independent) or sequentially."""
82
107
  return marvin.utilities.asyncio.run_sync(
83
108
  run_tasks_async(
84
109
  tasks=tasks,
@@ -20,6 +20,7 @@ from typing import (
20
20
  Sequence,
21
21
  TypeAlias,
22
22
  TypeVar,
23
+ get_args,
23
24
  )
24
25
 
25
26
  from pydantic import TypeAdapter
@@ -452,7 +453,10 @@ class Task(Generic[T]):
452
453
  else:
453
454
  raise ValueError("Result type is None but result is not None")
454
455
  elif raw_result is None:
455
- raise ValueError("Result is None but result type is not None")
456
+ # Check if None is allowed in the type (e.g., Optional[str] = str | None)
457
+ type_args = get_args(result_type)
458
+ if type(None) not in type_args:
459
+ raise ValueError("Result is None but result type is not None")
456
460
 
457
461
  type_adapter = get_type_adapter(result_type)
458
462
  return type_adapter.validate_python(raw_result)
@@ -0,0 +1,34 @@
1
+ from marvin import Task
2
+
3
+
4
+ class TestTaskPrompt:
5
+ async def test_task_return_string_prompt(self):
6
+ # default tasks return a list; ensure strings work as well
7
+ class MyTask(Task):
8
+ def get_prompt(self) -> str:
9
+ return 'Say the word "hello"'
10
+
11
+ task = MyTask("")
12
+ prompt = task.get_prompt()
13
+ assert prompt == 'Say the word "hello"'
14
+ assert task.run() == "hello"
15
+
16
+
17
+ class TestOptionalResults:
18
+ async def test_optional_result_returns_none(self):
19
+ """Test that tasks with optional result types can actually return None from AI."""
20
+ task = Task(
21
+ instructions="return null",
22
+ result_type=str | None,
23
+ )
24
+ result = await task.run_async()
25
+ assert result is None
26
+
27
+ async def test_optional_result_returns_value(self):
28
+ """Test that optional tasks can also return actual values."""
29
+ task = Task(
30
+ instructions='say "hello"',
31
+ result_type=str | None,
32
+ )
33
+ result = await task.run_async()
34
+ assert result == "hello"
@@ -258,6 +258,13 @@ async def test_task_mark_successful():
258
258
  assert task.result == "test result"
259
259
 
260
260
 
261
+ async def test_task_optional_result_type():
262
+ """Test that tasks with Optional result types can validate None."""
263
+ task = Task(instructions="test", result_type=str | None)
264
+ validated = task.validate_result(None)
265
+ assert validated is None
266
+
267
+
261
268
  async def test_task_mark_failed():
262
269
  """Test marking task as failed."""
263
270
  task = Task(instructions="Test failure")
@@ -0,0 +1,191 @@
1
+ """Unit tests for concurrent task execution."""
2
+
3
+ import asyncio
4
+
5
+ import pytest
6
+
7
+ from marvin import Task
8
+ from marvin.agents.agent import Agent
9
+ from marvin.fns.run import _tasks_are_independent, run_tasks, run_tasks_async
10
+
11
+
12
+ class TestTaskIndependenceDetection:
13
+ """Test the independence detection logic."""
14
+
15
+ def test_independent_tasks(self):
16
+ """Test that truly independent tasks are detected as such."""
17
+ task1 = Task("Say 'one'", result_type=str)
18
+ task2 = Task("Say 'two'", result_type=str)
19
+ task3 = Task("Say 'three'", result_type=str)
20
+
21
+ assert _tasks_are_independent([task1, task2, task3])
22
+
23
+ def test_dependent_tasks_depends_on(self):
24
+ """Test that tasks with depends_on are not independent."""
25
+ task1 = Task("Say 'one'", result_type=str)
26
+ task2 = Task("Say 'two'", result_type=str, depends_on=[task1])
27
+
28
+ assert not _tasks_are_independent([task1, task2])
29
+
30
+ def test_dependent_tasks_parent_child(self):
31
+ """Test that parent-child tasks are not independent."""
32
+ parent = Task("Parent task", result_type=str)
33
+ child = Task("Child task", result_type=str)
34
+ parent.subtasks.add(child) # subtasks is a set, not list
35
+ child.parent = parent
36
+
37
+ assert not _tasks_are_independent([parent, child])
38
+
39
+ def test_single_task_is_independent(self):
40
+ """Test that a single task is considered independent."""
41
+ task = Task("Solo task", result_type=str)
42
+ assert _tasks_are_independent([task])
43
+
44
+ def test_empty_task_list(self):
45
+ """Test empty task list."""
46
+ assert _tasks_are_independent([])
47
+
48
+
49
+ class TestConcurrentExecution:
50
+ """Test actual concurrent execution behavior."""
51
+
52
+ @pytest.mark.asyncio
53
+ async def test_independent_tasks_run_without_errors(self):
54
+ """Test that independent tasks run without Multiple EndTurn warnings or errors."""
55
+ task1 = Task("Say 'one'", result_type=str)
56
+ task2 = Task("Say 'two'", result_type=str)
57
+ task3 = Task("Say 'three'", result_type=str)
58
+
59
+ # Should not raise any errors (no Multiple EndTurn warnings, no infinite loops)
60
+ results = await run_tasks_async([task1, task2, task3])
61
+
62
+ assert len(results) == 3
63
+ assert all(task.is_successful() for task in results)
64
+
65
+ # Verify results
66
+ result_values = [task.result for task in results]
67
+ assert set(result_values) == {"one", "two", "three"}
68
+
69
+ @pytest.mark.asyncio
70
+ async def test_dependent_tasks_run_in_order(self):
71
+ """Test that dependent tasks run in correct order."""
72
+ task1 = Task("Say 'A'", result_type=str)
73
+ task2 = Task("Say 'B'", result_type=str, depends_on=[task1])
74
+ task3 = Task("Say 'C'", result_type=str, depends_on=[task2])
75
+
76
+ results = await run_tasks_async([task1, task2, task3])
77
+
78
+ assert len(results) == 3
79
+ assert all(task.is_successful() for task in results)
80
+
81
+ # Verify correct order
82
+ result_values = [task.result for task in results]
83
+ assert result_values == ["A", "B", "C"]
84
+
85
+ def test_sync_run_tasks_independent(self):
86
+ """Test synchronous run_tasks with independent tasks."""
87
+ task1 = Task("Say 'one'", result_type=str)
88
+ task2 = Task("Say 'two'", result_type=str)
89
+
90
+ results = run_tasks([task1, task2])
91
+
92
+ assert len(results) == 2
93
+ assert all(task.is_successful() for task in results)
94
+ assert set(t.result for t in results) == {"one", "two"}
95
+
96
+ def test_sync_run_tasks_dependent(self):
97
+ """Test synchronous run_tasks with dependent tasks."""
98
+ task1 = Task("Say 'A'", result_type=str)
99
+ task2 = Task("Say 'B'", result_type=str, depends_on=[task1])
100
+
101
+ results = run_tasks([task1, task2])
102
+
103
+ assert len(results) == 2
104
+ assert all(task.is_successful() for task in results)
105
+
106
+ # Verify correct order
107
+ result_values = [task.result for task in results]
108
+ assert result_values == ["A", "B"]
109
+
110
+
111
+ class TestAsyncioGatherCompatibility:
112
+ """Test that asyncio.gather works without ContextVar errors."""
113
+
114
+ @pytest.mark.asyncio
115
+ async def test_asyncio_gather_no_context_errors(self):
116
+ """Test that asyncio.gather doesn't throw ContextVar errors."""
117
+ task1 = Task("Say 'async1'", result_type=str)
118
+ task2 = Task("Say 'async2'", result_type=str)
119
+ task3 = Task("Say 'async3'", result_type=str)
120
+
121
+ # This should not raise ContextVar token errors
122
+ results = await asyncio.gather(
123
+ task1.run_async(), task2.run_async(), task3.run_async()
124
+ )
125
+
126
+ assert len(results) == 3
127
+ assert set(results) == {"async1", "async2", "async3"}
128
+
129
+ @pytest.mark.asyncio
130
+ async def test_mixed_execution_patterns(self):
131
+ """Test mixing run_tasks_async and asyncio.gather in same event loop."""
132
+ # First batch via run_tasks_async
133
+ task1 = Task("Say 'batch1'", result_type=str)
134
+ task2 = Task("Say 'batch2'", result_type=str)
135
+ batch1_results = await run_tasks_async([task1, task2])
136
+
137
+ # Second batch via asyncio.gather
138
+ task3 = Task("Say 'gather1'", result_type=str)
139
+ task4 = Task("Say 'gather2'", result_type=str)
140
+ batch2_results = await asyncio.gather(task3.run_async(), task4.run_async())
141
+
142
+ # Both should work without errors
143
+ assert len(batch1_results) == 2
144
+ assert all(task.is_successful() for task in batch1_results)
145
+ assert len(batch2_results) == 2
146
+ assert set(batch2_results) == {"gather1", "gather2"}
147
+
148
+
149
+ class TestContextVarHandling:
150
+ """Test ContextVar token handling across async contexts."""
151
+
152
+ @pytest.mark.asyncio
153
+ async def test_actor_context_across_asyncio_gather(self):
154
+ """Test that Actor context management handles asyncio.gather correctly."""
155
+ from marvin.agents.actor import _current_actor
156
+
157
+ async def task_with_actor(name):
158
+ actor = Agent(name=f"Agent_{name}")
159
+ # This should not raise an error even with asyncio.gather
160
+ with actor:
161
+ assert _current_actor.get() == actor
162
+ await asyncio.sleep(0.1) # Simulate async work
163
+ # Context should be reset without errors
164
+ return name
165
+
166
+ # Test that concurrent context management works
167
+ results = await asyncio.gather(
168
+ task_with_actor("1"), task_with_actor("2"), task_with_actor("3")
169
+ )
170
+
171
+ assert results == ["1", "2", "3"]
172
+ # Context should be None after all tasks complete
173
+ assert _current_actor.get() is None
174
+
175
+ def test_actor_context_sequential(self):
176
+ """Test that Actor context works normally in sequential execution."""
177
+ from marvin.agents.actor import _current_actor
178
+
179
+ actor1 = Agent(name="Sequential_1")
180
+ actor2 = Agent(name="Sequential_2")
181
+
182
+ # Test nested contexts work correctly
183
+ assert _current_actor.get() is None
184
+
185
+ with actor1:
186
+ assert _current_actor.get() == actor1
187
+ with actor2:
188
+ assert _current_actor.get() == actor2
189
+ assert _current_actor.get() == actor1
190
+
191
+ assert _current_actor.get() is None
@@ -1,14 +0,0 @@
1
- from marvin import Task
2
-
3
-
4
- class TestTaskPrompt:
5
- async def test_task_return_string_prompt(self):
6
- # default tasks return a list; ensure strings work as well
7
- class MyTask(Task):
8
- def get_prompt(self) -> str:
9
- return 'Say the word "hello"'
10
-
11
- task = MyTask("")
12
- prompt = task.get_prompt()
13
- assert prompt == 'Say the word "hello"'
14
- assert task.run() == "hello"
File without changes
File without changes
File without changes
File without changes
File without changes