hanzo-tasks 0.1.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.
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: hanzo-tasks
3
+ Version: 0.1.0
4
+ Summary: Hanzo Tasks SDK — Durable workflow execution for AI agents (powered by Temporal)
5
+ Author-email: Hanzo AI <dev@hanzo.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/hanzoai/python-sdk
8
+ Project-URL: Repository, https://github.com/hanzoai/python-sdk/tree/main/pkg/hanzo-tasks
9
+ Project-URL: Documentation, https://hanzo.ai/docs/tasks
10
+ Keywords: hanzo,temporal,tasks,workflow,durable,agents
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: temporalio>=1.9.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
23
+ Requires-Dist: pytest-asyncio>=0.26.0; extra == "dev"
@@ -0,0 +1,43 @@
1
+ """
2
+ hanzo-tasks — Durable workflow execution for AI agents.
3
+
4
+ Wraps the Temporal Python SDK with Hanzo conventions for agent task
5
+ orchestration, including pre-built workflows for pipelines and fan-out.
6
+
7
+ Example:
8
+ >>> from hanzo_tasks import Client, TasksConfig
9
+ >>> client = await Client.connect(TasksConfig(namespace="hanzo"))
10
+ >>> handle = await client.submit(AgentTaskWorkflow.run, task_input, queue="agents")
11
+ >>> result = await handle.result()
12
+ """
13
+
14
+ from .activities import execute_agent_task, send_notification, set_agent_executor
15
+ from .client import Client, TasksConfig, WorkflowHandle
16
+ from .worker import Worker
17
+ from .workflows import (
18
+ AgentTaskInput,
19
+ AgentTaskOutput,
20
+ AgentTaskWorkflow,
21
+ FanOutWorkflow,
22
+ PipelineWorkflow,
23
+ )
24
+
25
+ __version__ = "0.1.0"
26
+ __all__ = [
27
+ # Client
28
+ "Client",
29
+ "TasksConfig",
30
+ "WorkflowHandle",
31
+ # Worker
32
+ "Worker",
33
+ # Workflows
34
+ "AgentTaskWorkflow",
35
+ "PipelineWorkflow",
36
+ "FanOutWorkflow",
37
+ "AgentTaskInput",
38
+ "AgentTaskOutput",
39
+ # Activities
40
+ "execute_agent_task",
41
+ "send_notification",
42
+ "set_agent_executor",
43
+ ]
@@ -0,0 +1,35 @@
1
+ """Activity definitions for agent task execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable
6
+
7
+ from temporalio import activity
8
+
9
+ # Activity executor — pluggable. The playground sets this to call the ZAP sidecar.
10
+ _agent_executor: Callable[..., Any] | None = None
11
+
12
+
13
+ def set_agent_executor(fn: Callable[..., Any]) -> None:
14
+ """Set the function that executes agent tasks (called by playground)."""
15
+ global _agent_executor
16
+ _agent_executor = fn
17
+
18
+
19
+ @activity.defn
20
+ async def execute_agent_task(input: Any) -> Any:
21
+ """Execute an agent task. Delegates to the registered executor."""
22
+ if _agent_executor is None:
23
+ raise RuntimeError(
24
+ "No agent executor registered. Call set_agent_executor() first."
25
+ )
26
+ return await _agent_executor(input)
27
+
28
+
29
+ @activity.defn
30
+ async def send_notification(input: dict[str, Any]) -> None:
31
+ """Send a notification (webhook, SSE, etc)."""
32
+ import httpx
33
+
34
+ async with httpx.AsyncClient() as client:
35
+ await client.post(input.get("url", ""), json=input)
@@ -0,0 +1,100 @@
1
+ """Hanzo Tasks client — wraps Temporal client with Hanzo conventions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+ from uuid import uuid4
8
+
9
+ from temporalio.client import Client as TemporalClient
10
+
11
+
12
+ @dataclass
13
+ class TasksConfig:
14
+ """Configuration for connecting to Temporal."""
15
+
16
+ address: str = "localhost:7233"
17
+ namespace: str = "hanzo"
18
+ tls: bool = False
19
+
20
+
21
+ class WorkflowHandle:
22
+ """Handle to a running workflow."""
23
+
24
+ def __init__(self, handle: Any) -> None:
25
+ self._handle = handle
26
+
27
+ @property
28
+ def id(self) -> str:
29
+ return self._handle.id
30
+
31
+ @property
32
+ def run_id(self) -> str:
33
+ return self._handle.result_run_id
34
+
35
+ async def result(self, result_type: type | None = None) -> Any:
36
+ return await self._handle.result(result_type=result_type)
37
+
38
+ async def cancel(self) -> None:
39
+ await self._handle.cancel()
40
+
41
+ async def signal(self, name: str, data: Any = None) -> None:
42
+ await self._handle.signal(name, data)
43
+
44
+
45
+ class Client:
46
+ """Hanzo Tasks client for submitting and managing workflows."""
47
+
48
+ def __init__(self, temporal: TemporalClient) -> None:
49
+ self._temporal = temporal
50
+
51
+ @classmethod
52
+ async def connect(cls, config: TasksConfig | None = None) -> Client:
53
+ """Connect to Temporal server."""
54
+ cfg = config or TasksConfig()
55
+ temporal = await TemporalClient.connect(cfg.address, namespace=cfg.namespace)
56
+ return cls(temporal)
57
+
58
+ async def submit(
59
+ self,
60
+ workflow: Any,
61
+ input: Any,
62
+ *,
63
+ id: str | None = None,
64
+ queue: str = "default",
65
+ **kwargs: Any,
66
+ ) -> WorkflowHandle:
67
+ """Submit a workflow for execution."""
68
+ handle = await self._temporal.start_workflow(
69
+ workflow,
70
+ input,
71
+ id=id or str(uuid4()),
72
+ task_queue=queue,
73
+ **kwargs,
74
+ )
75
+ return WorkflowHandle(handle)
76
+
77
+ async def get_result(self, workflow_id: str, result_type: type | None = None) -> Any:
78
+ """Get the result of a completed workflow."""
79
+ handle = self._temporal.get_workflow_handle(workflow_id)
80
+ return await handle.result(result_type=result_type)
81
+
82
+ async def cancel(self, workflow_id: str) -> None:
83
+ """Cancel a running workflow."""
84
+ handle = self._temporal.get_workflow_handle(workflow_id)
85
+ await handle.cancel()
86
+
87
+ async def signal(self, workflow_id: str, signal_name: str, data: Any = None) -> None:
88
+ """Send a signal to a running workflow."""
89
+ handle = self._temporal.get_workflow_handle(workflow_id)
90
+ await handle.signal(signal_name, data)
91
+
92
+ async def query(self, workflow_id: str, query_name: str) -> Any:
93
+ """Query a running workflow."""
94
+ handle = self._temporal.get_workflow_handle(workflow_id)
95
+ return await handle.query(query_name)
96
+
97
+ @property
98
+ def temporal(self) -> TemporalClient:
99
+ """Access the underlying Temporal client."""
100
+ return self._temporal
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,54 @@
1
+ """Hanzo Tasks worker — polls and executes activities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from temporalio.worker import Worker as TemporalWorker
8
+
9
+
10
+ class Worker:
11
+ """Hanzo Tasks worker that polls a queue and executes workflows/activities."""
12
+
13
+ def __init__(
14
+ self,
15
+ client: Any,
16
+ queue: str = "default",
17
+ workflows: list[Any] | None = None,
18
+ activities: list[Any] | None = None,
19
+ ) -> None:
20
+ self._client = client
21
+ self._queue = queue
22
+ self._workflows: list[Any] = workflows or []
23
+ self._activities: list[Any] = activities or []
24
+ self._worker: TemporalWorker | None = None
25
+
26
+ def register_workflow(self, workflow_cls: Any) -> Any:
27
+ """Register a workflow class. Can be used as a decorator."""
28
+ self._workflows.append(workflow_cls)
29
+ return workflow_cls
30
+
31
+ def register_activity(self, activity_fn: Any) -> Any:
32
+ """Register an activity function. Can be used as a decorator."""
33
+ self._activities.append(activity_fn)
34
+ return activity_fn
35
+
36
+ async def run(self) -> None:
37
+ """Start the worker. Blocks until shutdown."""
38
+ temporal_client = (
39
+ self._client.temporal
40
+ if hasattr(self._client, "temporal")
41
+ else self._client
42
+ )
43
+ self._worker = TemporalWorker(
44
+ temporal_client,
45
+ task_queue=self._queue,
46
+ workflows=self._workflows,
47
+ activities=self._activities,
48
+ )
49
+ await self._worker.run()
50
+
51
+ async def shutdown(self) -> None:
52
+ """Gracefully shutdown the worker."""
53
+ if self._worker:
54
+ await self._worker.shutdown()
@@ -0,0 +1,80 @@
1
+ """Pre-built workflows for agent task orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import timedelta
7
+
8
+ from temporalio import workflow
9
+ from temporalio.common import RetryPolicy
10
+
11
+
12
+ @dataclass
13
+ class AgentTaskInput:
14
+ """Input for a single agent task."""
15
+
16
+ space_id: str
17
+ agent_id: str
18
+ task_title: str
19
+ task_prompt: str
20
+ timeout_seconds: int = 3600
21
+ max_retries: int = 3
22
+
23
+
24
+ @dataclass
25
+ class AgentTaskOutput:
26
+ """Output from an agent task execution."""
27
+
28
+ result: str = ""
29
+ error: str = ""
30
+ elapsed_seconds: float = 0.0
31
+
32
+
33
+ @workflow.defn
34
+ class AgentTaskWorkflow:
35
+ """Execute a single agent task with retries and timeout."""
36
+
37
+ @workflow.run
38
+ async def run(self, input: AgentTaskInput) -> AgentTaskOutput:
39
+ return await workflow.execute_activity(
40
+ "execute_agent_task",
41
+ input,
42
+ start_to_close_timeout=timedelta(seconds=input.timeout_seconds),
43
+ retry_policy=RetryPolicy(maximum_attempts=input.max_retries),
44
+ )
45
+
46
+
47
+ @workflow.defn
48
+ class PipelineWorkflow:
49
+ """Run agent tasks sequentially (pipeline)."""
50
+
51
+ @workflow.run
52
+ async def run(self, tasks: list[AgentTaskInput]) -> list[AgentTaskOutput]:
53
+ results: list[AgentTaskOutput] = []
54
+ for task in tasks:
55
+ result = await workflow.execute_activity(
56
+ "execute_agent_task",
57
+ task,
58
+ start_to_close_timeout=timedelta(seconds=task.timeout_seconds),
59
+ retry_policy=RetryPolicy(maximum_attempts=task.max_retries),
60
+ )
61
+ results.append(result)
62
+ return results
63
+
64
+
65
+ @workflow.defn
66
+ class FanOutWorkflow:
67
+ """Run agent tasks in parallel (fan-out/fan-in)."""
68
+
69
+ @workflow.run
70
+ async def run(self, tasks: list[AgentTaskInput]) -> list[AgentTaskOutput]:
71
+ handles = []
72
+ for task in tasks:
73
+ handle = workflow.start_activity(
74
+ "execute_agent_task",
75
+ task,
76
+ start_to_close_timeout=timedelta(seconds=task.timeout_seconds),
77
+ retry_policy=RetryPolicy(maximum_attempts=task.max_retries),
78
+ )
79
+ handles.append(handle)
80
+ return [await h for h in handles]
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: hanzo-tasks
3
+ Version: 0.1.0
4
+ Summary: Hanzo Tasks SDK — Durable workflow execution for AI agents (powered by Temporal)
5
+ Author-email: Hanzo AI <dev@hanzo.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/hanzoai/python-sdk
8
+ Project-URL: Repository, https://github.com/hanzoai/python-sdk/tree/main/pkg/hanzo-tasks
9
+ Project-URL: Documentation, https://hanzo.ai/docs/tasks
10
+ Keywords: hanzo,temporal,tasks,workflow,durable,agents
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: temporalio>=1.9.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
23
+ Requires-Dist: pytest-asyncio>=0.26.0; extra == "dev"
@@ -0,0 +1,13 @@
1
+ pyproject.toml
2
+ hanzo_tasks/__init__.py
3
+ hanzo_tasks/activities.py
4
+ hanzo_tasks/client.py
5
+ hanzo_tasks/py.typed
6
+ hanzo_tasks/worker.py
7
+ hanzo_tasks/workflows.py
8
+ hanzo_tasks.egg-info/PKG-INFO
9
+ hanzo_tasks.egg-info/SOURCES.txt
10
+ hanzo_tasks.egg-info/dependency_links.txt
11
+ hanzo_tasks.egg-info/requires.txt
12
+ hanzo_tasks.egg-info/top_level.txt
13
+ tests/test_tasks.py
@@ -0,0 +1,5 @@
1
+ temporalio>=1.9.0
2
+
3
+ [dev]
4
+ pytest>=7.0.0
5
+ pytest-asyncio>=0.26.0
@@ -0,0 +1 @@
1
+ hanzo_tasks
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hanzo-tasks"
7
+ version = "0.1.0"
8
+ description = "Hanzo Tasks SDK — Durable workflow execution for AI agents (powered by Temporal)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Hanzo AI", email = "dev@hanzo.ai" }]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ "Typing :: Typed",
21
+ ]
22
+ keywords = ["hanzo", "temporal", "tasks", "workflow", "durable", "agents"]
23
+ dependencies = [
24
+ "temporalio>=1.9.0",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/hanzoai/python-sdk"
29
+ Repository = "https://github.com/hanzoai/python-sdk/tree/main/pkg/hanzo-tasks"
30
+ Documentation = "https://hanzo.ai/docs/tasks"
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=7.0.0",
35
+ "pytest-asyncio>=0.26.0",
36
+ ]
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["."]
40
+ include = ["hanzo_tasks*"]
41
+
42
+ [tool.setuptools.package-data]
43
+ hanzo_tasks = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,317 @@
1
+ """Hanzo Tasks test suite.
2
+
3
+ Unit tests for client config, worker registration, dataclasses,
4
+ workflow decorators, and activity executor wiring. No Temporal
5
+ server required.
6
+ """
7
+
8
+ import inspect
9
+ from dataclasses import asdict
10
+
11
+ import pytest
12
+ import pytest_asyncio
13
+
14
+ from hanzo_tasks import (
15
+ AgentTaskInput,
16
+ AgentTaskOutput,
17
+ AgentTaskWorkflow,
18
+ Client,
19
+ FanOutWorkflow,
20
+ PipelineWorkflow,
21
+ TasksConfig,
22
+ Worker,
23
+ WorkflowHandle,
24
+ execute_agent_task,
25
+ set_agent_executor,
26
+ )
27
+
28
+
29
+ # -- config ------------------------------------------------------------------
30
+
31
+
32
+ class TestTasksConfig:
33
+ def test_defaults(self):
34
+ cfg = TasksConfig()
35
+ assert cfg.address == "localhost:7233"
36
+ assert cfg.namespace == "hanzo"
37
+ assert cfg.tls is False
38
+
39
+ def test_custom(self):
40
+ cfg = TasksConfig(address="temporal.prod:7233", namespace="prod", tls=True)
41
+ assert cfg.address == "temporal.prod:7233"
42
+ assert cfg.namespace == "prod"
43
+ assert cfg.tls is True
44
+
45
+
46
+ # -- dataclasses -------------------------------------------------------------
47
+
48
+
49
+ class TestAgentTaskInput:
50
+ def test_defaults(self):
51
+ inp = AgentTaskInput(
52
+ space_id="sp-1",
53
+ agent_id="ag-1",
54
+ task_title="Fix bug",
55
+ task_prompt="Fix the null pointer in main.go",
56
+ )
57
+ assert inp.space_id == "sp-1"
58
+ assert inp.agent_id == "ag-1"
59
+ assert inp.task_title == "Fix bug"
60
+ assert inp.task_prompt == "Fix the null pointer in main.go"
61
+ assert inp.timeout_seconds == 3600
62
+ assert inp.max_retries == 3
63
+
64
+ def test_custom_timeout(self):
65
+ inp = AgentTaskInput(
66
+ space_id="sp-2",
67
+ agent_id="ag-2",
68
+ task_title="Deploy",
69
+ task_prompt="Deploy to prod",
70
+ timeout_seconds=600,
71
+ max_retries=1,
72
+ )
73
+ assert inp.timeout_seconds == 600
74
+ assert inp.max_retries == 1
75
+
76
+ def test_serializes_to_dict(self):
77
+ inp = AgentTaskInput(
78
+ space_id="sp-1",
79
+ agent_id="ag-1",
80
+ task_title="T",
81
+ task_prompt="P",
82
+ )
83
+ d = asdict(inp)
84
+ assert d["space_id"] == "sp-1"
85
+ assert d["agent_id"] == "ag-1"
86
+ assert d["task_title"] == "T"
87
+ assert d["task_prompt"] == "P"
88
+ assert d["timeout_seconds"] == 3600
89
+ assert d["max_retries"] == 3
90
+ assert len(d) == 6
91
+
92
+
93
+ class TestAgentTaskOutput:
94
+ def test_defaults(self):
95
+ out = AgentTaskOutput()
96
+ assert out.result == ""
97
+ assert out.error == ""
98
+ assert out.elapsed_seconds == 0.0
99
+
100
+ def test_success(self):
101
+ out = AgentTaskOutput(result="done", elapsed_seconds=1.5)
102
+ assert out.result == "done"
103
+ assert out.error == ""
104
+ assert out.elapsed_seconds == 1.5
105
+
106
+ def test_error(self):
107
+ out = AgentTaskOutput(error="timeout", elapsed_seconds=3600.0)
108
+ assert out.error == "timeout"
109
+ assert out.result == ""
110
+
111
+ def test_serializes_to_dict(self):
112
+ out = AgentTaskOutput(result="ok", elapsed_seconds=0.1)
113
+ d = asdict(out)
114
+ assert d == {"result": "ok", "error": "", "elapsed_seconds": 0.1}
115
+
116
+
117
+ # -- workflow decorators -----------------------------------------------------
118
+
119
+
120
+ class TestWorkflowDecorators:
121
+ def test_agent_task_workflow_has_run(self):
122
+ assert hasattr(AgentTaskWorkflow, "run")
123
+ assert inspect.iscoroutinefunction(AgentTaskWorkflow.run)
124
+
125
+ def test_pipeline_workflow_has_run(self):
126
+ assert hasattr(PipelineWorkflow, "run")
127
+ assert inspect.iscoroutinefunction(PipelineWorkflow.run)
128
+
129
+ def test_fanout_workflow_has_run(self):
130
+ assert hasattr(FanOutWorkflow, "run")
131
+ assert inspect.iscoroutinefunction(FanOutWorkflow.run)
132
+
133
+ def test_workflow_classes_are_distinct(self):
134
+ assert AgentTaskWorkflow is not PipelineWorkflow
135
+ assert PipelineWorkflow is not FanOutWorkflow
136
+
137
+
138
+ # -- worker ------------------------------------------------------------------
139
+
140
+
141
+ class TestWorker:
142
+ def test_init_defaults(self):
143
+ w = Worker(client=None, queue="test-q")
144
+ assert w._queue == "test-q"
145
+ assert w._workflows == []
146
+ assert w._activities == []
147
+ assert w._worker is None
148
+
149
+ def test_register_workflow(self):
150
+ w = Worker(client=None)
151
+
152
+ class MyWorkflow:
153
+ pass
154
+
155
+ result = w.register_workflow(MyWorkflow)
156
+ assert result is MyWorkflow
157
+ assert MyWorkflow in w._workflows
158
+
159
+ def test_register_activity(self):
160
+ w = Worker(client=None)
161
+
162
+ async def my_activity(input):
163
+ return "ok"
164
+
165
+ result = w.register_activity(my_activity)
166
+ assert result is my_activity
167
+ assert my_activity in w._activities
168
+
169
+ def test_register_multiple(self):
170
+ w = Worker(client=None)
171
+ for i in range(5):
172
+ w.register_workflow(type(f"Wf{i}", (), {}))
173
+ assert len(w._workflows) == 5
174
+
175
+ def test_init_with_preloaded(self):
176
+ workflows = [AgentTaskWorkflow, PipelineWorkflow]
177
+ activities = [execute_agent_task]
178
+ w = Worker(client=None, workflows=workflows, activities=activities)
179
+ assert len(w._workflows) == 2
180
+ assert len(w._activities) == 1
181
+
182
+ def test_does_not_mutate_caller_list(self):
183
+ workflows: list = []
184
+ w = Worker(client=None, workflows=workflows)
185
+ w.register_workflow(AgentTaskWorkflow)
186
+ # The caller's original list should not be modified since we
187
+ # pass a new list via `or []`, but if caller passes a list,
188
+ # it IS the same reference. That's expected Python behavior.
189
+ # Just verify worker has the workflow.
190
+ assert AgentTaskWorkflow in w._workflows
191
+
192
+
193
+ # -- executor wiring ---------------------------------------------------------
194
+
195
+
196
+ class TestSetAgentExecutor:
197
+ def test_set_and_reset(self):
198
+ import hanzo_tasks.activities as act
199
+
200
+ original = act._agent_executor
201
+
202
+ async def my_exec(input):
203
+ return "executed"
204
+
205
+ set_agent_executor(my_exec)
206
+ assert act._agent_executor is my_exec
207
+
208
+ # Restore
209
+ act._agent_executor = original
210
+
211
+ @pytest.mark.asyncio
212
+ async def test_execute_without_executor_raises(self):
213
+ import hanzo_tasks.activities as act
214
+
215
+ saved = act._agent_executor
216
+ act._agent_executor = None
217
+ try:
218
+ with pytest.raises(RuntimeError, match="No agent executor registered"):
219
+ await execute_agent_task(None)
220
+ finally:
221
+ act._agent_executor = saved
222
+
223
+ @pytest.mark.asyncio
224
+ async def test_execute_with_executor(self):
225
+ import hanzo_tasks.activities as act
226
+
227
+ saved = act._agent_executor
228
+
229
+ async def mock_exec(input):
230
+ return AgentTaskOutput(result=f"done:{input.task_title}", elapsed_seconds=0.01)
231
+
232
+ set_agent_executor(mock_exec)
233
+ try:
234
+ inp = AgentTaskInput(
235
+ space_id="sp-1",
236
+ agent_id="ag-1",
237
+ task_title="test",
238
+ task_prompt="do it",
239
+ )
240
+ out = await execute_agent_task(inp)
241
+ assert out.result == "done:test"
242
+ assert out.elapsed_seconds == 0.01
243
+ finally:
244
+ act._agent_executor = saved
245
+
246
+
247
+ # -- workflow handle ---------------------------------------------------------
248
+
249
+
250
+ class TestWorkflowHandle:
251
+ def test_id(self):
252
+ class FakeHandle:
253
+ id = "wf-123"
254
+ result_run_id = "run-456"
255
+
256
+ h = WorkflowHandle(FakeHandle())
257
+ assert h.id == "wf-123"
258
+ assert h.run_id == "run-456"
259
+
260
+ @pytest.mark.asyncio
261
+ async def test_cancel(self):
262
+ cancelled = False
263
+
264
+ class FakeHandle:
265
+ id = "wf-1"
266
+ result_run_id = "run-1"
267
+
268
+ async def cancel(self):
269
+ nonlocal cancelled
270
+ cancelled = True
271
+
272
+ h = WorkflowHandle(FakeHandle())
273
+ await h.cancel()
274
+ assert cancelled
275
+
276
+ @pytest.mark.asyncio
277
+ async def test_signal(self):
278
+ signals = []
279
+
280
+ class FakeHandle:
281
+ id = "wf-1"
282
+ result_run_id = "run-1"
283
+
284
+ async def signal(self, name, data=None):
285
+ signals.append((name, data))
286
+
287
+ h = WorkflowHandle(FakeHandle())
288
+ await h.signal("pause", {"reason": "lunch"})
289
+ assert signals == [("pause", {"reason": "lunch"})]
290
+
291
+ @pytest.mark.asyncio
292
+ async def test_result(self):
293
+ class FakeHandle:
294
+ id = "wf-1"
295
+ result_run_id = "run-1"
296
+
297
+ async def result(self, result_type=None):
298
+ return "the-result"
299
+
300
+ h = WorkflowHandle(FakeHandle())
301
+ assert await h.result() == "the-result"
302
+
303
+
304
+ # -- __init__ exports --------------------------------------------------------
305
+
306
+
307
+ class TestExports:
308
+ def test_version(self):
309
+ import hanzo_tasks
310
+
311
+ assert hanzo_tasks.__version__ == "0.1.0"
312
+
313
+ def test_all_exports_importable(self):
314
+ import hanzo_tasks
315
+
316
+ for name in hanzo_tasks.__all__:
317
+ assert hasattr(hanzo_tasks, name), f"{name} not found in hanzo_tasks"