AgentDeck 6.0 is here: serve one deck over HTTP, AG-UI or the terminal.See what's new
AgentDeckv6.0.6
How Jack Is Built

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.

See Tools and Context.

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 after from_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 HTTPA 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 wireIt'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.completed is not among them: its result_preview is 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. Origin is 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.