Skip to content

PR-B4 (ADR 0008 Phase B): Python SDK — kakeya.Client + kakeya.Session - #47

Merged
FluffyAIcode merged 3 commits into
mainfrom
AgentMemory/v030-pr-b4-python-sdk-8e7f
Jun 1, 2026
Merged

PR-B4 (ADR 0008 Phase B): Python SDK — kakeya.Client + kakeya.Session#47
FluffyAIcode merged 3 commits into
mainfrom
AgentMemory/v030-pr-b4-python-sdk-8e7f

Conversation

@FluffyAIcode

Copy link
Copy Markdown
Owner

⚠️ Stacked PR

Depends on PR #46 (PR-B3). The diff against main shows both
PRs; once #46 merges, this auto-rebases to a clean PR-B4-only
diff.

What

Second-to-last PR of ADR 0008 Phase B. Ships
sdks/python/kakeya/ — the Python SDK matching the ADR §3.1
example exactly:

from kakeya import Client

with Client("localhost:50051") as client:
    with client.create_session(eos_token_ids=[151645]) as session:
        session.append([10, 20, 30])
        for token_id in session.generate(max_tokens=64):
            print(token_id)

Sync, not async

PR-B4 ships sync only (grpc.insecure_channel + sync stubs).
Three reasons:

  1. The ADR §3.1 example is sync.
  2. v0.3 target audience (REPL, scripts, agent harnesses on a
    single Mac) prefers sync — async adds friction without payoff
    under max_concurrent=1.
  3. Async can be added later (AsyncClient / AsyncSession)
    without breaking this surface; the runtime is wire-compatible.

Tokenization is NOT in the SDK

Per ADR §2.4 / §3.4: the runtime treats token ids as opaque
integers; rendering messages → tokens is the application's
responsibility. The SDK ships zero chat-template logic.
sdks/python/examples/ (a future PR) will demonstrate the
apply_chat_template pattern as opt-in usage.

Surface

Module Symbol Role
kakeya.client Client Connection to the runtime; create_session / close / context manager
kakeya.session Session One server-side session; append / generate / info / close / context manager
kakeya.session SessionInfo Read-only snapshot returned by Session.info()
kakeya.errors KakeyaError (+6 subclasses) Typed exception hierarchy mapping every gRPC status

Error mapping (per ADR §2.6 / §2.10, no graceful degradation):

gRPC StatusCode SDK exception
NOT_FOUND SessionNotFoundError
INVALID_ARGUMENT InvalidArgumentError
FAILED_PRECONDITION InvariantViolationError
RESOURCE_EXHAUSTED ResourceExhaustedError
UNIMPLEMENTED UnimplementedError
CANCELLED RpcCancelledError
(unknown) KakeyaError (base, with .rpc_code)

SessionClosedError is client-side only (raised when methods are
called on a closed Session instance) and never crosses the wire.

Tests (tests/sdk/python/)

File Tests Focus
test_errors.py 20 Every status → typed-exception mapping; .rpc_code carry; unknown-status fallback.
test_client.py 13 Construction, properties, idempotent close, context manager, create_session with all argument combinations, end-to-end RESOURCE_EXHAUSTEDResourceExhaustedError.
test_session.py 33 All 5 RPC methods, all error paths, last_* metadata fields, HistoryTruncated.last_history_truncated_dropped, greedy-only contract via InvalidArgumentError, double-close idempotency, SessionInfo standalone surface.
Total 66 All against a real grpc.aio server in a background thread (see conftest.py).

The fixture's notable choices (worth one read by reviewers):

  • The async server is constructed inside the worker thread's
    event loop, not the main thread. Constructing in the main thread
    and using in another thread's loop produces grpcio's "Future
    attached to a different loop" errors.
  • Shutdown uses server.wait_for_termination() rather than
    cancelling tasks. wait_for_termination() returns cleanly once
    server.stop() is invoked, which lets run_until_complete()
    exit without a cancellation tantrum.

Files

Status Path Notes
add sdks/python/kakeya/__init__.py Public API entry point (re-exports).
add sdks/python/kakeya/client.py (~130 lines) Client.
add sdks/python/kakeya/session.py (~300 lines) Session + SessionInfo.
add sdks/python/kakeya/errors.py (~130 lines) Typed exception hierarchy + _wrap_grpc_error.
add tests/sdk/python/conftest.py (~200 lines) Background-thread async-server fixture.
add tests/sdk/python/test_*.py (66 tests)
add scripts/review_pr_b4_on_mac.sh (Mac reviewer aid).
mod .github/workflows/ci.yaml PYTHONPATH=.:sdks/python; tests/sdk/python/; --cov=kakeya; import smoke for kakeya / client / session / errors.

