Tools, jobs and the prompt
The prompt
Section titled “The prompt”The cheapest extension point: a fragment injected into the system prompt while the app is installed.
prompt: (ctx) => { const trade = ctx.settings.trade; if (!trade) return null; return `### Bill of quantities\n\nThis company does ${trade}. Keep only its items…`;}It runs on every request, so it must be synchronous and cheap — no network,
no database — and it receives organizationId and the validated settings.
Return null when there’s nothing worth saying: an empty instruction is tokens
the customer pays for.
A function the model can call mid-conversation, with the JSON Schema of its arguments.
tools: [ { name: "gold_price", description: "Today's gold price for a fineness, net of fees.", parameters: { type: "object", properties: { fineness: { type: "string", enum: ["750", "585"] } }, required: ["fineness"], }, async handler(args, ctx) { const quote = await ctx.shared.get<number>("eur-per-gram"); return { pricePerGram: quote }; }, },]The host qualifies the name with the app’s id, runs the handler with a timeout, and returns the result to the model. A tool that throws doesn’t take the conversation down: the failure is isolated and the model is told.
Dictating a quote line
Section titled “Dictating a quote line”This is the part worth reading twice. A tool can attach the line that follows from its result:
import { withProposedLine } from "@prezzando/plugin-sdk";
return withProposedLine( { pricePerGram: 71.4 }, { description: "Gold 750 purchase", qty: 12, unit: "g", price: 71.4, vatRate: 0, vatNote: "Regime del margine" },);The host sets the line aside and hands the model an opaque reference. When the model fills in the quote and reports that reference, the host puts the real numbers back in place of whatever the model wrote. The model stops doing arithmetic and starts copying a string, which it is good at.
Note what the line does not decide: how totals are computed. The app knows
the nature of the operation it priced and says so with vatRate and
vatNote; the arithmetic stays in the core, the one place where tax behaves
the same way for everyone.
Showing your work in the chat
Section titled “Showing your work in the chat”withToolCard attaches a small card the chat renders under the answer — a
headline, a few rows, a footnote. Same idea as the settings: you declare what
to show, the host draws it.
Work repeated every day, once for the whole platform and not once per customer.
jobs: [ { name: "gold-fix", label: "Daily gold quote", dailyAt: "08:00", timeZone: "Europe/Rome", async handler(ctx) { const data = await ctx.http.fetchJson<{ price: number }>("https://api.metals.dev/…"); await ctx.shared.set("eur-per-gram", data.price); }, async summary(ctx) { const price = await ctx.shared.get<number>("eur-per-gram"); return price ? `In force: ${price} €/g` : null; }, },]It exists for data that is the same for everyone and costs something to fetch. Downloading it per tenant would multiply by the number of customers a call with one single answer — and with a metered source, that difference is the bill.
The context of a job is deliberately narrower than a tool’s: no organisation,
no settings, no per-tenant storage. What it produces goes in shared, where
every organisation then reads it. The host guarantees one run a day even with
several API replicas and retries a failure instead of skipping the day, but the
handler must be idempotent anyway: a retry after a timeout can find the
work already done.
summary is the line the panel shows about how the job is doing. It’s on the
app because only the app knows what it downloaded — the host holds the dates,
not the meaning — and it must never throw: it’s called to draw a panel, not to
make a price.