Skip to content
The AI Agent Factory

14.8 Reading SDK-shaped code

Approved by
Panaversity ·
12 min read

In everyday life. You can drive a rental car you have never seen before. The pedals, the wheel and the lights are where you expect, even if the radio is hard to understand. You recognize the shapes. You do not need to know how the engine was built. But before you drive off, you check what the company did for you and what it left to you, such as the fuel and the spare tire.

What you will learn. How to recognize the shapes in the code a company publishes for its service, then find what that code checks for you and what it leaves to you.

Why it matters. From Chapter 17 on, much of the code at your edges is someone else's. It is built from a few shapes. Name them, find what the code does for you, and do the rest at your edge.

What is an SDK?

An SDK, a software development kit, is a package a company publishes so programs can use its service: an AI model, a payment system, a database. SDKs use a few shapes. You have met two: the with block from 13.7, the door that closes itself, and @dataclass from 14.3. The agent writes these shapes. You need to recognize them, because from Chapter 17 on, the code you review uses SDKs, and agents write code in the same style.

Which shapes will I meet?

Mostly these seven.

ShapeRecognize it byRead it as
A client classnew Client(...) or Client(...) with a key and a URLOne object that holds the connection and the settings
An async methodasync before a methodA call that waits for the network
A generatoryield inside a function, or function*Hands out results one at a time, a stream, not all at once
An async iteratorfor await in TypeScript, async for in PythonLoops over results as they arrive
A context managerwith or async with in Python, using or await using in TypeScriptOpens something and closes it for you, even after an error, which then still reaches you
A decoratorA line starting with @ above a function or a class, in PythonWraps it with extra behavior, such as a retry. @dataclass in 14.3 is one
A wrapper functionA function that takes a function and returns a new oneThe same idea as a decorator, written by hand

At work. Brightline's repository includes a 50-line made-up "AP SDK" in each language, shaped like the client libraries that companies publish. Ana does not try to understand every character. She marks each shape from the table, says what it does in one line, and has the agent run the Python version against the sample API. Then she can read the real SDKs in Chapter 27 the same way.

In Python. An excerpt from the made-up SDK, exercises/sdk-shaped/ap_sdk.py, then its run against the sample API. Find the decorator, the generator and the context manager.

class ApClient:
    async def __aenter__(self) -> Self:
        return self

    async def __aexit__(self, *exc: object) -> None:
        await self._http.aclose()
        print("client closed")

    @retry_once
    async def vendors(self) -> list[dict[str, Any]]:
        response = await self._http.get("/vendors")
        return response.raise_for_status().json()["vendors"]

    async def open_invoices(self) -> AsyncIterator[dict[str, Any]]:
        page = 1
        while True:
            body = (await self._http.get(f"/invoices?status=open&page={page}")).json()
            for invoice in body["invoices"]:
                yield invoice
            if body["page"] >= body["pages"]:
                return
            page += 1

async def main() -> None:
    async with ApClient("http://localhost:4010", os.environ["AP_API_KEY"]) as client:
        print(f"{len(await client.vendors())} vendors")
        async for invoice in client.open_invoices():
            print(invoice["number"], invoice["amountCents"])

asyncio.run(main())
8 vendors
INV-8301 482500
INV-8302 61875
...
client closed

Here is what each shape did in that run:

  • The decorator. The first /vendors call got a 503. raise_for_status raised an error for it, and the @retry_once decorator caught that error and called the method once more.
  • The context manager. __aenter__ and __aexit__ are what make async with work: Python calls them on the way in and on the way out. They are dunder methods, as in 14.3: Python calls them for you.
  • The generator. open_invoices is an async generator. Each yield hands one invoice to the async for loop, which prints it, and the generator pauses there until the loop asks for another. The method fetches the next page only when the current one is used up, like a clerk who brings one box of invoices from the archive and goes back for the next box only when you finish this one. while True repeats until return stops it, on the last page.

A generator without async works the same way, with a plain for loop.

In TypeScript. The same client, from exercises/sdk-shaped/ap-sdk.ts. Find the generator, the async iterator and the context manager.

export class ApClient {
  vendors = withRetry(() => this.#get("/vendors"));

  async *openInvoices(): AsyncGenerator<Invoice> {
    for (let page = 1; ; page++) {
      const body = await this.#get(`/invoices?status=open&page=${page}`);
      yield* body.invoices;
      if (body.page >= body.pages) return;
    }
  }
  async [Symbol.asyncDispose](): Promise<void> { console.log("client closed"); }
}

await using client = new ApClient("http://localhost:4010", apiKey);
for await (const invoice of client.openInvoices()) console.log(invoice.number, invoice.amountCents);

Here is what each shape does:

  • The generator. async * marks an async generator, and yield* hands out each invoice of a page. for (let page = 1; ; page++) counts 1, 2, 3 with no end test, so only return stops it.
  • The async iterator. for await reads the invoices as they come.
  • The context manager. await using closes the client when the code around it ends, here the end of the file, by calling its Symbol.asyncDispose method. In this sample that method only prints client closed. A real client would close its connections there.
  • The wrapper function. withRetry takes a function, call, and gives back a new function that tries call once more after an error. So the retry is written once, and any call can use it, as Python's @retry_once does.

This is the new function that withRetry gives back:

async () => {
  try {
    return await call();
  } catch {
    return await call(); // one more try
  }
};

Where the languages differ. Python writes decorators with @, and Node cannot run TypeScript decorators, so TypeScript code for Node wraps functions by hand. Python closes things with with and two dunder methods, __enter__ and __exit__, or __aenter__ and __aexit__ for async with. TypeScript closes them with using and a Symbol.dispose or Symbol.asyncDispose method, which Node supports from version 24.

What does it check for me, and what does it leave to me?

Ask an SDK what Concepts 14.4 to 14.7 taught you to ask of your own client. An SDK sits at your edge, because it is someone else's code between your program and the service. Does it check the shape of each answer, or hand you whatever arrived? Which status codes does it retry, and how many times? Does every call have a deadline? Does it close what it opens? Whatever it does not do, your code does at the edge, with a schema on what it returns and a deadline on each call. Ana asks the agent, "Before we rely on this SDK, can you tell me what it does for us? Does it check each answer? Which errors does it retry, and how often? Does every call have a time limit, and does it close what it opens?" The made-up SDK has weaknesses on purpose, and she writes down what she would ask its authors to change.

The operating rule: before you read the details, name the shapes in SDK code. They are the client, async method, generator, iterator, context manager, decorator and wrapper function. Say in one line what each one does for the caller. Then find what it checks for you, and do the rest at your edge.

Figure 14.9. Annotated excerpts from the made-up AP client. In Python: the class, the async context manager methods aenter and aexit, the @retry_once decorator, an async method, and async with. In TypeScript: an async generator method, yield*, await using, and a for await loop.

Figure 14.9. Name the shapes first. Most SDK code is a handful of shapes, repeated.

Practice it. Practice: reading SDK-shaped code, in your own tool, ten minutes.

Check yourself

Question 1 / 9

0 answered

An agent's generator hands out open invoice numbers from a list.from collections.abc import Iterator def open_numbers(invoices: list[dict[str, str]]) -> Iterator[str]: for inv in invoices: print("checking", inv["number"]) if inv["status"] == "open": yield inv["number"] rows: list[dict[str, str]] = [{"number": "INV-5331", "status": "paid"}, {"number": "INV-5332", "status": "open"}, {"number": "INV-5333", "status": "open"}] for number in open_numbers(rows): print(number) breakWhat does it print?

Owner
Panaversity