14.8 Reading SDK-shaped code
- Approved by
- Panaversity ·
In everyday life. You can drive a rental car you have never seen before. You recognize the shapes: the pedals, the wheel, the lights. You do not need to know how the engine was built. But before you drive off, you check what the company 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, built from a few shapes.
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, and agents write code in the same style.
Which shapes will I meet?
Mostly these seven. You do not need to follow every line of the code below. Find each shape, and say what it does.
| Shape | Recognize it by | Read it as |
|---|---|---|
| A client class | new Client(...) or Client(...) with a key and a URL | One object that holds the connection and the settings |
| An async method | async before a method | A call that waits for the network |
| A generator | yield inside a function, or function* | Hands out results one at a time, a stream, not all at once |
| An async iterator | for await in TypeScript, async for in Python | Loops over results as they arrive |
| A context manager | with or async with in Python, using or await using in TypeScript | Opens something and closes it for you, even after an error, which then still reaches you |
| A decorator | A line starting with @ above a function or a class, in Python | Wraps it with extra behavior, such as a retry. @dataclass in 14.3 is one |
| A wrapper function | A function that takes a function and returns a new one | The 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 ones companies publish.
An excerpt, then its run against the sample API. Find the generator, the context manager, and the wrapper or decorator. Skip any mark you have not met. The comments explain the few you need.
export class ApClient {
vendors = withRetry(() => this.#get("/vendors"));
async *openInvoices(): AsyncGenerator<Invoice> {
for (let page = 1; ; page++) { // counts 1, 2, 3, with no end of its own
const body = await this.#get(`/invoices?status=open&page=${page}`);
yield* body.invoices;
if (body.page >= body.pages) return; // so return ends it
}
}
// ...and one method that closes the client and prints "client closed"
}
await using client = new ApClient("http://localhost:4010", apiKey);
const { vendors } = await client.vendors(); // takes that one field from the answer
console.log(`${vendors.length} vendors`);
for await (const invoice of client.openInvoices()) console.log(invoice.number, invoice.amountCents);Both print:
8 vendors
INV-8301 482500
INV-8302 61875
...
client closedHere is what each shape did in that run:
- The wrapper or decorator is like an assistant who places your call and, if the line is busy, dials once more. The first
/vendorscall got a503. That raises no error by itself, as 14.6 said, so the SDK makes one:raise_for_status()in Python, and a check of the status inside#getin TypeScript. Python's@retry_oncedecorator and TypeScript'swithRetrywrapper each caught that error and called once more. Node cannot run TypeScript decorators, so TypeScript code for Node wraps functions by hand. - The context manager is like a door on a spring, which closes behind you however you leave. Python's
async withand TypeScript'sawait usingclose the client when the code around them ends. - The generator is like a waiter who brings one dish, and goes back to the kitchen only when you have finished it.
yieldhands one invoice to the loop, andyield*does that for each invoice in a list. The generator pauses there until the loop asks for another. It fetches the next page only when the current one is used up.
What does it check for me, and what does it leave to me?
An SDK sits at your edge, because it is someone else's code between your program and the service. So ask it what 14.4 to 14.7 taught you to ask of your own client. Ana asks the agent about this SDK: "Does it check the shape of each answer, or hand us whatever arrived? Which status codes does it retry, and how many times? Does every call have a deadline, and 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.
Try it
The file in 14-8 is the whole made-up SDK. Start the sample API, as in 14.6. Then send: "Please run the file in 14-8 and show me what it prints. Don't explain it yet."
Investigate. Open the file in a text editor. Find the generator and the context manager, and say in one line what each does for the caller.
Modify. Send: "Please stop the invoice loop after the first invoice, with break. Don't run it yet." How many pages will it fetch? Will client closed print? Then restart the API and ask the agent to run it.
Make. Ask: "Does this SDK check the shape of each answer? Does every call have a time limit?" Write down what you would ask its authors to change. Then ask the agent to stop the API.
What you should see. After your change, 8 vendors, one invoice, then client closed. The generator fetches a page only when the loop asks for more, and the client closes even after break.
The operating rule: before you read the details, name the shapes in SDK code, and say in one line what each does for the caller. Then find what it checks for you, and do the rest at your edge.

Figure 14.9. Name the shapes first.
Next. Lab 14 puts it all together, and gives you the chance to set the rules Marcus's agent set for him. 14.9 shows where your agent differs from the others.
Check yourself
Question 1 / 7
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