Curiosity

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:

  1. 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.
  2. 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.CurrentUser is 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

© 2026 Curiosity. All rights reserved.