Calling an Agent from an AI Tool
An agent can call another agent — by exposing the inner agent as an AI tool the outer agent (or the main chat assistant) can invoke. There is no built-in InvokeAgent primitive in the workspace; instead you write a thin [Tool] that wraps scope.AgentAI.RunAgentAsync. That's the entire foundation of the sub-agent pattern.
This page covers the wrapper. For end-to-end research / summarisation / aggregation flows built on top of it, see Sub-agent Workflows.
The agent surface on ToolScope
Inside an AI tool, agents are reached through the safe AgentAI accessor on scope. It wraps the workspace's Mosaik.GraphDB.Safe.AgentAI and exposes one method:
Task<AgentRun> scope.AgentAI.RunAgentAsync(
AgentUID agentUID,
string userMessage,
UID128 userUID = default,
UID128 chatUID = default,
IDictionary<string, string> variables = null,
CancellationToken cancellationToken = default);
Differences from the static Mosaik.AI.AgentAI.RunAgentAsync used in endpoints:
- The graph is already bound — no
graph:argument. - It returns a fully-resolved
AgentRun(not just a UID), so you can readrun.Status,run.Result,run.ErrorMessageimmediately. agentUIDis the typedAgentUIDwrapper. Use the auto-generatedAI_Agents.*constants, or wrap a runtime-resolved UID asnew AgentUID(uid).- Always pass
userUID: scope.CurrentUserso the sub-agent inherits the caller's ACL. - Always pass
chatUID: scope.CurrentChatso the sub-agent's run is threaded onto the active chat — that's what makes the run visible in the chat trace, lets the orchestrator stitch citations across nested calls, and groups parent/child runs in the audit log.
Why expose an agent as a tool
| Without a wrapper | With an InvokeAgent wrapper |
|---|---|
| The main chat assistant has to know every tool and every prompt | The assistant sees one focused tool per specialist; the specialist's prompt stays internal |
| Prompt budget grows linearly with each new task | Specialist prompts don't burn the assistant's context |
| Reasoning is shallow — the LLM picks tools one at a time | The specialist is free to loop tools many times before reporting back |
| Hard to swap a sub-task for a different model | Each specialist pins its own ChatTaskUID — use a small model for routing, a large one for synthesis |
The wrapper shape
A generic wrapper that lets the assistant call any configured agent by name. Drop this into a workspace AI tool:
using Mosaik.AI;
using GraphDB.Schema;
public class AgentInvoker
{
[Tool("Delegate a sub-task to a named specialist agent. Use this when the user's " +
"request maps cleanly onto one of: 'TicketTriage', 'KBResearch', 'CaseSummary', " +
"'CustomerDigest'. Returns the specialist's JSON result. Do NOT use for free-form " +
"conversation or anything that isn't covered by the listed specialists.")]
public static async Task<string> InvokeAgent(ToolScope scope,
[Parameter("Name of the specialist agent to invoke. Must be one of the listed names.",
required: true)] string agentName,
[Parameter("The instruction for the specialist, in natural language.",
required: true)] string instruction,
[Parameter("Optional ${variable} bindings for the specialist's prompt. " +
"Pass as a flat object of string keys and string values.",
required: false)] Dictionary<string, string> variables = null)
{
// 1. Resolve the agent by name. Permission-filter so a non-admin can't run
// an agent they're not allowed to see.
var agentUID = scope.Q()
.StartAt(N._Agent.Type)
.Where(N._Agent.Name, agentName)
.AsUIDEnumerable()
.FirstOrDefault();
if (agentUID.IsNull())
return "{\"error\":\"Unknown agent '" + agentName + "'.\"}";
// 2. Run the agent as the calling user, on the same chat thread.
var run = await scope.AgentAI.RunAgentAsync(
agentUID: new AgentUID(agentUID),
userMessage: instruction,
userUID: scope.CurrentUser,
chatUID: scope.CurrentChat,
variables: variables,
cancellationToken: scope.CancellationToken);
if (run is null || run.Status != AgentRunStatus.Completed)
return $"{{\"error\":\"Agent '{agentName}' did not complete: {run?.ErrorMessage}\"}}";
scope.SetToolCallDisplayName($"Delegated to {agentName}");
scope.Logger.LogInformation("sub-agent {Agent} took {Ms}ms",
agentName, (run.Completed - run.Started).TotalMilliseconds);
return run.Result;
}
}
return new AgentInvoker();
Four things to call out:
scope.CurrentUserandscope.CurrentChatpropagate. The sub-agent runs under the same identity as the calling tool, on the same chat thread; tool calls inside the sub-agent are ACL-filtered as that user and their citations land in the parent chat. There is no privilege escalation path throughInvokeAgent.- The agent name is enumerated in the tool description. This is the lever the LLM uses to route. Add or remove names here when you change the agent catalog — the LLM only sees what you tell it.
- Variables are a flat
string → stringmap. The tool runtime advertises the parameter as a JSON object to the LLM, deserialises it into theDictionary<string, string>, and hands it straight toRunAgentAsync. Keep keys to the${VAR}names used in the specialist's system prompt. - The result is the raw JSON from the sub-agent (typically structured because the sub-agent has an
OutputSchema). The outer model now has a typed object to reason about instead of prose.
A typed wrapper for one specialist
When you want stronger contracts — a fixed agent, a fixed OutputSchema, validated arguments — write a per-specialist wrapper instead:
public class KBResearchTool
{
[Tool("Run a deep knowledge-base research pass. Use when the user asks a question that " +
"likely needs cross-referencing multiple articles or product docs. Returns a " +
"ResearchBrief with citations.")]
public static async Task<string> Research(ToolScope scope,
[Parameter("The research question, in the user's own words.", required: true)]
string question,
[Parameter("Optional product SKU to scope the research.", required: false)]
string productSku = null)
{
var variables = new Dictionary<string, string>();
if (!string.IsNullOrWhiteSpace(productSku))
variables["PRODUCT_SKU"] = productSku;
var run = await scope.AgentAI.RunAgentAsync(
agentUID: AI_Agents.KB_Research,
userMessage: question,
userUID: scope.CurrentUser,
chatUID: scope.CurrentChat,
variables: variables,
cancellationToken: scope.CancellationToken);
if (run is null || run.Status != AgentRunStatus.Completed)
return "{\"error\":\"Research did not complete.\"}";
// The KB_Research agent has OutputSchema = ResearchBrief, so run.Result is JSON.
scope.SetToolCallDisplayName("Researched knowledge base");
return run.Result;
}
}
return new KBResearchTool();
This is the idiomatic shape for shipped sub-agents: one tool per specialist, schema-typed result, narrowly scoped description.
When the wrapper lives elsewhere
A wrapper tool doesn't have to be invoked through an outer agent. The same tool can be called:
From the main chat assistant, by simply being in its tool catalog.
From an endpoint, via
RunToolAsync— useful when you want a single HTTP entry that dispatches to one of several specialists:var result = await RunToolAsync<string>( AI_Tools.AgentInvoker, functionName: "InvokeAgent", argumentsJson: new { agentName = "CaseSummary", instruction = req.CaseId, variables = new Dictionary<string, string> { ["DEPTH"] = "deep" } }.ToJson()); return Ok(result.Output, "application/json");From a scheduled task, to fan out a sub-agent over a batch of inputs nightly.
Common pitfalls
Unknown component: alert
Don't expose admin-only specialists through a generic InvokeAgent. The wrapper checks the calling user's permissions, but if the specialist's tools include something that bypasses ACLs (e.g. an admin tool), every caller inherits that privilege. Pin admin sub-agents to admin-only entry points.
[!/alert]
Unknown component: alert
Watch the cumulative timeout. Each RunAgentAsync defaults to 5 minutes. A wrapper called twice in a single chat turn can easily exhaust an HTTP timeout — set the outer endpoint to Pooling mode (see Creating Endpoints) or cap the number of sub-agent calls in the outer prompt.
[!/alert]
Unknown component: alert Don't let sub-agents call back into the same agent. The runtime does not detect recursion. Encode the no-recursion rule in the prompt ("Do not invoke yourself") and, in production, list a closed set of specialists the wrapper accepts. [!/alert]
See also
- Sub-agent Workflows — worked examples that build on this wrapper.
- Creating Agents
- AI Tools — the
[Tool]/[Parameter]surface this page builds on. - Calling from an Endpoint — the other way to invoke an agent.