agent-workflow-sdk 0.6.0__tar.gz → 0.6.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 (236) hide show
  1. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/CHANGELOG.md +20 -0
  2. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/PKG-INFO +1 -1
  3. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/__init__.py +1 -1
  4. agent_workflow_sdk-0.6.2/agentflow/backends/_winjob.py +152 -0
  5. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/backends/kiro.py +38 -15
  6. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/pyproject.toml +1 -1
  7. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_kiro_backend.py +117 -0
  8. agent_workflow_sdk-0.6.2/tests/test_winjob.py +83 -0
  9. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/.env.example +0 -0
  10. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  11. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  12. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/.github/workflows/ci.yml +0 -0
  14. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/.github/workflows/docs.yml +0 -0
  15. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/.github/workflows/release.yml +0 -0
  16. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/.gitignore +0 -0
  17. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/CONTRIBUTING.md +0 -0
  18. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/DESIGN.md +0 -0
  19. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/LICENSE +0 -0
  20. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/NOTICE +0 -0
  21. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/README.md +0 -0
  22. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/SECURITY.md +0 -0
  23. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/_pg.py +0 -0
  24. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/backends/__init__.py +0 -0
  25. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/backends/_http.py +0 -0
  26. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/backends/anthropic.py +0 -0
  27. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/backends/base.py +0 -0
  28. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/backends/claude_code.py +0 -0
  29. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/backends/cli_exec.py +0 -0
  30. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/backends/codex.py +0 -0
  31. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/backends/ollama.py +0 -0
  32. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/backends/openai.py +0 -0
  33. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/checkpoint/__init__.py +0 -0
  34. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/checkpoint/_serde.py +0 -0
  35. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/checkpoint/base.py +0 -0
  36. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/checkpoint/file.py +0 -0
  37. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/checkpoint/memory.py +0 -0
  38. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/checkpoint/postgres.py +0 -0
  39. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/checkpoint/redis.py +0 -0
  40. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/checkpoint/sqlite.py +0 -0
  41. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/compiled.py +0 -0
  42. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/controlplane/__init__.py +0 -0
  43. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/controlplane/memory.py +0 -0
  44. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/controlplane/postgres.py +0 -0
  45. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/controlplane/queue.py +0 -0
  46. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/controlplane/records.py +0 -0
  47. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/controlplane/registry.py +0 -0
  48. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/controlplane/worker.py +0 -0
  49. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/errors.py +0 -0
  50. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/events.py +0 -0
  51. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/graph.py +0 -0
  52. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/interrupts.py +0 -0
  53. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/observability.py +0 -0
  54. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/otel.py +0 -0
  55. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/prebuilt/__init__.py +0 -0
  56. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/prebuilt/budget.py +0 -0
  57. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/prebuilt/effects.py +0 -0
  58. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/prebuilt/gate.py +0 -0
  59. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/prebuilt/idempotent.py +0 -0
  60. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/prebuilt/loop.py +0 -0
  61. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/prebuilt/plan_executor.py +0 -0
  62. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/prebuilt/resilience.py +0 -0
  63. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/prebuilt/scoring.py +0 -0
  64. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/prebuilt/tool_loop.py +0 -0
  65. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/prebuilt/watch.py +0 -0
  66. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/prometheus.py +0 -0
  67. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/py.typed +0 -0
  68. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/redaction.py +0 -0
  69. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/runtime.py +0 -0
  70. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/server/__init__.py +0 -0
  71. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/server/app.py +0 -0
  72. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/state.py +0 -0
  73. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/store/__init__.py +0 -0
  74. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/store/_util.py +0 -0
  75. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/store/base.py +0 -0
  76. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/store/memory.py +0 -0
  77. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/store/postgres.py +0 -0
  78. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/agentflow/telemetry.py +0 -0
  79. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/design-notes/eval-harness.md +0 -0
  80. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/design-notes/plan-executor.md +0 -0
  81. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/design-notes/token-budget.md +0 -0
  82. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/backends/agent-backends.md +0 -0
  83. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/backends/llm-backends.md +0 -0
  84. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/backends/overview.md +0 -0
  85. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/backends/permissions.md +0 -0
  86. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/backends/retries.md +0 -0
  87. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/concepts/context.md +0 -0
  88. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/concepts/execution.md +0 -0
  89. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/concepts/nodes-and-edges.md +0 -0
  90. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/concepts/running.md +0 -0
  91. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/concepts/state.md +0 -0
  92. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/concepts/subgraphs.md +0 -0
  93. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/durability/checkpointing.md +0 -0
  94. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/durability/control-plane.md +0 -0
  95. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/durability/human-in-the-loop.md +0 -0
  96. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/durability/observability.md +0 -0
  97. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/durability/quality-gates.md +0 -0
  98. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/durability/store.md +0 -0
  99. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/get-started/installation.md +0 -0
  100. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/get-started/mental-model.md +0 -0
  101. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/get-started/overview.md +0 -0
  102. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/get-started/quickstart.md +0 -0
  103. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/index.md +0 -0
  104. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/patterns/long-running-loops.md +0 -0
  105. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/patterns/prebuilt.md +0 -0
  106. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/backends.md +0 -0
  107. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/budget.md +0 -0
  108. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/checkpoint.md +0 -0
  109. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/compiled.md +0 -0
  110. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/controlplane.md +0 -0
  111. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/errors.md +0 -0
  112. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/events.md +0 -0
  113. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/gate.md +0 -0
  114. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/graph.md +0 -0
  115. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/interrupts.md +0 -0
  116. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/observability.md +0 -0
  117. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/permissions.md +0 -0
  118. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/plan-executor.md +0 -0
  119. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/prebuilt.md +0 -0
  120. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/runtime.md +0 -0
  121. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/scoring.md +0 -0
  122. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/server.md +0 -0
  123. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/state.md +0 -0
  124. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/store.md +0 -0
  125. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api/watch.md +0 -0
  126. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/api.md +0 -0
  127. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/reference/glossary.md +0 -0
  128. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/index.md +0 -0
  129. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/optimizer/01-first-graph.md +0 -0
  130. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/optimizer/02-branching.md +0 -0
  131. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/optimizer/03-parallel.md +0 -0
  132. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/optimizer/04-dual-state.md +0 -0
  133. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/optimizer/05-backend.md +0 -0
  134. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/optimizer/06-gate.md +0 -0
  135. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/optimizer/07-human.md +0 -0
  136. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/optimizer/08-durability.md +0 -0
  137. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/optimizer/09-watch.md +0 -0
  138. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/optimizer/10-plan-executor.md +0 -0
  139. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/optimizer/index.md +0 -0
  140. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/qa-agent.md +0 -0
  141. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/query-optimizer/01-first-graph.md +0 -0
  142. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/query-optimizer/02-branching.md +0 -0
  143. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/query-optimizer/03-parallel.md +0 -0
  144. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/query-optimizer/04-dual-state.md +0 -0
  145. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/query-optimizer/05-agent.md +0 -0
  146. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/query-optimizer/06-gate.md +0 -0
  147. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/query-optimizer/07-human.md +0 -0
  148. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/query-optimizer/08-durability.md +0 -0
  149. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/query-optimizer/09-watch.md +0 -0
  150. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/query-optimizer/10-plan-executor.md +0 -0
  151. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/docs/tutorials/query-optimizer/index.md +0 -0
  152. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/agents_demo.py +0 -0
  153. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/codebase_qa_ollama.py +0 -0
  154. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/dual_state_search.py +0 -0
  155. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/hello_graph.py +0 -0
  156. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/hitl_permission.py +0 -0
  157. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/idempotent_effect.py +0 -0
  158. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/mixed_backends.py +0 -0
  159. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/optimize_loop.py +0 -0
  160. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/plan_executor_demo.py +0 -0
  161. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/quality_gate.py +0 -0
  162. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/research_plan_implement_review.py +0 -0
  163. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/serve_control_plane.py +0 -0
  164. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/telemetry_demo.py +0 -0
  165. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tool_loop_ollama.py +0 -0
  166. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/01_first_graph.py +0 -0
  167. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/02_branching.py +0 -0
  168. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/03_parallel.py +0 -0
  169. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/04_dual_state.py +0 -0
  170. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/05_backend.py +0 -0
  171. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/06_gate.py +0 -0
  172. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/07_human.py +0 -0
  173. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/08_durability.py +0 -0
  174. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/09_watch.py +0 -0
  175. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/10_plan_executor.py +0 -0
  176. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/__init__.py +0 -0
  177. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/ch01_first_graph.py +0 -0
  178. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/ch02_branching.py +0 -0
  179. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/ch03_parallel.py +0 -0
  180. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/ch04_dual_state.py +0 -0
  181. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/ch05_agent.py +0 -0
  182. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/ch06_gate.py +0 -0
  183. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/ch07_human.py +0 -0
  184. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/ch08_durability.py +0 -0
  185. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/ch09_watch.py +0 -0
  186. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/ch10_plan_executor.py +0 -0
  187. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/db.py +0 -0
  188. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/proposer.py +0 -0
  189. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/tutorial/queryopt/scorer.py +0 -0
  190. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/examples/watch_until_done.py +0 -0
  191. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/mkdocs.yml +0 -0
  192. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/conftest.py +0 -0
  193. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_anthropic_backend.py +0 -0
  194. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_backend_lifecycle.py +0 -0
  195. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_budget.py +0 -0
  196. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_checkpoint_contract.py +0 -0
  197. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_cli_backends.py +0 -0
  198. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_concurrency.py +0 -0
  199. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_controlplane.py +0 -0
  200. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_effects.py +0 -0
  201. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_gate.py +0 -0
  202. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_graph_engine.py +0 -0
  203. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_hitl_permission.py +0 -0
  204. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_http_retry.py +0 -0
  205. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_idempotent.py +0 -0
  206. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_input_validation.py +0 -0
  207. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_live_backends.py +0 -0
  208. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_observability.py +0 -0
  209. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_ollama_backend.py +0 -0
  210. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_openai_backend.py +0 -0
  211. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_otel.py +0 -0
  212. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_permission_policy.py +0 -0
  213. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_plan_executor.py +0 -0
  214. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_postgres_checkpointer.py +0 -0
  215. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_postgres_runqueue.py +0 -0
  216. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_postgres_store.py +0 -0
  217. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_prebuilt_loop.py +0 -0
  218. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_prebuilt_no_httpx.py +0 -0
  219. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_prometheus.py +0 -0
  220. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_provider_resilience.py +0 -0
  221. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_redaction.py +0 -0
  222. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_redis_checkpointer.py +0 -0
  223. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_reducers.py +0 -0
  224. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_resilience.py +0 -0
  225. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_run_timeout.py +0 -0
  226. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_scoring.py +0 -0
  227. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_server.py +0 -0
  228. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_sqlite_checkpointer.py +0 -0
  229. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_state_isolation.py +0 -0
  230. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_step_atomicity.py +0 -0
  231. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_store.py +0 -0
  232. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_subgraph.py +0 -0
  233. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_sync_facade.py +0 -0
  234. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_telemetry.py +0 -0
  235. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_tool_loop.py +0 -0
  236. {agent_workflow_sdk-0.6.0 → agent_workflow_sdk-0.6.2}/tests/test_watch.py +0 -0