Linux verification

PYTHONPATH=.:sdks/python pytest <Linux CI gate set> --cov-fail-under=100
695 passed (was 629 + 66 new), coverage 100.00 % on 1694 stmts

bash scripts/review_pr_b4_on_mac.sh
  SDK tests       : 66 tests, cov=173/173=100%
  Runtime smoke   : 10/10
  Appender smoke  : 10/10
  Generator smoke : 10/10

Per ADR 0008 §9

Linux-only-path carve-out invoked: zero MLX runtime code; the SDK
is pure Python over grpcio (sync). The review_pr_b4_on_mac.sh
runner is a review affordance for users who want to see the SDK
work on Apple Silicon end-to-end, but is not a §9 mandatory
report.

The next PR with mandatory §9 Mac M4 testing is PR-B5
(TypeScript SDK) — but that one is also platform-neutral; the
mandatory §9 gate doesn't return until v0.4 multi-tenant or any
new MLX-touching code lands.

Reviewer checklist

  • Public API matches ADR §3.1 example exactly: from kakeya import Client; with Client(...) as client: with client.create_session() as s: ... works as documented.
  • No tokenization in SDK core (grep -ri "apply_chat_template\|tokenizer" sdks/python/kakeya/ returns no production hits, only docstrings).
  • Error mapping is exhaustive: every documented gRPC status in _CODE_TO_EXCEPTION is exercised by test_errors.py::TestWrapGrpcError.
  • Streaming semantics: session.generate(...) is a sync generator; last_* metadata populated only after iteration completes.
  • Context manager pair: Client.__exit__ closes channel; Session.__exit__ closes session.
  • Local close-tracking distinct from runtime state: SessionClosedError (local) vs. SessionNotFoundError (runtime); test_session.py exercises both.
  • No new MLX runtime paths (git diff main inference_engine/backends/).
  • PYTHONPATH layout documented in CI yaml comments.

Next PR

PR-B5: TypeScript SDK at sdks/typescript/ — Node.js 20+ /
Electron 30+ / Bun 1.1+. Generates stubs from
proto/kakeya/v1/runtime.proto via protoc-gen-ts_proto. After
PR-B5, Phase B is complete.

Open in Web Open in Cursor 

cursoragent and others added 3 commits June 1, 2026 14:06
…ion-aware decoding

First Phase-B PR with mandatory Mac M4 integration test report
under \u00a79 (Linux CI is necessary but not sufficient).

Design choice: PR-B3 ships **greedy decoding only**. Speculative-
decoding integration (DLM proposer + AR verifier rejection sampling)
is reserved for a later PR. The wire contract is algorithm-agnostic
(GenerateResponse oneof on token_id / done / truncated), so the
upgrade path lands without breaking clients.

