- name
- uc-ai-multi-agent
- description
- Use when building multi-agent AI systems in Oracle PL/SQL with UC AI — creating agents with uc_ai_agents_api.create_agent, running them via execute_agent with session IDs, sequential/loop/conditional workflow definitions, orchestrator agents that delegate to sub-agents as tools, round-robin or AI-moderated agent conversations, handoff agents with transfer tools and can_transfer_to graphs, input mapping ({$.input.*}, {$.steps.*}), follow-up messages, conversation sessions with titles and feedback, binding a run to values with p_run_context, reaching an agent as a tool with run_agent_as_tool, removing an agent and its history with purge_agent, and debugging via uc_ai_agent_executions, uc_ai_agent_sessions and uc_ai_agent_messages.
# UC AI Multi-Agent Systems
UC AI lets you compose specialized AI agents into workflows, orchestrators, and conversations — all defined in the database and executed through one API: `uc_ai_agents_api.execute_agent()`.
**Reach for agents deliberately.** A single `uc_ai.generate_text()` call with tools is often enough (see the `uc-ai-quickstart` and `uc-ai-tools` skills). Agents add value when you need execution tracking (tokens, timing, audit trail), session grouping across calls, or composition of multiple AI steps.
## Choosing a pattern
| Pattern | Use when... | Example |
|---------|-------------|---------|
| **Profile agent** | A single AI call with execution tracking and composability | Classify a support ticket, answer a question |
| **Sequential workflow** | Steps must run in a fixed order, each building on the previous | Classify text, then summarize based on category |
| **Loop workflow** | Iterative refinement until a quality threshold is met | Generate a haiku, rate it, improve it, repeat |
| **Orchestrator** | A central AI should decide which agents to call and in what order | Travel planner delegating to calendar, flight, and hotel agents |
| **Handoff** | Agents pass control to each other based on their own output | Support triage agent handing off to a technical or sales specialist |
| **Round-robin conversation** | Multiple perspectives should take turns in a fixed order | Brainstormer, critic, and synthesizer collaborate on a plan |
| **AI-driven conversation** | A moderator should dynamically decide who speaks next | Panel discussion where the moderator picks the most relevant expert |
Deep dives sit next to this file: **workflows.md** (sequential/conditional/loop JSON), **orchestrator.md** (delegation config), **conversations.md** (round-robin and AI-driven dialogue).
An agent that must remember across conversations gets a memory store — see the `uc-ai-agent-memory` skill.
## The building block: profile agents
Every AI-calling agent wraps a **prompt profile** that defines its instructions, provider, and model (see the `uc-ai-prompt-profiles` skill or https://www.united-codes.com/products/uc-ai/docs/guides/prompt-profiles/). Higher-level patterns (workflows, orchestrators, conversations) compose profile agents by their `agent_code`.
The `create_agent` signature (from `uc_ai_agents_api`):
```sql
function create_agent(
p_code in uc_ai_agents.code%type,
p_description in uc_ai_agents.description%type,
p_agent_type in uc_ai_agents.agent_type%type,
p_prompt_profile_code in uc_ai_agents.prompt_profile_code%type default null,
p_prompt_profile_version in uc_ai_agents.prompt_profile_version%type default null,
p_workflow_definition in uc_ai_agents.workflow_definition%type default null,
p_orchestration_config in uc_ai_agents.orchestration_config%type default null,
p_input_schema in uc_ai_agents.input_schema%type default null,
p_output_schema in uc_ai_agents.output_schema%type default null,
p_timeout_seconds in uc_ai_agents.timeout_seconds%type default null,
p_max_iterations in uc_ai_agents.max_iterations%type default null,
p_max_history_messages in uc_ai_agents.max_history_messages%type default null,
p_version in uc_ai_agents.version%type default 1,
p_status in uc_ai_agents.status%type default c_status_draft
) return uc_ai_agents.id%type;
```
Agent type constants: `uc_ai_agents_api.c_type_profile`, `c_type_workflow`, `c_type_orchestrator`, `c_type_handoff`, `c_type_conversation`. Status constants: `c_status_draft`, `c_status_active`, `c_status_archived`. **Always use these constants, never string literals.**
Complete example — profile + agent, activated:
```sql
declare
l_profile_id number;
l_agent_id number;
begin
l_profile_id := uc_ai_prompt_profiles_api.create_prompt_profile(
p_code => 'geo_assistant'
, p_description => 'Answers geography questions'
, p_system_prompt_template => 'You are a geography assistant. Answer in one short sentence.'
, p_user_prompt_template => '{question}'
, p_provider => uc_ai.c_provider_openai
, p_model => uc_ai_openai.c_model_gpt_5_6_luna
, p_status => uc_ai_prompt_profiles_api.c_status_active
);
l_agent_id := uc_ai_agents_api.create_agent(
p_code => 'geo_agent'
, p_description => 'Answers geography questions in one sentence'
, p_agent_type => uc_ai_agents_api.c_type_profile
, p_prompt_profile_code => 'geo_assistant'
, p_status => uc_ai_agents_api.c_status_active
);
commit;
end;
/
```
The agent uses the latest active version of the profile unless pinned with `p_prompt_profile_version`. Agents are versioned too: `create_new_version(p_code, p_source_version)` copies an agent, `change_status(...)` activates or archives.
**Removing an agent.** `delete_agent` only works while an agent has no history.
`uc_ai_agents_api.purge_agent(p_code)` removes every version of an agent, its
runs, the runs started from them, its sessions, the messages of both, and the
memory that belongs only to this agent. It keeps what other things also use: a
shared or global memory store, a session another agent opened, and the prompt
profile. It raises when another agent references this one (an orchestrator
delegate, a workflow step), and it does not commit. To keep the history, archive
the agent instead: `change_status(p_code, p_version, uc_ai_agents_api.c_status_archived)`.
A tool whose PL/SQL text names the agent stays — delete that tool yourself.
## Executing agents
All agent types share the same API:
```sql
function execute_agent(
p_agent_code in uc_ai_agents.code%type,
p_agent_version in uc_ai_agents.version%type default null,
p_input_parameters in json_object_t default null,
p_follow_up_message in clob default null,
p_session_id in varchar2 default null,
p_parent_exec_id in uc_ai_agent_executions.id%type default null,
p_response_schema in json_object_t default null,
p_files in uc_ai_message_api.t_files default null,
p_extra_tool_tag in varchar2 default null,
p_run_context in json_object_t default null
) return json_object_t;
```
(An overload taking `p_agent_id` instead of code/version exists as well.)
```sql
declare
l_result json_object_t;
l_session_id varchar2(100);
begin
-- API key: uc_ai_get_key function or uc_ai_openai.g_apex_web_credential := 'OPENAI';
l_session_id := uc_ai_agents_api.generate_session_id;
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'geo_agent'
, p_input_parameters => json_object_t('{"question": "What is the capital of France?"}')
, p_session_id => l_session_id
);
dbms_output.put_line('Answer: ' || l_result.get_clob('final_message'));
end;
/
```
- `p_input_parameters` keys map to `{placeholder}` names in the prompt profile templates (for workflows/conversations, they feed `{$.input.*}` mappings).
- `p_session_id` groups related executions — one workflow run with three steps produces multiple rows in `uc_ai_agent_executions` under the same session. Generate one with `uc_ai_agents_api.generate_session_id` (SYS_GUID-based).
- The result is a `json_object_t` in the `generate_text` shape: `final_message` (clob), `messages`, `usage`, `finish_reason` — plus pattern-specific keys (workflows add `_workflow_iterations`, orchestrators expose `tool_calls_count`, handoffs add `handoff_count` and `conversation_history`).
- `p_files` sends documents and images along with the text (profile and orchestrator agents). The files attach to the first user message, or to the follow-up message. See the `uc-ai-file-analysis` skill.
- `p_run_context` binds name/value pairs to the run — see [Run context](#run-context).
- `p_response_schema` validates the response against a JSON schema (profile agents only).
### Follow-up messages (multi-turn)
Profile and orchestrator agents support conversation continuation. Pass `p_follow_up_message` **with the same `p_session_id`** — the previous conversation is loaded from the session and your message is appended:
```sql
declare
l_result json_object_t;
l_session_id varchar2(100);
begin
-- API key: uc_ai_get_key function or uc_ai_openai.g_apex_web_credential := 'OPENAI';
l_session_id := uc_ai_agents_api.generate_session_id;
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'geo_agent'
, p_input_parameters => json_object_t('{"question": "What are the 5 largest cities in Europe?"}')
, p_session_id => l_session_id
);
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'geo_agent'
, p_follow_up_message => 'Which of those have the best public transport?'
, p_session_id => l_session_id
);
dbms_output.put_line(l_result.get_clob('final_message'));
end;
/
```
To cap token usage in long conversations, set `p_max_history_messages` on the agent — the system then keeps the system message plus the most recent N messages.
## Handoff agents
`c_type_handoff` transfers control between agents with **tools**. The engine registers a temporary `transfer_to_<agent>` tool for each allowed target. The AI transfers by calling one with a context summary, and the target agent answers the user directly.
```sql
l_config := '{
"initial_agent_code": "support_triage",
"handoff_agents": [
{"agent_code": "support_triage", "description": "Triage and general support"},
{"agent_code": "support_product", "description": "Product specs, prices, availability"},
{"agent_code": "support_shipping", "description": "Shipping options, costs, delivery times"}
],
"max_handoffs": 3
}';
l_id := uc_ai_agents_api.create_agent(
p_code => 'customer_support'
, p_description => 'Customer support entry point'
, p_agent_type => uc_ai_agents_api.c_type_handoff
, p_orchestration_config => l_config
, p_status => uc_ai_agents_api.c_status_active
);
```
**Restricting the transfer graph.** Add `can_transfer_to` to an entry to limit its outgoing edges. Use it for hierarchies — triage reaches product support, and only product support reaches the product technician:
```json
{"agent_code": "support_triage", "description": "...", "can_transfer_to": ["support_product", "support_shipping"]}
```
Without `can_transfer_to`, every agent may transfer to every other one (full mesh).
**Multi-turn (sticky agent).** Handoff agents accept `p_follow_up_message`. The follow-up turn resumes with the agent that answered the previous turn and continues its history. It keeps its transfer tools, so it can hand off again when the topic changes.
**Results.** The result object carries `final_agent_code`, `handoff_count`, `handoff_trail`, and `max_handoffs_reached`. The hop at `max_handoffs` runs without transfer tools and must answer — a graceful cap, not an error. Transfer tool calls are persisted in the session message log.
## Run context
`p_run_context` binds name/value pairs to a run, for example the document the
conversation is about. UC AI hands them to every tool of the run under the
reserved key `_ctx`, so a tool reads a value the model can neither see nor
choose. Use it whenever an agent must stay inside one document, one case, or one
tenant.
```sql
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'doc_agent'
, p_input_parameters => json_object_t('{"question": "What are the payment terms?"}')
, p_session_id => l_session_id
, p_run_context => json_object_t('{"document_id": "7", "tenant_id": "ACME"}')
);
```
- **Nested runs inherit it.** A workflow step, an orchestrator delegate and a handoff target all run under the same binding, and none of them can drop it. A `p_run_context` on a `generate_text` call inside the run is ignored.
- **A session keeps its binding.** The first turn binds the context to the session, so a follow-up turn does not pass it again. A later turn can add a key but cannot change one — a change raises `ORA-20507`.
- **It fills prompt placeholders.** A `{document_id}` placeholder in the profile template resolves from the run context when `p_input_parameters` does not supply it. An input parameter of the same name wins.
- **It is recorded.** `uc_ai_agent_executions.run_context` and `uc_ai_agent_sessions.run_context` hold the effective bag.
- Tool-handler side and the `_ctx` shape: see the `uc-ai-tools` skill.
## An agent as a tool
`uc_ai_agents_api.run_agent_as_tool` is the whole handler of a tool that runs
another agent. Register the tool, and every agent, workflow step and
`generate_text` call reaches that agent like any other tool:
```sql
l_schema := json_object_t('{
"type": "object",
"properties": {
"question": { "type": "string", "description": "The geography question to answer" }
},
"required": ["question"]
}');
l_tool_id := uc_ai_tools_api.merge_tool_from_schema(
p_tool_code => 'ASK_GEO_AGENT'
, p_description => 'Answers a geography question. Example parameters: {"question": "Capital of France?"}'
, p_function_call => 'return uc_ai_agents_api.run_agent_as_tool(''geo_agent'', :parameters);'
, p_json_schema => l_schema
, p_tags => apex_t_varchar2('geo')
);
```
The function takes the tool arguments as the input parameters of the agent, hands
it the run context of the caller, joins the session of the caller, records the run
as a child of the calling run, and returns the final message as text. A failed run
comes back as text instead of raising, so the calling model reads the error and
can react to it. The orchestrator pattern uses the same handler for its delegate
tools.
Outside an agent run there is no session to join. A `session_id` key in the run
context is then read as the session to group the run with.
## Input mapping cheat sheet
View on GitHub