Designing the tool surface

The agent-computer interface is the tool surface as the model experiences it. It is designed, not derived. Nextcloud exposes hundreds of endpoints, and wrapping them would produce an agent that knows Nextcloud — where the good outcome is an agent that knows the domain.

The complete list of what exists is in Reference: tools. This page is the five rules those tools obey, and what each one costs when it is broken.

1. A tool models an intention, not an endpoint

ocs_get, dav_propfind — the model has to know Nextcloud's shape. ✅ files_search, calendar_list_events — the model knows what a person wants.

The test is whether a capable person who has never heard of Nextcloud could use the tool correctly from its description alone.

A consequence people find surprising: calendar_list_events performs calendar discovery internally. Making the model chain a "list the calendars" call before it can ask a question spends a whole turn on plumbing, and the earlier version leaked raw DAV paths into the context while doing it.

2. Identifiers circulate

Any tool that returns an object returns the exact handle the other tools accept. One name per concept, and the storage layout never reaches the model:

ConceptThe handleNot
a filepath/Documents/Readme.md/remote.php/dav/files/uid/…
a calendarcalendarpersonalthe full DAV href
an eventuid
an accountusera display name
a Deck itemboard, stack, card

The retired predecessor leaked DAV hrefs into the context. That was expensive in tokens and an injection surface, and it meant the model could hold a name it could not act on.

The same rule is why contacts_search merges two sources: vCards give humans (names, emails, phones) while the sharees endpoint gives the account id that sharing actually accepts. Returning only vCards would hand the model a person it cannot share with.

3. The context budget is a design parameter

truncated and hint matter more than content:

{ "path": "/notes.md", "content": "…", "bytesReturned": 40000,
  "totalBytes": 91234, "truncated": true, "nextOffset": 40000,
  "hint": "Truncated at 40000 bytes of 91234. Call again with offset=40000 to
           continue, or format:'outline' to see the structure without the text." }

Without those fields the model believes it read the whole file and answers confidently from a third of it.

This rule has teeth in a way that is easy to underestimate. Asked a question about a 140 000-character manual, an early version read offset 0, then 10000, then 133560, then 2500, then 0 again with a different limit — and hit the step cap having said nothing. Paging is the wrong move and the model will do it anyway unless the tool offers something better. So files_read takes a find argument that returns each matching passage with its offset.

4. Errors are instructions

Every error message names the cause, names the next action, and says explicitly whether to retry. The failure mode it prevents is not confusion but persistence: a bare "403 Forbidden" makes an agent retry forever.

The messages are a table, not a translation of status codes, and the tests assert the substring the model needs — "do not retry", "files_search" — rather than whole sentences, so wording stays editable. → the table

5. Every tool declares annotations

readOnlyHint, destructiveHint, idempotentHint. These are not documentation: they are what drives the confirmation prompt three services away, and the policy fails closed — a tool with no usable annotation is treated as needing approval.

That is the rule with the sharpest consequence when broken, because breaking it is silent. A destructive tool that forgets destructiveHint runs without asking, and nothing anywhere reports an error. Omitting annotations entirely is safe; a wrong one is not. → G3

Two conventions that follow from the rules

Describe every argument. A field description is the model's only documentation for that argument. There is no second place to look.

Results are compact JSON. Never indented — indentation is tokens the user pays for and the model discards.

The rule the tools do not have: knowing about formats

files_read is the only reading tool, and it is format-agnostic on purpose. Text is sliced; an image is handed to the model to look at; a PDF, spreadsheet or presentation is converted to text by the document server and then sliced exactly like text. The model never learns what a format is.

A second document_read tool would be the obvious way to add document support and would put format knowledge back into the interface — the model would have to decide which reader to use, from a file extension, before knowing whether the file is readable at all.

How you know the surface is wrong

Every tool call is recorded with a failure kind. The distinction that matters is between Nextcloud refused — the system working exactly as intended — and the model could not use the tool: a wrong argument, a retried impossibility, a guessed identifier.

The second category is the feedback loop. It is not a model problem; it is a description problem, an identifier problem, or a missing hint.