Author an LLM agent
An agent is a prompt + an LLM provider/model + typed input/output ports + a list
of catalog nodes it may call as tools, declared entirely inside plugin.yaml —
there is no behavior module to write for the agent itself. This recipe adds one
to an existing plugin and gets it loaded and runnable end to end.
It assumes a scaffolded plugin (see Scaffold a plugin) with the types your ports will reference already registered (see Add vocabulary and types and TypeRef). For the manifest schema in full, see Plugin manifest; for wiring the resulting node into a workflow, see Use agents in a workflow.
Choose the ports and tools
- Pick a type for each input and output port. The base type name must resolve
to one of seven primitives —
String,Number,Decimal,Integer,Boolean,Date,DateTime— or the name of a registeredobjecttype from your (or adependsOn) plugin'scontributes.types(libs/shared/src/domain/agent-spec.ts:PRIMITIVES,resolveTypeName).Time,Duration,Code,Uri,coded, andquantityare not resolvable directly on an agent port — wrap them inside anobjecttype first if you need one. Addlist: trueon a port that carries a list of that type. - Pick the tools: any node id already in the catalog whose
kindis nottrigger— a node without abehavior, or a trigger, cannot be invoked as a tool (libs/engine-core/src/engine/agents.ts,validateAgent). A tool always runs with an emptyconfig(libs/engine-core/src/engine/catalog.ts:runTool({ inputs: args, config: {}, context })), so don't pick a tool whose behavior needs its own node config to do anything useful.
Declare the agent in plugin.yaml
Add an entry under
contributes.agentswith aname, aprovider, amodel, aprompt, at least one input and one output port, and atoolslist (libs/shared/src/plugin/manifest.ts,AgentContribSchema):contributes: agents: - name: Renal triage description: Estimates renal function from a creatinine result. provider: anthropic model: claude-sonnet-4-5 maxIterations: 6 inputs: - { name: result, type: Observation, description: The creatinine result. } outputs: - { name: assessment, type: Condition, description: The proposed condition, if warranted. } tools: [patient.load-demographics, acme.egfr.ckd-epi-2021] prompt: | You are a nephrologist. From the creatinine result, estimate renal function (use the tools) and propose a Condition if warranted.provideris a plain string —anthropic,openai,gemini,mistral,azure-openaiandlocalare built in (libs/llm-client/src/index.ts,providerConfig), and an instance may declare its own names, one per endpoint, inports.agent.config.providers(see Instance manifest). A name that is neither is refused when the agent is saved and logged at boot, naming the providers the instance does know.list: trueon a port carries a list of the base type — the@posos/demo-softwayagent uses it for a list of extracted strings (external-plugins/demo-softway/plugin.yaml).The older single-type shorthand —
input: Observation/output: Condition, one implicit port namedinput/output— is still accepted and normalized into the array form above (libs/shared/src/plugin/manifest.ts, theAgentContribSchemapreprocess step);external-plugins/geriatrie/plugin.yamlstill uses it. Prefer the named-ports form for anything new: it supports more than one input or output, each with its own description.Mind what
namebecomes: the node type id isagent.<slug(name)>(lower-cased, accents stripped, non-alphanumerics collapsed to-) (apps/api/src/plugins.ts,slugand the agents-loading loop) — "Renal triage" becomesagent.renal-triage. Renaming the agent later changes this id and breaks any workflow already wired to it.
Write the prompt
prompt is sent to the model as-is; the runner appends its own instructions
describing the available tools and a terminal submit_result tool whose
schema is generated from your outputs (external-plugins/llm/adapters/runner.ts,
systemPrompt/buildTools). Write the prompt as pure domain guidance — what
to look for, when to call which tool, when to abstain — and don't tell the
model to answer in free text: only a submit_result call is accepted as the
final answer.
maxIterations caps the number of model↔tool round-trips (default 8 if
omitted — external-plugins/llm/adapters/runner.ts, DEFAULT_MAX_ITERS). A
run that exhausts the budget without calling submit_result fails explicitly
(pas de réponse finale après N tours.) instead of returning a partial
answer — raise the value for agents that legitimately need many tool calls,
keep it low for anything cost- or latency-sensitive.
Build, and check what the loader won't
- Run
meridian-plugin gen-typesonly if this change also touchedcontributes.typesor a node'sbehavior— an agent contributes neither, so on its own it has nothing to generate (see Generate types from the manifest). - Run
meridian-plugin build. It bundles behavior/adapter/event-source modules and copiesplugin.yaml/i18n/intodist/(libs/plugin-cli/src/commands/build.ts) — an agent has no module of its own, so adding one never adds anything to bundle, but you still need this step whenever the plugin has other nodes. meridian-plugin validate <workflow.yaml>does not know about agent nodes: it only registers metadata from the manifests you pass it, so a workflow node of kindagent.*is reported as unknown (libs/plugin-cli/src/commands/validate.ts, header comment). Check a workflow that uses your agent by loading the plugin into a running instance instead, and using its own validate/simulate — see Validate a workflow and Simulate a workflow.- Two mistakes the loader accepts silently, so verify them by eye: a
toolsentry that doesn't resolve to a runnable catalog node id is dropped from the tool list rather than rejected (libs/engine-core/src/engine/catalog.ts:if (!toolNode?.run) return [];), and a porttypethat doesn't resolve (see the type list above) falls back to the untypedanyrather than failing (libs/shared/src/domain/agent-spec.ts,agentPortType/resolveTypeName). A typo in either place fails silently, not loudly.
Select the runner adapter on the target instance
Executing an agent node needs the agent port bound to an adapter — normally
@posos/llm's agent/llm adapter, or another one implementing the same
port (external-plugins/llm/plugin.yaml; the contract is AgentRunnerPort in
Port contracts). This is instance
configuration, not something your plugin declares:
ports:
agent: { adapter: llm }
Without a matching provider API key in the environment, running the node
fails explicitly rather than falling back to a mock
(external-plugins/llm/adapters/runner.ts: Provider « … » non configuré (clé API en variable d'environnement manquante).). See
Configure ports and adapters
for wiring the port, Declare and install plugins
for getting @posos/llm onto the instance in the first place, and
Environment variables
for the per-provider key/base-URL variables (ANTHROPIC_API_KEY,
OPENAI_API_KEY, GEMINI_API_KEY/GOOGLE_API_KEY, MISTRAL_API_KEY,
AZURE_OPENAI_API_KEY, LOCAL_LLM_API_KEY, …).
Localize it, if needed
The agent's description and each port's description are ordinary
translatable manifest strings (an i18n/<locale>.yaml overlay) — but name
is not: it determines the node type id, so translating it would change that
id and break wiring (libs/shared/src/plugin/i18n.ts). See
Translate a plugin.
Confirm it loaded
Restart the host process, check the boot log lists your plugin, then look for
agent.<slug> in /api/catalog or the editor palette — agent nodes are
grouped under their own category (libs/engine-core/src/engine/agents.ts,
agentToNodeMeta). From there, wiring it into a workflow is
Use agents in a workflow.
Related
- Plugin manifest — the full
plugin.yamlschema, includingAgentContribSchema. - TypeRef and Add vocabulary and types — where the types your ports reference come from.
- Port contracts — the
agentport'sAgentRunnerPort/AgentToolHandleshapes. - Implement a node behavior — writing the tool nodes an agent calls.
- Use agents in a workflow
— wiring the resulting
agent.<slug>node into a workflow. - Validate a workflow and Simulate a workflow — checking a workflow that includes an agent node.
- Configure ports and adapters
— binding
ports.agenton an instance. - Translate a plugin — localizing the agent's description and port descriptions.
- Plugins concept — why agents are a manifest-declared, code-free contribution point.