I think I've figured it out in a simple test program
Firstly, I've got a base class for the NPC's like this:
EDIT: Updated NpcBase to use TaskCompletionSource:
public class NpcBase
{
    // Derived classes to call this when starting an async operation
    public Task BeginTask()
    {
        // Task already running?
        if (_tcs!= null)
        {
            throw new InvalidOperationException("busy");
        }
        _tcs = new TaskCompletionSource<int>();
        return _tcs.Task;
    }
    TaskCompletionSource<int> _tcs;
    // Derived class calls this when async operation complete
    public void EndTask()
    {
        if (_tcs != null)
        {
            var temp = _tcs;
            _tcs = null;
            temp.SetResult(0);
        }
    }
    // Is this NPC currently busy?
    public bool IsBusy
    {
        get
        {
            return _tcs != null;
        }
    }
}
For reference, here's the old version of NpcBase with custom IAsyncResult implementation instead of TaskCompletionSource:
// DONT USE THIS, OLD VERSION FOR REFERENCE ONLY
public class NpcBase
{
    // Derived classes to call this when starting an async operation
    public Task BeginTask()
    {
        // Task already running?
        if (_result != null)
        {
            throw new InvalidOperationException("busy");
        }
        // Create the async Task
        return Task.Factory.FromAsync(
            // begin method
            (ac, o) =>
            {
                return _result = new Result(ac, o);
            },
            // End method
            (r) =>
            {
            },
            // State object
            null
            );
    }
    // Derived class calls this when async operation complete
    public void EndTask()
    {
        if (_result != null)
        {
            var temp = _result;
            _result = null;
            temp.Finish();
        }
    }
    // Is this NPC currently busy?
    public bool IsBusy
    {
        get
        {
            return _result != null;
        }
    }
    // Result object for the current task
    private Result _result;
    // Simple AsyncResult class that stores the callback and the state object
    class Result : IAsyncResult
    {
        public Result(AsyncCallback callback, object AsyncState)
        {
            _callback = callback;
            _state = AsyncState;
        }
        private AsyncCallback _callback;
        private object _state;
        public object AsyncState
        {
            get { return _state; ; }
        }
        public System.Threading.WaitHandle AsyncWaitHandle
        {
            get { throw new NotImplementedException(); }
        }
        public bool CompletedSynchronously
        {
            get { return false; }
        }
        public bool IsCompleted
        {
            get { return _finished; }
        }
        public void Finish()
        {
            _finished = true;
            if (_callback != null)
                _callback(this);
        }
        bool _finished;
    }
}
Next, I've got a simple "NPC" that moves in one dimension.  When a moveTo operation starts it calls BeginTask in the NpcBase.  When arrived at the destination, it calls EndTask().
public class NpcTest : NpcBase
{
    public NpcTest()
    {
        _position = 0;
        _target = 0;
    }
    // Async operation to count
    public Task MoveTo(int newPosition)
    {
        // Store new target
        _target = newPosition;
        return BeginTask();
    }
    public int Position
    {
        get
        {
            return _position;
        }
    }
    public void onFrame()
    {
        if (_position == _target)
        {
            EndTask();
        }
        else if (_position < _target)
        {
            _position++;
        }
        else
        {
            _position--;
        }
    }
    private int _position;
    private int _target;
}
And finally, a simple WinForms app to drive it.  It consists of a button and two labels.  Clicking the button starts both NPC and their position is displayed on the labels.
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
    private void onButtonClick(object sender, EventArgs e)
    {
        RunNpc1();
        RunNpc2();
    }
    public async void RunNpc1()
    {
        while (true)
        {
            await _npc1.MoveTo(20);
            await _npc1.MoveTo(10);
        }
    }
    public async void RunNpc2()
    {
        while (true)
        {
            await _npc2.MoveTo(80);
            await _npc2.MoveTo(70);
        }
    }
    NpcTest _npc1 = new NpcTest();
    NpcTest _npc2 = new NpcTest();
    private void timer1_Tick(object sender, EventArgs e)
    {
        _npc1.onFrame();
        _npc2.onFrame();
        label1.Text = _npc1.Position.ToString();
        label2.Text = _npc2.Position.ToString();
    }
}
And it works, all seems to be running on the main UI thread... which is what I wanted.
Of course it needs to be fixed to handle cancelling of operations, exceptions etc... but the basic idea is there.