Creating Agents
An agent ties together three things: a system prompt (with optional variables), a tool set (a subset of the workspace's AI tools), and a chat model. You author one in the workspace UI under Management → Agents, and the runtime stores it as an _Agent graph node.
This page is the reference for the dialog and for the exported-as-code form.
The dialog
| Field | What it controls |
|---|---|
| Name | Human-readable label. Used in the runs list, the audit log, and as the tool name if you expose the agent as an InvokeAgent sub-agent. |
| Description | One-line summary of what the agent does. Becomes the LLM-facing description when used as a sub-agent. |
| Icon | UIcon name (e.g. ticket, magnifying-glass). Surfaced in the UI. |
| Model | The chat model. Backed by a scheduled-task UID (ChatTaskUID). Leave empty to fall back to the user's default provider. |
| System prompt | The instruction text. Supports ${VAR} placeholders — see Variable substitution. |
| Tools | Any subset of the workspace's AI Tools. Pick the smallest set that lets the agent do its job. |
| Output schema | Optional. The name of a C# class marked with [AgentOutputSchema]. Forces the model to emit JSON that conforms to that schema. |
All fields except Name and System prompt are optional, but you almost always want to set the model and a focused tool set explicitly.
The system prompt
The prompt is the agent's contract with the model. The same rules that apply to good tool descriptions apply here — be specific about intent, scope, and output convention. Two things are specific to agents:
- Tools must be named in the prompt if you want the model to favour them. The runtime advertises tools to the model, but giving them an explicit cue ("use SearchTickets first") makes routing reliable.
- The prompt is the only place to constrain behaviour. Agents have no per-turn moderation hook; if a behaviour is forbidden, say so in the prompt.
Variable substitution
The prompt body supports ${VAR} placeholders. The runtime substitutes them at call time from the variables dictionary passed to RunAgentAsync. Unknown variables are left in place verbatim, so a typo in the caller doesn't blow up the run.
You are a support assistant for ${PRODUCT}.
The current user's locale is ${LOCALE}. Respond in that language
unless the question is itself in another language.
When citing knowledge-base articles, use the bracketed snippet id
convention, e.g. [1].
Variable names follow the regex [a-zA-Z_][a-zA-Z0-9_]* — letters, digits, and underscores.
Unknown component: alert
Keep variables bounded — enums, IDs, locale codes — not free text. Passing raw user input through ${VAR} is functionally fine but defeats the purpose: that's what the user message is for.
[!/alert]
Choosing the model
The ChatTaskUID points at a chat-AI scheduled task that defines the provider, model name, temperature, and other generation parameters. Configure providers in Management → Chat AI; see LLM Configuration for the full provider surface.
Picking a model:
| Workload | Model class |
|---|---|
| Routing, classification, short JSON outputs | Small / fast (Haiku-class) |
| Multi-step research, long synthesis, structured output | Larger (Sonnet-class) |
| Code generation, complex reasoning | Largest available |
Leaving ChatTaskUID empty resolves to the calling user's default provider at run time, which is convenient for prototypes but unpredictable in production — always pin a model for shipped agents.
Attaching tools
An agent's tool list is just edges from the _Agent node to existing _ChatAITool nodes. There is no separate "agent-only" tool concept; an agent re-uses the same [Tool]-annotated classes documented in AI Tools.
Two important properties carry over from the tool layer:
- Permission filtering. At run time the runtime drops any tool the calling user can't access (
_ChatAITool.IsAccessible(...)). An agent that lists 10 tools may only see 6 of them for a non-admin user — design for that. scope.CurrentUseris the agent's caller, not the agent itself. Every tool call inside an agent run is ACL-filtered as if the user were calling the tool directly from chat.
Unknown component: alert Never build an admin-only escape hatch into an agent's tool set. If admin-level data needs to participate, run the agent inside an endpoint that has already authenticated the caller as an admin and pass that identity through — don't smuggle elevated tools. [!/alert]
Structured output (OutputSchema)
For agents whose result you'll consume programmatically, force a JSON shape. Define a class in an Imported endpoint and mark it:
using Mosaik.AI;
[AgentOutputSchema]
public record TriageDecision(
string Category, // "Hardware" | "Software" | "Billing" | "Other"
string Severity, // "Low" | "Medium" | "High" | "Critical"
string ProposedAction,
string[] CitedArticleIds);
Then put TriageDecision in the agent's Output schema field. The runtime advertises the schema to the model and validates the response — a malformed reply triggers a retry and, if still malformed, ends the run as Failed.
Consume it on the caller side:
var runUID = await AgentAI.RunAgentAsync(...);
var run = Graph.GetReadOnlyContent<_AgentRun>(runUID);
var decision = run.Result.FromJson<TriageDecision>();
Without an output schema the model returns prose, which is fine for end-user-facing summaries and bad for everything else.
Exporting agents as code
Agents are exportable from the management UI. The export format is plain text, designed to live in git alongside endpoints:
[agent: Curiosity.Agents.UID("01HZ…")]
[agent: Curiosity.Agents.Name("Ticket Triage")]
[agent: Curiosity.Agents.Description("Categorise a support ticket and propose the next action.")]
[agent: Curiosity.Agents.Icon("ticket")]
[agent: Curiosity.Agents.ChatTask("01HQ…")]
[agent: Curiosity.Agents.OutputSchema("TriageDecision")]
[agent: Curiosity.Agents.Tool("01J0…")] // SearchTickets
[agent: Curiosity.Agents.Tool("01J1…")] // SearchKB
You triage incoming support tickets for ${PRODUCT}.
Given the ticket body, call SearchTickets to find similar past cases
and SearchKB to find resolution articles. Then return a TriageDecision.
Never invent resolutions — only suggest articles you have cited.
The header attributes are repeatable for Tool(...); everything below the blank line is the system prompt body.
Follow the same promotion workflow as endpoints: export from dev, commit, import on staging and production.
Testing
The management UI offers an inline test pane: type a user message, optionally set variables, hit Run. The pane shows the resolved prompt, every tool call (with arguments and JSON results), and the final answer. Every run is also persisted to _AgentRun, so you can revisit the trace later from Management → Agents → Runs.
For automated tests, drive the agent from a Sync endpoint and assert on the parsed OutputSchema result — that gives you a deterministic harness without dealing with the run-list UI.
See also
- Calling from an Endpoint
- Calling from an AI Tool
- Sub-agent Workflows
- AI Tools — the tool surface agents call into.
- LLM Configuration — configuring chat providers.