@@ -4,6 +4,26 @@ All notable changes to `agent-workflow-sdk` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  project aims to follow [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.6.2] - 2026-11-05
8
+
9
+ ### Fixed
10
+
11
+ - On Windows the `kiro-cli` child now runs in a kill-on-close Job Object. A hard
12
+ kill ends the whole tree (`kiro-cli-chat` included), and if the host Python
13
+ process dies the OS ends it too. `taskkill /F /T` stays as the fallback. A
14
+ grandchild spawned in the first instant before the child joins the job is only
15
+ covered by that fallback.
16
+
17
+ ## [0.6.1] - 2026-11-05
18
+
19
+ ### Fixed
20
+
21
+ - `KiroBackend.close()` no longer raises `AttributeError` on Windows, where
22
+ `signal.SIGKILL`, `os.killpg` and `os.getpgid` do not exist. Shutdown escalates
23
+ by intent (terminate, then kill): POSIX still signals the process group; on
24
+ Windows the hard kill runs `taskkill /F /T` so grandchildren such as
25
+ `kiro-cli-chat` are reaped, then `proc.kill()`.
26
+
7
27
  ## [0.6.0] - 2026-09-29
8
28
 
9
29
  ### Changed (breaking)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-workflow-sdk
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Async-first SDK for orchestrating local coding-agent CLIs (Claude Code, Codex, Kiro) and LLMs as durable, graph-based workflows.
5
5
  Project-URL: Homepage, https://github.com/dankosorkin/agent-workflow-sdk