Three deliverables:

  1. inference_engine/session/generator.py (new, ~240 lines)
     - VerifierProtocol-driven GenerationCoordinator.
     - Yields TokenEvent / HistoryTruncatedEvent / DoneEvent.
     - Strict v0.3 validation: temperature/top_p/top_k must be in
       greedy no-op defaults; max_tokens must be >= 1; session must
       have prior AppendTokens. Per \u00a72.10 'no graceful degradation',
       any deviation raises ValueError -> INVALID_ARGUMENT.
     - HistoryTruncated emitted at most once per call, BEFORE any
       TokenEvent, when the cache is in sink+window-truncated state
       (matches the runtime.proto contract exactly).
     - INV-1 / INV-2 enforced via SessionStore at every step.
     - INV-3 byte-exact under greedy: same (session_id, history)
       -> bit-identical token stream. Tested with FakeVerifier on
       Linux and asserted in TestDeterminism.

  2. inference_engine/server/grpc_app.py (modified, +119 lines)
     - RuntimeServiceServicer.__init__ takes optional
       generation_coordinator: GenerationCoordinator. None = PR-B2
       UNIMPLEMENTED default preserved (regression-tested).
     - Generate RPC implements server-streaming with the four typed
       error mappings:
         SessionNotFoundError  -> NOT_FOUND
         ValueError            -> INVALID_ARGUMENT
         InvariantViolation    -> FAILED_PRECONDITION
       Plus the success path streams TokenEvent -> token_id,
       HistoryTruncatedEvent -> truncated, DoneEvent -> done.
     - Cancellation: polls context.cancelled() between events; on
       True, emits a final GenerateDone(STOP_REASON_CANCELLED) and
       returns. Cancellation latency is bounded by one generation
       step (the in-flight forward pass finishes before the next
       poll).
     - create_grpc_server factory plumbed with the new keyword.

  3. tests/inference_engine/session/test_generator.py (new, 31 tests)
     - TestGreedyHappyPath: 4 tests on token-then-done emission,
       max_tokens cap, single Done event, total_seconds reporting.
     - TestGreedyAdvancesVerifier: 4 tests confirming verifier
       state mirrored onto session after every step.
     - TestEos: 3 tests on EOS detection + STOP_REASON_EOS.
     - TestHistoryTruncated: 3 tests on at-start emission + at-most-
       once contract.
     - TestValidation: 9 tests on max_tokens, sampling params, and
       no-AppendTokens-prior rejection.
     - TestInvariants: 2 tests on INV-1 / INV-2 propagation.
     - TestDeterminism: 1 test on byte-exact greedy output across
       parallel sessions.
     - TestConstructorAndEventDataclasses: 4 tests on frozen dataclass
       contract and constructor.

  + tests/inference_engine/server/test_grpc_app.py (extended, +11 tests)
     - Generate streams tokens then done.
     - EOS triggers STOP_REASON_EOS.
     - HistoryTruncated frame emitted before tokens.
     - Unknown session -> NOT_FOUND.
     - No AppendTokens prior -> INVALID_ARGUMENT.
     - max_tokens=0 -> INVALID_ARGUMENT.
     - temperature=0.7 -> INVALID_ARGUMENT.
     - seed accepted on the wire.
     - InvariantViolation during generation -> FAILED_PRECONDITION.
     - Cancellation -> STOP_REASON_CANCELLED Done frame (direct
       Servicer invocation with FakeContext; cancellation latency
       bounded as documented).
     - Factory accepts generation_coordinator keyword.

Mac M4 reviewer aids:

  scripts/smoke_grpc_generator.py
    10-scenario smoke walking Generate scenarios with Append +
    Generate coordinators wired against FakeVerifier.

  scripts/review_pr_b3_on_mac.sh
    One-shot Mac M4 reviewer producing 5 JSON artifacts:
      pr-b3-mac-generator-tests-<unix>.json (31 tests, 100% on
      generator.py)
      pr-b3-mac-grpc-tests-<unix>.json (39 tests after PR-B3
      additions, 100% on grpc_app.py)
      pr-b3-mac-grpc-runtime-smoke-<unix>.json (PR-B1 regression)
      pr-b3-mac-grpc-appender-smoke-<unix>.json (PR-B2 regression)
      pr-b3-mac-grpc-generator-smoke-<unix>.json (PR-B3 new)

Local verification (Linux VM, py3.12):
  Linux CI gate: 629 passed (was 587 + 31 generator + 11 grpc
                 generate). Coverage 100.00% on 1521 stmts (was
                 1428 + 63 generator + 30 grpc_app additions
                 - 0 deletions).

