Write a provider
A provider is the part of your app that owns some commands. This guide builds the cart from the coffee shop demo: status first, then reading, then changing, then the one command that needs a person's yes. The full code is in site/app.js.
1. Status first
Start with what an agent needs before anything else: what state is this in?
const NONE = { type: "object", properties: {} };
Sutram.register({
name: "cart status",
provider: "cart",
description: "Show what is in the cart",
input: NONE,
output: {
type: "object",
required: ["count", "total", "items"],
properties: {
count: { type: "integer" },
total: { type: "number", format: "money" },
items: { type: "array", items: LINE },
},
},
view: { columns: ["sku", "name", "qty", "price"] },
annotations: { readOnly: true },
seeAlso: ["cart add", "checkout"],
run: () => {
if (!cart.size) {
return {
text: "The cart is empty. Try shop list, then cart add.",
data: { count: 0, total: 0, items: [] },
};
}
return {
text: `${summary()}.`,
data: { count: count(), total: total(), items: items() },
};
},
});- It is read-only, so nobody is ever asked before it runs.
textis one line for people;datais the state, for agents and for--json.- The empty case says what to do next. Agents follow hints like that.
- A bare
status(and the agent'ssutram_status) now includes the cart.
2. A change
Sutram.register({
name: "cart add",
provider: "cart",
description: "Add a product to the cart",
input: {
type: "object",
properties: {
sku: {
type: "string",
enum: products.map((p) => p.sku),
description: "Product code, from shop list",
},
qty: {
type: "integer",
minimum: 1,
maximum: 20,
default: 1,
description: "How many",
},
},
required: ["sku"],
},
positional: ["sku", "qty"],
output: {
type: "object",
required: ["count", "total"],
properties: {
count: { type: "integer" },
total: { type: "number", format: "money" },
},
},
annotations: {},
examples: [
"cart add V60-02",
"cart add GUJI-250 2",
"cart add SCALE --qty=1",
],
seeAlso: ["cart remove", "cart status", "checkout"],
run: ({ sku, qty }, { caller }) => {
cart.set(sku, (cart.get(sku) || 0) + qty);
changed(caller);
return {
text: `Added ${qty} × ${bySku.get(sku).name}. The cart has ${summary()}.`,
data: { count: count(), total: total() },
};
},
});- The schema does the checking.
cart add V61fails with exit 2 beforerun()is called, and the message lists the valid codes. enumalso gives completion with Tab for free.positionalmakescart add V60-02 2work, as well as--sku=V60-02 --qty=2.run()calls the app's own code. The shop's buttons call the same command, so people, the console and agents all go one way.
3. A command that only exists when it makes sense
let removeCheckout = null;
function toggleCheckout() {
if (cart.size && !removeCheckout) {
removeCheckout = Sutram.register({
name: "checkout",
provider: "cart",
description: "Place the order for everything in the cart",
input: NONE,
annotations: { consequential: true },
confirm: () => `Place an order for ${summary()}?`,
run: () => { /* … */ },
});
} else if (!cart.size && removeCheckout) {
removeCheckout();
removeCheckout = null;
}
}checkoutappears inhelpand in the agent's tools once the cart has something in it, and disappears when it is empty. Nobody calls a command that cannot work.consequential: truemeans the policy asks first.confirmwords the question with the real numbers.
4. Failures that help
run: ({ sku }) => {
if (!cart.has(sku)) {
const message = `cart remove: ${sku} is not in the cart.`;
throw new CommandError(message, EXIT.FAILED);
}
// …
},Say which command failed and why, in words the caller can act on. See Errors that help agents.
5. Check it
› help cart
› man cart add
› cart add V61
› cart add V60-02 2
› status
› checkoutThen the agent's view: Sutram.tools() in the developer tools, and await Sutram.call("cart_add", { sku: "V60-02" }). See Test with Playwright to make that a test.
Checklist
- A read-only
<provider> statuswith one line oftext - Names start with the provider:
cart add, notadd to cart - Descriptions say what the command does, in one line
- Every input property has a type, and a
descriptionwhere the name is not obvious -
annotationsset on purpose: read-only, consequential, navigates or reinitializes (andreinitializes()called where it happens) - Commands registered only while they make sense
- Failures name the command and say what to do instead
-
examplesfor anything with more than one argument