6
6
  Project-URL: Repository, https://github.com/dankosorkin/agent-workflow-sdk
@@ -99,7 +99,7 @@ from agentflow.state import (
99
99
  from agentflow.store import Item, MemoryStore, Store
100
100
  from agentflow.telemetry import JsonlTelemetry, MultiHooks
101
101
 
102
- __version__ = "0.6.0"
102
+ __version__ = "0.6.2"
103
103
 
104
104
  __all__ = [
105
105
  "__version__",
@@ -0,0 +1,152 @@
1
+ # Copyright 2026 Daniel Sorkin
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Windows Job Objects: keep a child and everything it spawns on a leash.
16
+
17
+ Windows has no process groups. A Job Object created with
18
+ ``JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`` contains a process and its descendants:
19
+ terminating the job kills the whole tree, and when the last handle to the job
20
+ closes (including because *our* process crashed or was killed) the OS kills the
21
+ tree itself, so no ``kiro-cli-chat`` is left behind.
22
+
23
+ Standard library (``ctypes``) only. Every function is a safe no-op returning
24
+ ``None``/``False`` off Windows or when the API call fails, so callers can fall
25
+ back to ``taskkill``. A process is assigned after it starts, so a grandchild
26
+ spawned in that first instant is not covered; the ``taskkill /T`` fallback exists
27
+ for that.
28
+
29
+ Known limit: a process already inside a job that forbids nesting cannot be
30
+ assigned (Windows 8+ allows nested jobs, so this is rare).
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import sys
36
+ from typing import Any
37
+
38
+ _KILL_ON_JOB_CLOSE = 0x2000
39
+ _JOB_OBJECT_EXTENDED_LIMIT_INFORMATION = 9
40
+ _PROCESS_TERMINATE = 0x0001
41
+ _PROCESS_SET_QUOTA = 0x0100
42
+
43
+
44
+ def _kernel32() -> Any:
45
+ """kernel32 with 64-bit-safe signatures, or ``None`` when not on Windows."""
46
+ if sys.platform != "win32":
47
+ return None
48
+ import ctypes
49
+ from ctypes import wintypes
50
+
51
+ k32 = ctypes.WinDLL("kernel32", use_last_error=True)
52
+ k32.CreateJobObjectW.argtypes = [wintypes.LPVOID, wintypes.LPCWSTR]
53
+ k32.CreateJobObjectW.restype = wintypes.HANDLE
54
+ k32.SetInformationJobObject.argtypes = [
55
+ wintypes.HANDLE,
56
+ ctypes.c_int,
57
+ wintypes.LPVOID,
58
+ wintypes.DWORD,
59
+ ]
60
+ k32.SetInformationJobObject.restype = wintypes.BOOL
61
+ k32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
62
+ k32.OpenProcess.restype = wintypes.HANDLE
63
+ k32.AssignProcessToJobObject.argtypes = [wintypes.HANDLE, wintypes.HANDLE]
64
+ k32.AssignProcessToJobObject.restype = wintypes.BOOL
65
+ k32.TerminateJobObject.argtypes = [wintypes.HANDLE, wintypes.UINT]
66
+ k32.TerminateJobObject.restype = wintypes.BOOL
67
+ k32.CloseHandle.argtypes = [wintypes.HANDLE]
68
+ k32.CloseHandle.restype = wintypes.BOOL
69
+ return k32
70
+
71
+
72
+ def _limit_info() -> Any:
73
+ """A JOBOBJECT_EXTENDED_LIMIT_INFORMATION with KILL_ON_JOB_CLOSE set."""
74
+ import ctypes
75
+ from ctypes import wintypes
76
+
77
+ class BasicLimits(ctypes.Structure):
78
+ _fields_ = [
79
+ ("PerProcessUserTimeLimit", ctypes.c_int64),
80
+ ("PerJobUserTimeLimit", ctypes.c_int64),
81
+ ("LimitFlags", wintypes.DWORD),
82
+ ("MinimumWorkingSetSize", ctypes.c_size_t),
83
+ ("MaximumWorkingSetSize", ctypes.c_size_t),
84
+ ("ActiveProcessLimit", wintypes.DWORD),
85
+ ("Affinity", ctypes.c_size_t),
86
+ ("PriorityClass", wintypes.DWORD),
87
+ ("SchedulingClass", wintypes.DWORD),
88
+ ]
89
+
90
+ class IoCounters(ctypes.Structure):
91
+ _fields_ = [(n, ctypes.c_uint64) for n in ("r_ops", "w_ops", "o_ops", "r_b", "w_b", "o_b")]
92
+
93
+ class ExtendedLimits(ctypes.Structure):
94
+ _fields_ = [
95
+ ("BasicLimitInformation", BasicLimits),
96
+ ("IoInfo", IoCounters),
97
+ ("ProcessMemoryLimit", ctypes.c_size_t),
98
+ ("JobMemoryLimit", ctypes.c_size_t),
99
+ ("PeakProcessMemoryUsed", ctypes.c_size_t),
100
+ ("PeakJobMemoryUsed", ctypes.c_size_t),
101
+ ]
102
+
103
+ info = ExtendedLimits()
104
+ info.BasicLimitInformation.LimitFlags = _KILL_ON_JOB_CLOSE
105
+ return info
106
+
107
+
108
+ def attach(pid: int) -> Any | None:
109
+ """Put process ``pid`` in a new kill-on-close job and return the job handle.
110
+
111
+ Returns ``None`` (nothing to clean up) off Windows or if any step fails.
112
+ """
113
+ import ctypes
114
+
115
+ k32 = _kernel32()
116
+ if k32 is None:
117
+ return None
118
+ job = k32.CreateJobObjectW(None, None)
119
+ if not job:
120
+ return None
121
+ info = _limit_info()
122
+ ok = k32.SetInformationJobObject(
123
+ job, _JOB_OBJECT_EXTENDED_LIMIT_INFORMATION, ctypes.byref(info), ctypes.sizeof(info)
124
+ )
125
+ if not ok:
126
+ k32.CloseHandle(job)
127
+ return None
128
+ process = k32.OpenProcess(_PROCESS_SET_QUOTA | _PROCESS_TERMINATE, False, pid)
129
+ if not process:
130
+ k32.CloseHandle(job)
131
+ return None
132
+ try:
133
+ assigned = k32.AssignProcessToJobObject(job, process)
134
+ finally:
135
+ k32.CloseHandle(process)
136
+ if not assigned:
137
+ k32.CloseHandle(job)
138
+ return None
139
+ return job
140
+
141
+
142
+ def terminate(job: Any) -> bool:
143
+ """Kill every process in ``job``. ``True`` if Windows accepted it."""
144
+ k32 = _kernel32()
145
+ return bool(k32 is not None and job and k32.TerminateJobObject(job, 1))
146
+
147
+
148
+ def close(job: Any) -> None:
149
+ """Release the job handle; with kill-on-close this also kills any survivor."""
150
+ k32 = _kernel32()
151
+ if k32 is not None and job:
152
+ k32.CloseHandle(job)
@@ -32,10 +32,13 @@ import contextlib
32
32
  import json
33
33
  import os
34
34
  import signal
35
+ import subprocess
36
+ import sys
35
37
  from collections.abc import AsyncIterator
36
38
  from pathlib import Path
37
39
  from typing import Any, Literal
38
40
 
41
+ from agentflow.backends import _winjob
39
42
  from agentflow.backends.base import BaseAgentBackend, PermissionPolicy
40
43
  from agentflow.errors import BackendError, BackendTransportError
41
44
  from agentflow.events import (
@@ -83,6 +86,8 @@ class KiroBackend(BaseAgentBackend):
83
86
  self._close_grace = close_grace
84
87
 
85
88
  self._proc: asyncio.subprocess.Process | None = None
89
+ # Windows only: the kill-on-close Job Object holding the child's whole tree.
90
+ self._job: Any = None
86
91
  self._session_id: str | None = None
87
92
  self._request_id = 0
88
93
 
@@ -131,6 +136,8 @@ class KiroBackend(BaseAgentBackend):
131
136
  )
132
137
  except OSError as exc:
133
138
  raise BackendTransportError(f"could not start kiro-cli: {exc}") from exc
139
+ if sys.platform == "win32":
140
+ self._job = _winjob.attach(self._proc.pid)
134
141
 
135
142
  self._reader_task = asyncio.create_task(self._read_loop())
136
143
 
@@ -156,35 +163,51 @@ class KiroBackend(BaseAgentBackend):
156
163
  try:
157
164
  await asyncio.wait_for(proc.wait(), timeout=self._close_grace)
158
165
  except TimeoutError:
159
- self._signal_group(proc, signal.SIGTERM)
166
+ self._stop(proc, hard=False, job=self._job)
160
167
  try:
161
168
  await asyncio.wait_for(proc.wait(), timeout=self._close_grace)
162
169
  except TimeoutError:
163
- self._signal_group(proc, signal.SIGKILL)
170
+ self._stop(proc, hard=True, job=self._job)
164
171
  finally:
165
172
  if self._reader_task:
166
173
  self._reader_task.cancel()
167
174
  with contextlib.suppress(asyncio.CancelledError, Exception):
168
175
  await self._reader_task
169
176
  self._proc = None
177
+ if self._job is not None:
178
+ # Kill-on-close: also reaps any descendant that outlived a clean exit.
179
+ _winjob.close(self._job)
180
+ self._job = None
170
181
  self._fail_pending(BackendTransportError("backend closed"))
171
182
 
172
183
  @staticmethod
173
- def _signal_group(proc: asyncio.subprocess.Process, sig: int) -> None:
174
- """Send ``sig`` to the child's whole process group, if possible.
175
-
176
- The child was started with ``start_new_session=True`` so it leads its
177
- own group; signalling ``-pgid`` reaches its descendants too. Falls back
178
- to signalling just the process where process groups aren't available
179
- (e.g. Windows) or the group has already gone. Never raises.
184
+ def _stop(proc: asyncio.subprocess.Process, *, hard: bool, job: Any = None) -> None:
185
+ """Stop ``proc`` by intent (soft: terminate, hard: kill). Never raises.
186
+
187
+ POSIX: SIGTERM/SIGKILL to the child's process group (it leads its own
188
+ via ``start_new_session=True``), reaching its descendants. Windows has
189
+ no ``signal.SIGKILL``/``os.killpg``/``os.getpgid``, so nothing
190
+ platform-specific is read unguarded: the hard kill terminates the Job Object
191
+ holding the whole tree (``taskkill /F /T`` if there is none), then ``proc.kill()``.
180
192
  """
181
- with contextlib.suppress(ProcessLookupError, PermissionError, OSError, AttributeError):
182
- pgid = os.getpgid(proc.pid)
183
- os.killpg(pgid, sig)
184
- return
185
- # Fallback: signal the process directly.
193
+ killpg = getattr(os, "killpg", None)
194
+ getpgid = getattr(os, "getpgid", None)
195
+ sig = getattr(signal, "SIGKILL" if hard else "SIGTERM", None)
196
+ if killpg is not None and getpgid is not None and sig is not None:
197
+ with contextlib.suppress(ProcessLookupError, PermissionError, OSError, AttributeError):
198
+ killpg(getpgid(proc.pid), sig)
199
+ return
200
+ if hard and sys.platform == "win32" and not (job is not None and _winjob.terminate(job)):
201
+ with contextlib.suppress(OSError, subprocess.SubprocessError):
202
+ subprocess.run(
203
+ ["taskkill", "/F", "/T", "/PID", str(proc.pid)],
204
+ stdout=subprocess.DEVNULL,
205
+ stderr=subprocess.DEVNULL,
206
+ timeout=5,
207
+ check=False,
208
+ )
186
209
  with contextlib.suppress(ProcessLookupError, OSError):
187
- if sig == signal.SIGKILL:
210
+ if hard:
188
211
  proc.kill()
189
212
  else:
190
213
  proc.terminate()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agent-workflow-sdk"
7
- version = "0.6.0"
7
+ version = "0.6.2"
8
8
  description = "Async-first SDK for orchestrating local coding-agent CLIs (Claude Code, Codex, Kiro) and LLMs as durable, graph-based workflows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -24,6 +24,10 @@ from __future__ import annotations
24
24
 
25
25
  import asyncio
26
26
  import json
27
+ import os
28
+ import signal
29
+ import subprocess
30
+ import sys
27
31
 
28
32
  import pytest
29
33
  from agentflow.backends.base import AllowAll, DenyAll
@@ -303,6 +307,119 @@ async def test_close_terminates_then_kills_on_timeout(fake_proc, monkeypatch):
303
307
  assert fake_proc.returncode == -9 # kill() was reached
304
308
 
305
309
 
310
+ async def test_close_on_windows_has_no_posix_signals(fake_proc, monkeypatch):
311
+ """Windows shape: no signal.SIGKILL / os.killpg / os.getpgid. close() must not
312
+ raise AttributeError and must reap the tree with taskkill, then kill."""
313
+ backend = KiroBackend("fake-agent", engine="v2", permission=AllowAll())
314
+ setup = asyncio.create_task(_drive_setup(fake_proc))
315
+ await backend.start()
316
+ await setup
317
+ fake_proc.pid = 4242
318
+
319
+ async def _never() -> int:
320
+ await asyncio.sleep(3600)
321
+ return 0
322
+
323
+ async def _fast_wait_for(coro, timeout):
324
+ coro.close()
325
+ raise TimeoutError
326
+
327
+ monkeypatch.setattr(fake_proc, "wait", _never)
328
+ monkeypatch.setattr(asyncio, "wait_for", _fast_wait_for)
329
+ monkeypatch.delattr(os, "killpg", raising=False)
330
+ monkeypatch.delattr(os, "getpgid", raising=False)
331
+ monkeypatch.delattr(signal, "SIGKILL", raising=False)
332
+ monkeypatch.setattr(sys, "platform", "win32")
333
+ ran: list[list[str]] = []
334
+ monkeypatch.setattr(subprocess, "run", lambda cmd, **kw: ran.append(cmd))
335
+
336
+ await backend.close()
337
+
338
+ assert ran == [["taskkill", "/F", "/T", "/PID", "4242"]]
339
+ assert fake_proc.returncode == -9
340
+
341
+
342
+ async def _windows_close_with_job(fake_proc, monkeypatch, *, job_terminates: bool):
343
+ """close() on 'Windows' that must escalate, with a Job Object already attached."""
344
+ from agentflow.backends import _winjob
345
+
346
+ backend = KiroBackend("fake-agent", engine="v2", permission=AllowAll())
347
+ setup = asyncio.create_task(_drive_setup(fake_proc))
348
+ await backend.start()
349
+ await setup
350
+ fake_proc.pid = 4242
351
+ backend._job = "JOB"
352
+
353
+ async def _never() -> int:
354
+ await asyncio.sleep(3600)
355
+ return 0
356
+
357
+ async def _fast_wait_for(coro, timeout):
358
+ coro.close()
359
+ raise TimeoutError
360
+
361
+ monkeypatch.setattr(fake_proc, "wait", _never)
362
+ monkeypatch.setattr(asyncio, "wait_for", _fast_wait_for)
363
+ monkeypatch.delattr(os, "killpg", raising=False)
364
+ monkeypatch.delattr(os, "getpgid", raising=False)
365
+ monkeypatch.setattr(sys, "platform", "win32")
366
+ events: list[str] = []
367
+ monkeypatch.setattr(
368
+ _winjob, "terminate", lambda job: events.append("job-terminate") or job_terminates
369
+ )
370
+ monkeypatch.setattr(_winjob, "close", lambda job: events.append("job-close"))
371
+ monkeypatch.setattr(subprocess, "run", lambda cmd, **kw: events.append("taskkill"))
372
+
373
+ await backend.close()
374
+ return events, backend
375
+
376
+
377
+ async def test_windows_start_puts_the_child_in_a_job(fake_proc, monkeypatch):
378
+ from agentflow.backends import _winjob
379
+
380
+ fake_proc.pid = 777
381
+ attached: list[int] = []
382
+ monkeypatch.setattr(sys, "platform", "win32")
383
+ monkeypatch.setattr(_winjob, "attach", lambda pid: attached.append(pid) or "JOB")
384
+ backend = KiroBackend("fake-agent", engine="v2", permission=AllowAll())
385
+ setup = asyncio.create_task(_drive_setup(fake_proc))
386
+ await backend.start()
387
+ await setup
388
+
389
+ assert attached == [777]
390
+ assert backend._job == "JOB"
391
+
392
+
393
+ async def test_windows_hard_kill_terminates_the_job_not_taskkill(fake_proc, monkeypatch):
394
+ events, backend = await _windows_close_with_job(fake_proc, monkeypatch, job_terminates=True)
395
+
396
+ assert events == ["job-terminate", "job-close"]
397
+ assert backend._job is None
398
+
399
+
400
+ async def test_windows_hard_kill_falls_back_to_taskkill_when_the_job_fails(fake_proc, monkeypatch):
401
+ events, _ = await _windows_close_with_job(fake_proc, monkeypatch, job_terminates=False)
402
+
403
+ assert events == ["job-terminate", "taskkill", "job-close"]
404
+
405
+
406
+ async def test_windows_clean_exit_still_releases_the_job(fake_proc, monkeypatch):
407
+ from agentflow.backends import _winjob
408
+
409
+ backend = KiroBackend("fake-agent", engine="v2", permission=AllowAll())
410
+ setup = asyncio.create_task(_drive_setup(fake_proc))
411
+ await backend.start()
412
+ await setup
413
+ backend._job = "JOB"
414
+ closed: list[str] = []
415
+ monkeypatch.setattr(_winjob, "close", lambda job: closed.append(job))
416
+
417
+ await backend.close() # the fake process exits at once: no escalation
418
+
419
+ assert closed == ["JOB"]
420
+ assert backend._job is None
421
+
422
+
306
423
  async def test_turn_rpc_failure_yields_error_event(fake_proc):
307
424
  backend = KiroBackend("fake-agent", engine="v2", permission=AllowAll())
308
425
  setup = asyncio.create_task(_drive_setup(fake_proc))
@@ -0,0 +1,83 @@
1
+ """Job Object wrapper, against a fake kernel32 (the real one exists only on Windows)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from agentflow.backends import _winjob
8
+
9
+
10
+ class _K32:
11
+ def __init__(self, *, fail: str | None = None) -> None:
12
+ self.fail = fail
13
+ self.calls: list[str] = []
14
+
15
+ def CreateJobObjectW(self, *a): # noqa: N802
16
+ self.calls.append("create")
17
+ return None if self.fail == "create" else 11
18
+
19
+ def SetInformationJobObject(self, job, cls, info, size): # noqa: N802
20
+ self.calls.append(f"set:{cls}:{info._obj.BasicLimitInformation.LimitFlags:#x}")
21
+ return 0 if self.fail == "set" else 1
22
+
23
+ def OpenProcess(self, access, inherit, pid): # noqa: N802
24
+ self.calls.append(f"open:{access:#x}:{pid}")
25
+ return None if self.fail == "open" else 22
26
+
27
+ def AssignProcessToJobObject(self, job, process): # noqa: N802
28
+ self.calls.append(f"assign:{job}:{process}")
29
+ return 0 if self.fail == "assign" else 1
30
+
31
+ def TerminateJobObject(self, job, code): # noqa: N802
32
+ self.calls.append(f"terminate:{job}")
33
+ return 1
34
+
35
+ def CloseHandle(self, handle): # noqa: N802
36
+ self.calls.append(f"close:{handle}")
37
+ return 1
38
+
39
+
40
+ def test_everything_is_a_noop_off_windows(monkeypatch):
41
+ monkeypatch.setattr(sys, "platform", "linux")
42
+ assert _winjob.attach(1) is None
43
+ assert _winjob.terminate(object()) is False
44
+ _winjob.close(object()) # must not raise
45
+
46
+
47
+ def test_attach_builds_a_kill_on_close_job_and_assigns_the_process(monkeypatch):
48
+ k32 = _K32()
49
+ monkeypatch.setattr(_winjob, "_kernel32", lambda: k32)
50
+
51
+ job = _winjob.attach(4242)
52
+
53
+ assert job == 11
54
+ assert k32.calls == [
55
+ "create",
56
+ "set:9:0x2000", # extended limits, KILL_ON_JOB_CLOSE
57
+ "open:0x101:4242", # SET_QUOTA | TERMINATE
58
+ "assign:11:22",
59
+ "close:22", # the process handle is released, the job handle is kept
60
+ ]
61
+
62
+
63
+ def test_attach_cleans_up_after_each_failure(monkeypatch):
64
+ for step, expected_tail in [
65
+ ("create", []),
66
+ ("set", ["close:11"]),
67
+ ("open", ["close:11"]),
68
+ ("assign", ["close:22", "close:11"]),
69
+ ]:
70
+ k32 = _K32(fail=step)
71
+ monkeypatch.setattr(_winjob, "_kernel32", lambda k=k32: k)
72
+ assert _winjob.attach(1) is None, step
73
+ assert k32.calls[len(k32.calls) - len(expected_tail) :] == expected_tail, step
74
+
75
+
76
+ def test_terminate_and_close_use_the_job_handle(monkeypatch):
77
+ k32 = _K32()
78
+ monkeypatch.setattr(_winjob, "_kernel32", lambda: k32)
79
+
80
+ assert _winjob.terminate(11) is True
81
+ _winjob.close(11)
82
+
83
+ assert k32.calls == ["terminate:11", "close:11"]