Per ADR 0008 \u00a79: this PR introduces a NEW MLX-reachable runtime
path (verifier.forward_block + commit_or_truncate driven by the
coordinator's per-token loop). Even though FakeVerifier covers the
Linux-side dispatch logic exhaustively, the v0.3 GA contract
requires a Mac M4 integration test report on the PR branch before
merge. The carve-out paragraph does NOT apply here. PR is opened as
Draft pending the report.

Mac M4 verification command for the PR branch:
  bash scripts/review_pr_b3_on_mac.sh

The script writes 5 JSON artifacts under results/platform-tests/;
the user commits them back to this branch and the PR description is
updated to quote the smoke summary line.

Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ssion

Stacks on PR-B3 (#46). When this PR is merged, PR-B3 lands along
with it.

Public API matches the ADR 0008 \u00a73.1 example exactly:

    from kakeya import Client

    with Client('localhost:50051') as client:
        with client.create_session(eos_token_ids=[151645]) as session:
            session.append([10, 20, 30])
            for token_id in session.generate(max_tokens=64):
                print(token_id)

Surface (sdks/python/kakeya/):

  client.py (Client)
    - .address / .closed properties
    - .create_session(eos_token_ids, client_label) -> Session
    - .close() / context manager
    - Wraps grpc.insecure_channel + RuntimeServiceStub (sync)

  session.py (Session, SessionInfo)
    - .session_id / .closed
    - .last_stop_reason / .last_generated_token_count /
      .last_prefill_duration_seconds / .last_total_duration_seconds /
      .last_history_truncated_dropped (populated by .generate)
    - .append(token_ids) -> int
    - .generate(max_tokens, seed, temperature, top_p, top_k) -> Iterator[int]
    - .info() -> SessionInfo
    - .close() / context manager
    - SessionClosedError on local-side reuse after close()

  errors.py (typed exception hierarchy)
    - KakeyaError (base, carries .rpc_code)
    - SessionNotFoundError       <- gRPC NOT_FOUND
    - InvalidArgumentError       <- gRPC INVALID_ARGUMENT
    - InvariantViolationError    <- gRPC FAILED_PRECONDITION
    - ResourceExhaustedError     <- gRPC RESOURCE_EXHAUSTED
    - UnimplementedError         <- gRPC UNIMPLEMENTED
    - RpcCancelledError          <- gRPC CANCELLED
    - SessionClosedError         <- client-side, never crosses the wire

Sync vs async: PR-B4 ships sync only (grpc.insecure_channel). The
ADR \u00a73.1 example is sync, and the v0.3 target audience (REPL,
scripts, agent harnesses on a single Mac) prefers sync. An async
API can be added in a later PR without breaking this surface; the
runtime is identical (grpc.aio.server is wire-compatible with sync
clients).

Tokenization: per ADR \u00a72.4 / \u00a73.4, the SDK ships ZERO chat-template
logic. The runtime treats token ids as opaque integers; rendering
messages to tokens lives in sdks/python/examples/ (a future PR).

Tests (tests/sdk/python/):

  test_errors.py       (20 tests)
    Every gRPC StatusCode -> typed-exception mapping verified.
    Plus the catch-all path for unknown codes, the .rpc_code carry,
    and the SessionClosedError no-rpc-code variant.

  test_client.py       (13 tests)
    Construction, address property, closed flag, idempotent close,
    context manager, create_session with all argument combinations,
    RESOURCE_EXHAUSTED -> ResourceExhaustedError end-to-end.

  test_session.py      (33 tests)
    All 5 RPC methods, all error mappings, last_* metadata fields,
    HistoryTruncated -> .last_history_truncated_dropped, sampling
    param validation (greedy-only contract surfaces as
    InvalidArgumentError), context manager, double-close idempotency,
    SessionInfo dataclass standalone construction + repr.

All 66 tests run against a real grpc.aio runtime spun up in a
background thread (see tests/sdk/python/conftest.py). The
fixture creates the gRPC server INSIDE the worker thread's loop
(not the main thread) to avoid 'Future attached to a different
loop' from grpcio's internals; shutdown uses
server.wait_for_termination() so the loop can exit cleanly without
a cancellation tantrum.

CI changes (.github/workflows/ci.yaml):
  - PYTHONPATH gains sdks/python so 'import kakeya' resolves
    without pip-install (consistent with the rest of the repo's
    PYTHONPATH-based layout).
  - tests/sdk/python/ added to pytest paths.
  - --cov=kakeya added to the coverage gate.
  - package-import-smoke imports kakeya, kakeya.client,
    kakeya.session, kakeya.errors (regression catches an
    accidentally-broken __init__).

Mac M4 reviewer aid (\u00a79 carve-out applies — Linux-only path):
  scripts/review_pr_b4_on_mac.sh
    Runs SDK pytest + 100% coverage gate, plus all three prior
    PR-B1/B2/B3 smokes for regression. Produces 4 JSON artifacts
    under results/platform-tests/.

Local verification (Linux VM, py3.12):
  Linux CI gate: 695 passed (was 629 + 66 new). Coverage 100.00%
                 on 1694 stmts (was 1521 + 173 new = sdk surface).
  Reviewer script end-to-end:
    SDK tests       : 66 tests, cov=173/173=100%
    Runtime smoke   : 10/10
    Appender smoke  : 10/10
    Generator smoke : 10/10

Next PR after merge:
  PR-B5 (\u00a76.2): TypeScript SDK under sdks/typescript/. Targets
        Node.js 20+ / Electron 30+ / Bun 1.1+. Generates stubs
        from proto/kakeya/v1/runtime.proto via protoc-gen-ts_proto.
        Linux-only path; \u00a79 carve-out continues to apply.

Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com>
@FluffyAIcode
FluffyAIcode marked this pull request as ready for review June 1, 2026 15:30
@FluffyAIcode
FluffyAIcode merged commit db38dcc into main Jun 1, 2026
5 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.

2 participants