Implementation notes
The engineering behind how Jack is built. Each note is a decision that had a reasonable alternative, and the reason that alternative lost.
The tools are @tool
A tool that declares a ToolCtx parameter is @tool, not @function_tool: that decorator puts
every parameter into the schema the model sees, and the model has no DocsCorpus to pass. @tool
compiles the function instead, so the model is offered only query or slug - and is required
here precisely because these tools carry a context: a plain function doing the same is refused at
build(), naming @tool as the fix.
The context type is declared
deck = Deck(agents=[jack], context=DocsCorpus)context=DocsCorpus is the type; the instance goes in per run. Declaring it makes build()
check every ToolCtx[...] in the catalog, tools and the instructions callable alike, before a
question is ever asked. The wrong type raises ContextTypeError naming both, at startup rather
than mid-answer.
Search is a dict and a scan
The corpus is about thirty pages and 120 KB, so retrieval is TF-IDF over Path.read_text(), in
roughly thirty lines.
A vector store lost on three counts: at this size a scan beats an embedding round-trip, there is
no index to rebuild when a page changes, and it cannot return a stale chunk. DocsCorpus.search
is where that decision lives, and its signature is what stays fixed if the corpus outgrows it.
Explicit composition, not Deck.from_project()
Every other example discovers its catalog from a .agentdeck/ directory. Jack cannot, because
his tools and his Deck share one DocsCorpus class and a bundle has no clean way to share a type
with the program composing it:
- a module beside
.agentdeck/imports only when the process started in that directory, and a server starts wherever its supervisor puts it; - a module inside it resolves through
agentdeck_project, an internal alias that exists only afterfrom_project()has run, which is too late for a module-level import.
Explicit composition has neither problem. Both front doors are described in Deck.
Its own route, not Deck.asgi()
AgentDeck packages an HTTP surface as bindings: deck.serve() / deck.expose()
take a Binding and open a standalone server or an ASGI app. Jack writes his own route instead,
for two structural reasons:
| Context cannot cross HTTP | A run started through a binding carries context=None. There is no wire form for a live Python object, and both tools need the DocsCorpus. |
Native.http() speaks a different wire | It's AgentDeck's generic protocol - POST /runs, GET /runs/{id}/events, and the rest - not a single POST /ask with a chat body. Adopting it still means translating between Jack's own request and the protocol's, which is the layer this route exists to avoid. |
The result is about forty lines over deck.stream(), streaming canonical events with no
translation layer.
What a public endpoint needed
Jack is unauthenticated on purpose: a documentation assistant that asks you to log in is not a documentation assistant. The limits below are the whole of what stands between a public hostname and someone else's bill.
- An event allowlist. Five kinds reach the browser.
tool.call.completedis not among them: itsresult_previewis the tool's output verbatim, so a tool that raised would put its exception text on a public wire. - A quota shaped as conversations. Three sessions per client per day, twenty turns each. Turns bound a conversation that re-sends its history every turn; sessions stop the way around that, which is to finish twenty turns and start again.
- A token ceiling. The only structural answer to "can this be used to write someone's essay". An instruction is persuadable; a ceiling is not.
- An origin check, which is not authentication.
Originis browser-set and forged in one flag. It stops another site embedding the endpoint, and nothing more.
Prompt injection
The structure of the prompt cannot be forged: the page slug is validated against the corpus, so only known values survive, and the delimiter is stripped from any reader selection. What the model chooses to say with that text is not guarded, and no delimiter makes it so.
The full accounting is in the example's README.
These notes stop at the process boundary. For how a Deck becomes a supervised service, what a
health check must actually check, and what shutdown owes aclose(), see
Deployment.