I haven't used tasks heavily yet so I'm not sure how to implement everything in your example. But I believe it's all possible.
Async/await is "just" syntactic sugar for tasks. So I'm not sure what you mean by "still blocking from the perspective of the caller". When you call an async method, you get back a Task. When you use await on a task, the compiler rewrites your code to introduce continuations. If you need some more powerful Task features hidden by the syntactic sugar, you always still have the option of using tasks explicitly.