Anatomy
tools/opportunities.ts
Descriptions are prompt engineering
The model chooses tools by reading descriptions. A description that only restates the name wastes the one signal you have:get_current_date description explicitly warns that every staleness judgement is relative to the simulated date, not the real calendar — because a model that assumes otherwise gets the task wrong.
Validation happens before your code runs
Arguments are checked againstinputSchema in bindTools. A call with a hallucinated property never reaches run:
Failing usefully
ThrowtoolError with a code the agent can act on. It is returned as a value, not raised — a rejected call is an observation, not the end of the rollout.
Any string is accepted, so a domain can add its own vocabulary — but prefer these when they fit.
Business rules belong in the tool, not in the verifier. CRM refuses to assign work to a deactivated user inside
assertAssignable, which is why “reassign the departed rep’s pipeline” is a real task rather than a formality.
Registration is explicit
tools/ does not become callable until its export is in tools/index.ts. That asymmetry with data/ is deliberate: a dataset landing on disk is inert, a tool landing on disk would widen what the agent can do.
tools/index.ts
How many tools?
More than feels necessary. The count follows from the rule that the agent sees nothing but tools: every collection it must enumerate needs a way to list, search and fetch, before a single mutation exists. CRM needs 42 across nine collections. ERP has 185. A surface small enough to feel tidy is usually one that hides state the agent needs.Sharing a contract
Templates pindefineTool to their own state type once, so every tool file imports one thing:
tools/contract.ts
S.string, S.enumeration), input readers (readString, readOptionalNumber) and paging helpers every tool shares. Lookup and mutation helpers go in tools/helpers.ts, so a rule like “only an active user may act” is written once rather than re-implemented per tool.
Silo