Expose an agent to another agent as a tool. The parent model calls it
by name with a message; the sub-agent answers that message with its
own model, its own system prompt and its own tools, and its final text
is what the parent is shown. Nothing else crosses: the sub-agent's
transcript stays on the sub-agent, so the parent pays for one tool
result rather than for everything the sub-agent read.
The sub-agent keeps its conversation between calls, exactly as it would
if the script drove it — a second call from the parent continues the
first. Give it a fresh B.ai.agent(...) where that is not what you want.
const researcher = B.ai.agent({ systemPrompt: "You look things up and answer briefly.", tools: [search] });
const ask = B.ai.tool.agent(researcher, {
name: "ask_researcher",
description: "Ask for a fact about a resident's chart. One question at a time.",
});
B.ai.call({ message, tools: [ask] });
Nesting is capped (bluestep.ai.max-nesting, three by default): a
sub-agent may have sub-agents of its own, but not without end.
Only a finished turn is an answer. A turn the usage gate denied, a turn the sub-agent's own hook left deferred, and a turn that stopped at a cap or a hook are each reported to the parent model as a tool failure, and the tool is then withdrawn for the rest of the call — none of them is something the parent could fix by asking again. A deferred turn is also undone on the sub-agent, so it stays usable afterwards.
The sub-agent to expose. Configure it fully before passing it.
Tool name and description, both required.
Open a builder for a fully-typed custom AI tool. Chain
.setName(...), .setDescription(...), .setSchema(...), then
.setExecutor(...) on the typed builder returned by setSchema;
setSchema captures the schema literal's type so setExecutor's
args parameter is typed via FromSchema<S> — eliminating drift
between the input_schema sent to the model and the JS function that
reads its arguments. The terminal setExecutor(...) call returns the
built AiTool.
const tool = B.ai.tool.custom()
.setName("add_1")
.setDescription("adds one!")
.setSchema({
type: "object",
properties: {
a: { type: "integer" },
b: { type: "integer" },
},
required: ["a", "b"],
})
.setExecutor((args) => args.a + args.b + 1);
Auto-generate an AiTool that updates an existing entry. The same entry
is mutated on every tool invocation within a single B.ai.call — useful
for incrementally refining a record from narration. Pass afterApply
for derived state the model can't produce.
The default required list is empty: for an update the model is
expected to supply only the fields it intends to change. Use
options.required if you want to force certain fields.
const mar = thisRecord.forms.marMedsToBeAdministeredToday.optById(marId).orElseThrow();
const tool = B.ai.tool.forExistingEntry(mar, {
description: "Sign off the MAR entry the caregiver just administered.",
include: [],
afterApply: (entry) => entry.fields.adminSig.sign(Date.now()),
});
B.ai.call({ message, tools: [tool] });
The form entry to update.
Optionaloptions: AiToolForExistingEntryOptionsAuto-generate an AiTool that creates a new entry on a multi-entry form.
The executor calls form.newEntry() per invocation and applies the
model's args to the fresh entry. Pass afterApply for derived state
the model can't produce (e.g. signing a signature with the current
time).
The default required list is every included field that is marked
required in the form definition (field.metaData().required()).
Override with options.required (pass [] to relax everything).
const tool = B.ai.tool.forNewEntry(thisRecord.forms.nursingNotes, {
description: "Capture a nursing note from the caregiver's narration.",
exclude: ["staffSig"],
afterApply: (entry) => entry.fields.staffSig.sign(Date.now()),
});
B.ai.call({ message, systemPrompt, tools: [tool] });
The multi-entry form to create entries on.
Optionaloptions: AiToolForNewEntryOptionsReturns a JS-friendly view of an AiTool's input_schema — safe to
pass to JSON.stringify and member-accessible at every nesting level.
Returns null when the tool has no input_schema set.
The wrap is necessary because tools built via the Relate-form
auto-generators store their schema as a Java Map/List tree, which
JS would otherwise see as host-class members (.put, .get, etc.)
instead of as ordinary object/array entries. Tools authored from a JS
literal already store a Polyglot value and pass through unchanged.
The tool whose input_schema should be JS-wrapped.
Helpers that auto-generate AiTools from Relate form metadata.
Example: ```javascript
const tool = B.ai.tool.custom() .setName("resident_allergies") .setDescription("Lists a resident's known allergies") .setSchema({ type: "object", properties: { id: { type: "string" } }, required: ["id"] }) .setExecutor((args) => allergiesFor(args.id).join(", "));
B.ai.call({ message: B.net.request.parameter("q"), tools: [tool] });