The consult tool
When a user attaches something to a conversation — a file, a web page, a note, a record of your own
schema — the assistant is shown a listing of what is attached: an id, a type, a title and a
timestamp for each item, and not its content. consult is how it reads one of them.
consult({"id": "7bWQhKzR2mNvT4pLsXdYcE"})
It is a built-in, and it is the one built-in you are meant to edit. What consulting a File returns is the same everywhere; what consulting an Invoice, a Person or a Support case returns is entirely specific to your workspace, so that is the part you write.
What you write, and what is generated
The tool's stored code is only a list of per-node-type readers. Everything around them is generated when the tool is compiled:
The generated entry point does four things before your code runs, and none of them is yours to write or to bypass:
- parses the
idand rejects anything that is not a valid UID; - rejects a UID with no node behind it;
- rejects a node the current user cannot access, even when it is attached to the chat;
- rejects a node that is not part of this conversation's context.
By the time your reader is called, the item exists, the user may see it, and the user put it there.
The reader schema
Every method whose name starts with Consult is picked up as a reader and must follow exactly this
shape:
object Consult<NodeType>(ToolScope scope, UID128 uid) { … }
| Rule | Detail |
|---|---|
| Name | Consult + an existing node type. The leading _ of an internal type may be omitted, so ConsultFileEntry and Consult_FileEntry both map to _FileEntry. |
| Parameters | Exactly two: a ToolScope then a UID128. |
| Return | Any object. It is serialized to JSON for the model. |
| One per type | Two readers for the same node type is an error, not a race. |
No class, no [Tool] |
Do not declare a class or a tool method — the surface is generated around your readers. |
All four rules are checked when you save the tool, against your workspace's real schema, so a typo in a node type name fails immediately with a message naming the method — it does not fail later, in a conversation.
A node type with no reader is not an error: consulting one answers
"Items of type 'X' cannot be consulted", which tells the model to stop rather than to retry.
The readers that ship
The seeded code contains three, one per built-in type a user commonly attaches. The file reader is the one worth reading closely — it shows the whole idiom:
object ConsultFileEntry(ToolScope scope, UID128 uid)
{
var graph = scope.Graph.Internals;
if (!graph.TryGetReadOnlyContent<_FileEntry>(uid, out var file))
{
return new { ok = false, error = "File not found" };
}
var pages = Mosaik.AI.ChatAI.GetTextFromNodePerPage(graph, uid);
var text = string.Join("\n\n", pages.OrderBy(p => p.Key).Select(p => p.Value));
scope.SetToolCallDisplayName($"Reading '{Path.GetFileName(file.OriginalName)}'");
//Adding a snippet lets the interface show the source of the answer next to the tool call
if (!string.IsNullOrWhiteSpace(text)) scope.AddSnippet(uid: uid, text: text, page: 1, endPage: Math.Max(1, pages.Count));
return new
{
ok = true,
uid = uid.ToString(),
name = Path.GetFileName(file.OriginalName),
contentType = file.ContentType,
source = file.Source,
language = file.Language.ToString(),
url = file.Url,
pages = pages.Count,
text = text
};
}
Three habits to copy:
ok/error. Answer{ ok = false, error = "…" }for anything you cannot read, and write the error for the model: say what to do instead, not just what went wrong.scope.SetToolCallDisplayName(…)— the label the user sees in the chat trace. "Reading 'Q3-forecast.xlsx'" beats "consult".scope.AddSnippet(…)— registers the text as a citation, so the interface can show the source next to the answer. Skip it for a record whose fields are the answer; use it for anything long enough that the user will want to check it.
The other two are short:
object ConsultWebPage(ToolScope scope, UID128 uid)
{
var graph = scope.Graph.Internals;
if (!graph.TryGetReadOnlyContent<_WebPage>(uid, out var page))
{
return new { ok = false, error = "Web page not found" };
}
scope.SetToolCallDisplayName($"Reading '{page.Title}'");
return new { ok = true, uid = uid.ToString(), title = page.Title, url = page.Url, text = page.TextContent };
}
object ConsultNote(ToolScope scope, UID128 uid)
{
var graph = scope.Graph.Internals;
if (!graph.TryGetReadOnlyContent<_Note>(uid, out var note))
{
return new { ok = false, error = "Note not found" };
}
scope.SetToolCallDisplayName($"Reading '{note.Title}'");
return new { ok = true, uid = uid.ToString(), title = note.Title, text = Mosaik.AI.ChatAI.GetTextFromNode(graph, uid) };
}
Adding a reader for your own node type
Your node types are schema-defined rather than C# classes, so you read them through the graph and the
generated N helpers instead of TryGetReadOnlyContent<T>. A support case, with its fields and the
records it links to:
object ConsultSupportCase(ToolScope scope, UID128 uid)
{
if (!scope.Graph.TryGet(uid, out var node))
{
return new { ok = false, error = "Support case not found" };
}
var id = node.GetString(N.SupportCase.Id);
var summary = node.GetString(N.SupportCase.Summary);
scope.SetToolCallDisplayName($"Reading case {id}");
//The records this case links to: what makes a case answerable is rarely on the case itself.
//scope.Q() queries as the calling user, so a linked record they cannot see is not returned.
var products = scope.Q()
.StartAt(uid)
.Out(N.Product.Type)
.Take(20)
.AsEnumerable()
.Select(p => p.GetString(N.Product.Name))
.ToArray();
var body = Mosaik.AI.ChatAI.GetTextFromNode(scope.Graph.Internals, uid);
if (!string.IsNullOrWhiteSpace(body)) scope.AddSnippet(uid: uid, text: body);
return new
{
ok = true,
uid = uid.ToString(),
id = id,
summary = summary,
status = node.GetString(N.SupportCase.Status),
opened = node.Timestamp.ToDateString(),
products = products,
text = body
};
}
What to return is a design decision, and the same one you make for a tool description: return what answers questions about the item, and nothing else.
- Include the linked records a reader of the item would want — the customer on an invoice, the products on a case. The model cannot follow an edge itself.
- Leave out internal identifiers the user will never ask about. Every field costs tokens on a call that is made for one item at a time.
- Never return what the user cannot see. The generated checks cover the consulted node; a field or a
neighbour you reach from it is yours to check.
scope.Q()(andscope.Query()) start a query as the calling user, which is what you want for anything you walk out to.
Read-only means read-only
A reader runs on a tool call the model makes on its own initiative. Do not mutate the graph from one, and do not call out to anything with a side effect — a user who attached a document did not ask for anything to happen to it.
Editing the tool
Open Settings → AI → AI Tools → Consult. The editor holds only the readers; saving compiles the generated tool around them and reports a schema error against the offending method.
A release can overwrite your readers
consult is a built-in with a version stamp. A release that changes the shipped readers bumps that
stamp, and the workspace then re-seeds the tool, replacing your edits. Keep your readers in your
workspace definitions export so they can be
restored, and re-check the tool after an upgrade that touches it.
What the tool tells the assistant to do
consult carries a [ToolSystemPrompt]
excerpt — one appended to the system prompt of every chat or agent run that offers the tool. It says how
the ids are to be handled (22 characters, case-sensitive, copied verbatim, never invented), that only
attached items can be consulted, and that an item is consulted before it is answered about rather than
assumed. It holds whether or not anything is attached yet, because the tool is enabled by the default
workspace picker entry, not only by a conversation that already has context.
Because that rule lives on the tool, you do not repeat it in your assistants' prompts — and an agent run
that enables consult gets it too.
Related
- The search tool — finding an item in the first
place. A search result's
uidis not consultable unless the user attached it. - AI Tools Scope — everything
scopeoffers. - Querying the graph — the query surface used in the reader above.