Skip to content

feat: an async client, and the lessons a turn was prepared with - #11

Merged
hugo8xx merged 2 commits into
mainfrom
feat/async-client
Aug 25, 2026
Merged

feat: an async client, and the lessons a turn was prepared with#11
hugo8xx merged 2 commits into
mainfrom
feat/async-client

Conversation

@hugo8xx

@hugo8xx hugo8xx commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Groundwork for a CrewAI package, and useful on its own.

Why

Every agent framework worth integrating is async. A blocking client on an event loop either stalls it or grows a thread pool to hide the stall — so each integration rewrites this transport instead of using one. Our own CrewAI runtime carries a 209-line copy for exactly that reason, and its docstring says so outright.

async with AsyncKhwan(api_key="kwk_live_xxx", core="acme", user_id="Web") as kw:
    turn   = await kw.prepare("what did we decide about billing?")
    answer = await your_model(turn.messages)
    await kw.record(turn, answer)

Behind an optional extra — pip install "khwan[async]" — so a sync user does not pay for httpx, and a missing import names the install that fixes it.

One rule set, not two

The retry policy moved to module level rather than being copied into the new client. A retry rule that drifts between two clients is a bug nobody notices until a duplicate record lands in production. Behaviour is unchanged; one test that reached for the old private method now asserts on the shared rule.

aclose() waits

asyncio keeps only a weak reference to a task, so a fire-and-forget record can be collected before it is sent — losing a turn silently, which is the one failure that mode must not have. Background writes are held and awaited on close, which async with does for you.

Turn.lessons

What synthesis distilled, next to the raw sources. Both were already inside messages, but a caller building its own context — a recall tool, a subagent brief — could not take the rules without replaying the whole prompt.

Tests

httpx driven through a MockTransport, so no network:

✓ policy: shared module-level rules, honoured by both clients
✓ loop: prepare → record, with core and sub-brain headers on both
✓ background: the write lands, and aclose() waits for it
✓ background swallows failure; blocking raises it
✓ retry: 503 retried, and the loop kept running while it waited
✓ missing httpx names the install that fixes it

The two existing suites still pass. Built and twine checked; verified that a
sync-only install (no httpx) imports fine and only fails — clearly — when
AsyncKhwan is actually constructed.

Version → 0.3.0. Not published — that is yours to run.

Every agent framework worth integrating is async. A blocking client on an event
loop either stalls it or grows a thread pool to hide the stall, so each
integration ends up rewriting this transport rather than using one — our own
CrewAI runtime carries a 209-line copy for exactly that reason.

AsyncKhwan is the same loop over httpx, behind an optional extra so a sync user
does not pay for it, and it says which install fixes the import when it is
missing. It holds one pool; aclose() waits for background records first, because
asyncio keeps only a weak reference to a task and a fire-and-forget write can
otherwise be collected before it is sent — losing a turn silently, which is the
one failure this mode must not have.

The retry rules moved to module level rather than being copied. A rule that
drifts between two clients is a bug nobody notices until a duplicate record
lands, so there is one copy and both use it. Behaviour is unchanged; one test
that reached for the old private method now asserts on the shared rule instead.

Turn.lessons exposes what synthesis distilled, next to the raw sources. Both were
already inside messages; a caller building its own context — a recall tool, a
subagent brief — could not take the rules without replaying the whole prompt.

Tests drive httpx through a MockTransport, so no network: the loop with scope
headers, a background write actually landing, failure swallowed there and raised
when blocking, and a 503 retry that yields to the loop rather than sleeping on
it.
This repository is public and always has been. The PyPI page carried Homepage and
Documentation and nothing else, so the page a developer actually lands on offered
no route to the code — and a reviewer looking for exactly this, to check what a
client that handles prompt data does with it, concluded the SDK was closed.

Repository renders as "Source" in the sidebar; Issues gives the same reader
somewhere to go.
@hugo8xx
hugo8xx merged commit c59b3d1 into main Aug 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant