# E.D.D.I Documentation

Multi-Agent Orchestration Middleware for Conversational AI — coordinate multiple AI agents, business systems, and conversation flows through configuration, not code.

Welcome to the official documentation for **E.D.D.I** (Enhanced Dialog Driven Interface) — a production-grade multi-agent orchestration middleware for conversational AI.

**Latest version: 6.0.0** · License: Apache 2.0 · [GitHub](https://github.com/labsai/EDDI) · [Website](https://eddi.labs.ai/)

***

## What Is EDDI?

EDDI coordinates between users, AI agents (LLMs), and business systems. It provides intelligent routing, conversation management, and API orchestration — all through **versioned JSON configurations**, not code.

Built with **Java 25** and **Quarkus**. Ships as a **Red Hat-certified Docker image**. Supports **MongoDB or PostgreSQL**. Deploy on Docker, Kubernetes, or OpenShift.

***

## Start Here

| Guide                                                                      | Time   | Description                                        |
| -------------------------------------------------------------------------- | ------ | -------------------------------------------------- |
| 🚀 [**Getting Started**](/getting-started/getting-started)                 | 5 min  | Install EDDI and run your first agent              |
| ⚡ [**Developer Quickstart**](/getting-started/developer-quickstart)        | 10 min | Build a complete agent step-by-step via REST API   |
| 🏗️ [**Architecture Overview**](/architecture-and-concepts/architecture)   | 15 min | Understand the lifecycle pipeline and config model |
| 🧩 [**Putting It All Together**](/getting-started/putting-it-all-together) | 20 min | Real-world hotel booking agent walkthrough         |

***

## Key Capabilities

### 🤖 Multi-Agent Orchestration

* **12 LLM Providers** — OpenAI, Anthropic, Google Gemini, Mistral AI, Azure OpenAI, Amazon Bedrock, Oracle GenAI, Vertex AI, Ollama, Jlama, Hugging Face, plus OpenAI-compatible endpoints
* [**Group Conversations**](/conversations-and-orchestration/group-conversations) — Multi-agent debates (Round Table, Peer Review, Devil's Advocate, Delphi, Debate)
* [**Managed Agents**](/conversations-and-orchestration/managed-agents) — Intent-based auto-routing with one conversation per user per intent
* [**Model Cascading**](/agent-configuration/model-cascade) — Cost-optimized multi-model routing with confidence-based escalation

### 🔗 Protocols & Interoperability

* [**MCP Server**](/protocols-and-integration/mcp-server) (48+ tools) — Full EDDI control from Claude Desktop, IDE plugins, or any MCP client
* [**A2A Protocol**](/protocols-and-integration/a2a-protocol) — Agent-to-Agent peer communication with skill discovery

### 🧠 Intelligence & Memory

* [**LLM Integration**](/agent-configuration/langchain) — Connect any of 12 providers with agent mode and tool calling
* [**RAG**](/agent-configuration/rag) — 8 embedding providers, 6 vector stores, plus zero-infrastructure httpCall RAG
* [**Persistent User Memory**](/architecture-and-concepts/user-memory) — Agents remember facts across conversations
* [**Properties**](/architecture-and-concepts/properties) — Config-driven slot-filling and importance extraction

### 🔐 Enterprise Security

* [**Secrets Vault**](/security-and-compliance/secrets-vault) — Envelope encryption (AES-256-GCM + PBKDF2) for API keys
* [**Security**](/security-and-compliance/security) — SSRF protection, sandboxed evaluation, Keycloak auth
* [**Audit Ledger**](/security-and-compliance/audit-ledger) — Write-once trail with HMAC integrity for EU AI Act compliance

***

## Agent Configuration

Build agent behavior by composing these extensions:

| Extension             | Purpose                                              | Guide                                                       |
| --------------------- | ---------------------------------------------------- | ----------------------------------------------------------- |
| **Behavior Rules**    | Decision-making logic — IF conditions THEN actions   | [→ Guide](/agent-configuration/behavior-rules)              |
| **HTTP Calls**        | Call external REST APIs with templated requests      | [→ Guide](/agent-configuration/httpcalls)                   |
| **LLM Integration**   | Chat, agent mode, tool calling with any provider     | [→ Guide](/agent-configuration/langchain)                   |
| **Output**            | Define what the agent says, with alternatives        | [→ Guide](/agent-configuration/output-configuration)        |
| **Output Templating** | Dynamic responses using Qute templates               | [→ Guide](/agent-configuration/output-templating)           |
| **Properties**        | Extract and store structured data from conversations | [→ Guide](/architecture-and-concepts/properties)            |
| **Semantic Parser**   | Map user input to expressions via dictionaries       | [→ Guide](/agent-configuration/semantic-parser)             |
| **Context**           | Inject external data from your application           | [→ Guide](/agent-configuration/passing-context-information) |

***

## Deployment & Operations

| Topic                   | Guide                                                                          |
| ----------------------- | ------------------------------------------------------------------------------ |
| 🐳 Docker               | [→ Guide](/deployment-and-infrastructure/docker)                               |
| ☸️ Kubernetes & Helm    | [→ Guide](/deployment-and-infrastructure/kubernetes)                           |
| 🔴 Red Hat & OpenShift  | [→ Guide](/deployment-and-infrastructure/redhat-openshift)                     |
| ☁️ AWS + MongoDB Atlas  | [→ Guide](/deployment-and-infrastructure/setup-eddi-on-aws-with-mongodb-atlas) |
| 📊 Metrics & Monitoring | [→ Guide](/deployment-and-infrastructure/metrics)                              |
| 📋 Log Administration   | [→ Guide](/deployment-and-infrastructure/log-administration)                   |
| 🔖 Release & Versioning | [→ Guide](/deployment-and-infrastructure/release-versioning)                   |

***

## Quick Start

```bash
# One-command install (interactive wizard)
curl -fsSL https://raw.githubusercontent.com/labsai/EDDI/main/install.sh | bash

# Or pull and run directly
docker pull labsai/eddi:latest
docker compose up
```

Then open <http://localhost:7070> to access the Manager Dashboard.

See [**Getting Started**](/getting-started/getting-started) for all setup options.

***

## Browse All Documentation

See the full [**Table of Contents**](https://github.com/labsai/EDDI/blob/main/docs/SUMMARY.md) for the complete documentation index.

**Have a question?** Check the [**FAQs**](/reference/how-to...) for common setup and configuration answers.


# Getting Started

**Version: 6.2.0**

Welcome to **EDDI**!

This article will help you to get started with **EDDI**.

## What You're Installing

EDDI is a **middleware orchestration service** for conversational AI. When you run EDDI, you're starting:

1. **The EDDI Service**: A Java/Quarkus application that exposes REST APIs for agent management and conversations
2. **MongoDB**: A database that stores agent configurations, packages, and conversation history
3. **Optional UI**: A web-based dashboard for managing agents (accessible at <http://localhost:7070>)

Once running, you can:

* Create and configure agents through the API or dashboard
* Integrate agents into your applications via REST API
* Connect to LLM services (OpenAI, Claude, Gemini, etc.)
* Build complex conversation flows with behavior rules
* Call external APIs from your agent logic

## Installation Options

### Option 0 - One-Command Install (Recommended)

**Linux / macOS / WSL2:**

```bash
curl -fsSL https://raw.githubusercontent.com/labsai/EDDI/main/install.sh | bash
```

**Windows (PowerShell):**

```powershell
Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/labsai/EDDI/main/install.ps1" -OutFile "install.ps1"
Unblock-File .\install.ps1
.\install.ps1
```

The wizard guides you through choosing a database (MongoDB or PostgreSQL), optional authentication (Keycloak), and monitoring (Grafana). After setup, Agent Father is deployed automatically to help you create your first AI agent.

### Option 1 - EDDI with Docker (Manual)

There are two ways to use `Docker` with **EDDI**, either with **`docker-compose`** or launch the container manually.

***Prerequisite**: You need an up and running `Docker` environment. (For references, see:* <https://docs.docker.com/learn/>)

### Use docker-compose (recommended)

1. `Checkout` the `docker-compose` file from `Github`:[`https://github.com/labsai/EDDI/blob/main/docker-compose.yml`](https://github.com/labsai/EDDI/blob/main/docker-compose.yml)
2. Run Docker Command:

   ```
    docker-compose up
   ```

### Use launch docker containers manually

1. Create a shared network

   ```
   docker network create eddi-network
   ```
2. Start a `MongoDB` instance using the `MongoDB` `Docker` image:

   ```
   docker run --name mongodb --network=eddi-network -d mongo
   ```
3. Start **EDDI** :

   ```
   docker run --name eddi --network=eddi-network -p 7070:7070 -d labsai/eddi
   ```

## Option 2 - Deploy on Kubernetes

EDDI runs natively on any Kubernetes cluster (minikube, kind, GKE, EKS, AKS).

**Quickstart (all-in-one):**

```bash
kubectl apply -f https://raw.githubusercontent.com/labsai/EDDI/main/k8s/quickstart.yaml
bash k8s/create-secrets.sh  # generate vault key
```

**Using Kustomize overlays:**

```bash
kubectl apply -k k8s/overlays/mongodb/    # MongoDB backend
kubectl apply -k k8s/overlays/postgres/   # PostgreSQL backend
```

**Using Helm:**

```bash
helm install eddi ./helm/eddi --namespace eddi --create-namespace
```

See the [Kubernetes Deployment Guide](/deployment-and-infrastructure/kubernetes) for full details including auth, monitoring, NATS, Ingress, and production hardening.

## Option 3 - Run from Source

#### *Prerequisites:*

* Java 25
* Maven 3.9+
* MongoDB ≥ 6.0 (or PostgreSQL)

### How to run the project

Setup a local MongoDB (≥ 6.0) or PostgreSQL instance.

> **Note:** If no database instance is available, Quarkus Dev Services will try to start a container automatically (requires Docker running on the host).

On a terminal, under project root folder, run the following command:

```shell
./mvnw compile quarkus:dev
```

1. Go to Browser --> <http://localhost:7070>

### Build App & Docker image

```bash
./mvnw clean package '-Dquarkus.container-image.build=true'
```

### Download from Docker hub registry

```bash
docker pull labsai/eddi
```

<https://hub.docker.com/r/labsai/eddi>

### Run Docker image

For production, launch standalone mongodb and then start an eddi instance as defined in the docker-compose file

```bash
docker-compose up
```

For development, use

```bash
docker-compose -f docker-compose.yml -f docker-compose.local.yml up
```

For integration testing run

```bash
./mvnw verify -DskipITs=false
```

This uses Testcontainers to automatically start EDDI + MongoDB/PostgreSQL in Docker containers for E2E testing. Requires Docker to be running.


# Developer Quickstart Guide

**Version: 6.2.0**

This guide helps developers quickly understand EDDI's architecture and start building agents.

## Understanding EDDI in 5 Minutes

### What EDDI Is

EDDI is **middleware for conversational AI**—it sits between your app and AI services (OpenAI, Claude, etc.), providing:

* **Orchestration**: Control when and how LLMs are called
* **Business Logic**: IF-THEN rules for decision-making
* **State Management**: Maintain conversation history and context
* **API Integration**: Call external REST APIs from agent logic

### Key Concept: The Lifecycle Pipeline

Every user message goes through a **pipeline of tasks**:

```
Input → Parser → Rules → API/LLM → Output
```

Each task transforms the **Conversation Memory** (a state object containing everything about the conversation).

### Agent Composition

Agents aren't code—they're **JSON configurations**:

```
Agent (list of packages)
  └─ Workflow (list of extensions)
      ├─ Behavior Rules (.behavior.json)
      ├─ HTTP Calls (.httpcalls.json)
      ├─ LangChain (.langchain.json)
      └─ Output Templates (.output.json)
```

## Quick Setup

### Prerequisites

* Java 25
* Maven 3.8.4
* MongoDB 6.0+
* Docker (optional, recommended)

### Run with Docker (Easiest)

```bash
# Clone repo
git clone https://github.com/labsai/EDDI.git
cd EDDI

# Start EDDI + MongoDB
docker-compose up

# Access dashboard
open http://localhost:7070
```

### Run from Source

```bash
# Clone repo
git clone https://github.com/labsai/EDDI.git
cd EDDI

# Start MongoDB (or use Docker)
# On Mac: brew services start mongodb-community
# On Linux: sudo systemctl start mongod

# Run EDDI in dev mode
./mvnw compile quarkus:dev

# Access dashboard
open http://localhost:7070
```

> **💡 Secrets Vault:** If you plan to store API keys through the Manager UI or use `${vault:...}` references, set the vault master key first:
>
> ```bash
> export EDDI_VAULT_MASTER_KEY=my-dev-passphrase   # Linux/macOS
> $env:EDDI_VAULT_MASTER_KEY = "my-dev-passphrase"  # Windows PowerShell
> ```
>
> Without this, the vault is disabled and secret endpoints return HTTP 503. Any passphrase works for local dev. See [Secrets Vault](/security-and-compliance/secrets-vault) for full details.

### Configuring AI Tools

If you plan to use the **Web Search** or **Weather** tools in your agents, you need to set up API keys in your environment or `application.properties`.

**Web Search (Google):**

* `eddi.tools.websearch.provider=google`
* `eddi.tools.websearch.google.api-key=...`
* `eddi.tools.websearch.google.cx=...`

**Weather (OpenWeatherMap):**

* `eddi.tools.weather.openweathermap.api-key=...`

See [LangChain Documentation](/agent-configuration/langchain#tool-configuration-server-side) for details.

## Your First Agent (via API)

### 1. Create a Dictionary

Dictionaries define what users can say:

```bash
curl -X POST http://localhost:7070/regulardictionarystore/regulardictionaries \
  -H "Content-Type: application/json" \
  -d '{
    "words": [
      {
        "word": "hello",
        "expressions": "greeting(hello)",
        "frequency": 0
      },
      {
        "word": "hi",
        "expressions": "greeting(hi)",
        "frequency": 0
      }
    ],
    "phrases": []
  }'
```

**Response**: Dictionary ID (e.g., `eddi://ai.labs.parser.dictionaries.regular/regulardictionarystore/regulardictionaries/abc123?version=1`)

### 2. Create Behavior Rules

Rules define what the agent does:

```bash
curl -X POST http://localhost:7070/behaviorstore/behaviorsets \
  -H "Content-Type: application/json" \
  -d '{
    "behaviorGroups": [
      {
        "name": "Greetings",
        "behaviorRules": [
          {
            "name": "Welcome",
            "conditions": [
              {
                "type": "inputmatcher",
                "configs": {
                  "expressions": "greeting(*)",
                  "occurrence": "currentStep"
                }
              }
            ],
            "actions": ["welcome_action"]
          }
        ]
      }
    ]
  }'
```

**Response**: Behavior set ID

### 3. Create Output Templates

```bash
curl -X POST http://localhost:7070/outputstore/outputsets \
  -H "Content-Type: application/json" \
  -d '{
    "outputSet": [
      {
        "action": "welcome_action",
        "timesOccurred": 0,
        "outputs": [
          {
            "valueAlternatives": [
              "Hello! How can I help you today?"
            ]
          }
        ]
      }
    ]
  }'
```

**Response**: Output set ID

### 4. Create a Workflow

Workflows bundle extensions together:

```bash
curl -X POST http://localhost:7070/packagestore/packages \
  -H "Content-Type: application/json" \
  -d '{
    "packageExtensions": [
      {
        "type": "eddi://ai.labs.parser.dictionaries.regular",
        "extensions": {
          "uri": "eddi://ai.labs.regulardictionary/regulardictionarystore/regulardictionaries/abc123?version=1"
        }
      },
      {
        "type": "eddi://ai.labs.behavior",
        "extensions": {
          "uri": "eddi://ai.labs.behavior/behaviorstore/behaviorsets/def456?version=1"
        },
        "config": {
          "appendActions": true
        }
      },
      {
        "type": "eddi://ai.labs.output",
        "extensions": {
          "uri": "eddi://ai.labs.output/outputstore/outputsets/ghi789?version=1"
        }
      }
    ]
  }'
```

**Response**: Workflow ID

### 5. Create an Agent

```bash
curl -X POST http://localhost:7070/agentstore/agents \
  -H "Content-Type: application/json" \
  -d '{
    "packages": [
      "eddi://ai.labs.package/packagestore/packages/xyz123?version=1"
    ]
  }'
```

**Response**: Agent ID (e.g., `agent-abc-123`)

### 6. Deploy the Agent

```bash
curl -X POST "http://localhost:7070/administration/production/deploy/agent-abc-123?version=1"
```

### 7. Chat with Your Agent

```bash
# Start conversation
curl -X POST http://localhost:7070/agents/agent-abc-123/start \
  -H "Content-Type: application/json" \
  -d '{"input": "hello"}'

# Response includes conversationId
# {
#   "conversationId": "conv-123",
#   "conversationState": "READY",
#   "conversationOutputs": [
#     {"output": ["Hello! How can I help you today?"]}
#   ]
# }

# Continue conversation
curl -X POST http://localhost:7070/agents/conv-123 \
  -H "Content-Type: application/json" \
  -d '{"input": "hi"}'
```

## Adding an LLM (OpenAI Example)

### 1. Create LangChain Configuration

```bash
curl -X POST http://localhost:7070/langchainstore/langchains \
  -H "Content-Type: application/json" \
  -d '{
    "tasks": [
      {
        "actions": ["send_to_ai"],
        "id": "openai_chat",
        "type": "openai",
        "description": "OpenAI ChatGPT integration",
        "parameters": {
          "apiKey": "your-openai-api-key",
          "modelName": "gpt-4o",
          "temperature": "0.7",
          "systemMessage": "You are a helpful assistant",
          "sendConversation": "true",
          "addToOutput": "true"
        }
      }
    ]
  }'
```

### 2. Add LangChain to Workflow

Add this extension to your package:

```json
{
  "type": "eddi://ai.labs.llm",
  "extensions": {
    "uri": "eddi://ai.labs.llm/langchainstore/langchains/langchain-id?version=1"
  }
}
```

### 3. Create Behavior Rule to Trigger LLM

```json
{
  "name": "Ask AI",
  "conditions": [
    {
      "type": "inputmatcher",
      "configs": {
        "expressions": "question(*)",
        "occurrence": "currentStep"
      }
    }
  ],
  "actions": ["send_to_ai"]
}
```

Now when users ask questions, the LLM is automatically called!

## Understanding the Flow

Let's trace what happens when a user says "hello":

### 1. API Request

```json
POST /agents/agent-abc-123/start
{"input": "hello"}
```

### 2. RestAgentEngine

* Validates agent ID
* Creates/loads conversation memory
* Submits to ConversationCoordinator

### 3. ConversationCoordinator

* Ensures sequential processing (no race conditions)
* Queues message for this conversation

### 4. LifecycleManager Executes Pipeline

**Parser Task**:

```
Input: "hello"
→ Parses using dictionary
→ Output: expressions = ["greeting(hello)"]
→ Stores in memory
```

**Behavior Rules Task**:

```
Reads: expressions = ["greeting(hello)"]
→ Evaluates rules
→ Rule matches: "if greeting(*) then welcome_action"
→ Output: actions = ["welcome_action"]
→ Stores in memory
```

**Output Task**:

```
Reads: actions = ["welcome_action"]
→ Looks up output template for "welcome_action"
→ Output: "Hello! How can I help you today?"
→ Stores in memory
```

### 5. Save & Return

* Memory saved to MongoDB
* Response returned to user

## Key Architectural Components

### IConversationMemory

The state object passed through the pipeline:

```java
IConversationMemory memory = ...;

// Read user input
String input = memory.getCurrentStep().getLatestData("input").getResult();

// Store parsed data
memory.getCurrentStep().storeData(
    dataFactory.createData("expressions", expressions)
);

// Access conversation properties
String userName = memory.getConversationProperties().get("userName");
```

### ILifecycleTask

Interface all tasks implement:

```java
public class MyTask implements ILifecycleTask {
    @Override
    public void execute(IConversationMemory memory, Object component) {
        // 1. Read from memory
        String input = memory.getCurrentStep().getLatestData("input").getResult();

        // 2. Process
        String result = process(input);

        // 3. Write to memory
        memory.getCurrentStep().storeData(
            dataFactory.createData("myResult", result)
        );
    }
}
```

### ConversationCoordinator

Ensures messages are processed in order:

```java
// Messages for same conversation execute sequentially
coordinator.submitInOrder(conversationId, () -> {
    processMessage(memory, input);
    return null;
});
```

## Common Patterns

### Pattern 1: Conditional LLM Invocation

Only call LLM for complex queries:

```json
{
  "behaviorRules": [
    {
      "name": "Simple Greeting",
      "conditions": [
        { "type": "inputmatcher", "configs": { "expressions": "greeting(*)" } }
      ],
      "actions": ["simple_greeting"]
    },
    {
      "name": "Complex Question",
      "conditions": [
        { "type": "inputmatcher", "configs": { "expressions": "question(*)" } }
      ],
      "actions": ["send_to_ai"]
    }
  ]
}
```

### Pattern 2: API Call Before LLM

Fetch data, then ask LLM to format it:

```json
{
  "behaviorRules": [
    {
      "name": "Weather Query",
      "conditions": [
        {
          "type": "inputmatcher",
          "configs": { "expressions": "entity(weather)" }
        }
      ],
      "actions": ["httpcall(weather-api)", "send_to_ai"]
    }
  ]
}
```

The LLM receives the API response in memory and can format it naturally.

### Pattern 3: Context-Aware Responses

Use context passed from your app:

```bash
curl -X POST http://localhost:7070/agents/agent-abc-123/start \
  -H "Content-Type: application/json" \
  -d '{
    "input": "What is my name?",
    "context": {
      "userName": {"type": "string", "value": "John"},
      "userId": {"type": "string", "value": "user-123"}
    }
  }'
```

Access in output template:

```
Hello {context.userName}!
```

## Next Steps

### Learn More

* [**Architecture Overview**](/architecture-and-concepts/architecture) - Deep dive into design
* [**Behavior Rules**](/agent-configuration/behavior-rules) - Master decision logic
* [**HTTP Calls**](/agent-configuration/httpcalls) - Integrate external APIs
* [**LangChain Integration**](/agent-configuration/langchain) - Configure LLMs
* [**Agent Father Deep Dive**](/advanced-concepts/agent-father-deep-dive) - Real-world example

### Use the Dashboard

Visit `http://localhost:7070` to:

* Create agents visually
* Test conversations interactively
* Browse configurations
* Monitor deployments

### Explore Examples

Check the `examples/` folder for:

* Weather agent (API integration)
* Support agent (multi-turn conversations)
* E-commerce agent (context management)

### Build Your Own Task

Create a custom lifecycle task:

```java
@ApplicationScoped
public class MyCustomTask implements ILifecycleTask {
    @Override
    public TaskId getId() {
        return new TaskId("ai.labs.mycompany.customtask");
    }

    @Override
    public String getType() {
        return "custom_processing";
    }

    @Override
    public void execute(IConversationMemory memory, Object component) {
        // Your logic here
    }
}
```

Register it in CDI and it becomes available as an extension!

## Troubleshooting

### Agent doesn't respond

1. Check deployment status: `GET /administration/deploy/{agentId}`
2. Check conversation state: `GET /conversationstore/conversations/{conversationId}`
3. Check logs for errors

### Rules not matching

* Verify dictionary expressions match your input
* Check rule conditions are correct
* Use `occurrence: "anyStep"` to match across conversation

### LLM not being called

* Ensure behavior rule triggers the LLM action
* Check LangChain configuration is in the package
* Verify API key is correct

### Memory not persisting

* Ensure MongoDB is running
* Check connection string in config
* Use correct scope (`conversation` not `step`)

## Getting Help

* **Documentation**: <https://github.com/labsai/EDDI/tree/main/docs>
* **GitHub**: <https://github.com/labsai/EDDI>
* **Issues**: <https://github.com/labsai/EDDI/issues>

## Summary

EDDI's power comes from its **configurable pipeline architecture**:

* Agents are JSON configurations, not code
* Everything flows through Conversation Memory
* Tasks are pluggable and reusable
* LLMs are orchestrated, not just proxied

Start simple, then add complexity as needed. The architecture scales from basic agents to sophisticated multi-API workflows.


# Agent Manager Dashboard

**Version: 6.2.0**

## Overview

The EDDI Manager is a modern **React 19 single-page application** for building, testing, deploying, and monitoring EDDI agents. It is served directly from the EDDI backend — no separate deployment needed.

## Access

Open your browser to the EDDI root URL:

```
http://localhost:7070
```

The Manager is the default landing page. No `apiUrl` query parameter is needed — the Manager automatically connects to the backend that serves it.

## Features

### Agent Management

* **Agent List** — Browse all agents with search, version, deployment status, and last-modified date
* **Agent Editor** — Edit agent name, description, and package references with a form-based UI
* **Version Picker** — Switch between agent versions; compare configurations across versions
* **Deploy / Undeploy** — One-click deployment with status badges
* **Duplicate** — Clone an agent and all its packages/extensions in one operation

### Pipeline Builder

* **Drag-and-Drop** — Visually compose packages by dragging workflow extensions (behavior rules, HTTP calls, LangChain, output, etc.) into a pipeline
* **Extension Editors** — Form-based editors for all 8 resource types:

  | Resource Type   | Editor                                                               |
  | --------------- | -------------------------------------------------------------------- |
  | Behavior Rules  | Rule groups, conditions (input/action/context match), actions        |
  | HTTP Calls      | URL, method, headers, body, pre/post property instructions           |
  | LangChain (LLM) | Provider, model, system prompt, tools, RAG, cascade                  |
  | Output          | Action-based output sets with text, quick replies, and delays        |
  | Property Setter | Property instructions with scope and visibility                      |
  | Dictionary      | Words, phrases, and expression mappings                              |
  | RAG             | Embedding provider, vector store, chunk settings, document ingestion |
  | MCP Calls       | External MCP server connections                                      |
* **JSON Editor** — Monaco-based JSON editor with syntax highlighting for any resource
* **Version History** — Every save creates a new version; switch and compare at will

### Chat Panel

* **Embedded Chat** — Test conversations with any deployed agent directly in the Manager
* **SSE Streaming** — Real-time response streaming
* **Secret Input** — Password field mode for entering API keys securely
* **Undo / Redo** — Time-travel through conversation steps

### Secrets Administration

* **Secrets Page** (`/manage/secrets`) — Manage vault entries through the UI
* **Write-Only** — Secret values can be stored but never retrieved (API returns metadata only)
* **Vault Health** — Live status badge showing vault online/offline state

### Observability

* **Logs Panel** — Live server-side log streaming via SSE with level filtering and search
* **Audit Trail** — Per-conversation timeline of pipeline execution (tasks, LLM details, tool calls, costs)

### Additional Features

* **Dark / Light Theme** — System-aware with manual toggle
* **11 Locales** — English, German, French, Spanish, Arabic, Chinese, Thai, Japanese, Korean, Portuguese, Hindi
* **RTL Support** — Full right-to-left layout for Arabic
* **Responsive Layout** — Collapsible sidebar, mobile-friendly

## Technology Stack

| Layer              | Technology                                 |
| ------------------ | ------------------------------------------ |
| **Framework**      | React 19 + TypeScript 5                    |
| **Build**          | Vite 6                                     |
| **Styling**        | Tailwind CSS v4 with CSS variables         |
| **State (server)** | TanStack Query v5                          |
| **State (UI)**     | Zustand (chat/debug), `useState` elsewhere |
| **Routing**        | React Router v7                            |
| **i18n**           | react-i18next                              |
| **Editor**         | Monaco (@monaco-editor/react)              |
| **DnD**            | @dnd-kit                                   |
| **Testing**        | Vitest + React Testing Library + MSW       |

## Authentication

When Keycloak is enabled (`QUARKUS_OIDC_TENANT_ENABLED=true`), the Manager uses `keycloak-js` for login:

* Automatic token refresh (every 30s before expiry)
* Role-based UI (admin sees deploy/delete, viewer sees read-only)
* Graceful degradation when auth is disabled (open access)

## Source Code

The Manager is developed in the [EDDI-Manager](https://github.com/labsai/EDDI-Manager) repository and bundled into the EDDI Docker image at build time.


# Creating your first Agent

*Prerequisites: Up and Running instance of **EDDI** (see:* [*Getting started*](/getting-started/getting-started)*)*

## How does it work?

In order to build an Agent with **EDDI**, you will have to create a few configuration files and `POST` them to the corresponding REST APIs.

![](/files/-M69UTDQDilb54yzlw-1)

A agent can consists of the following elements:

1. (Regular) **`Dictionary`** to define the inputs from the users as well as their meanings in respective categories, expressed by a expression language `e.g. apple -> fruit(apple)`
2. **`Behavior Rules`** triggering **actions** based on execution of behavior rules checking on certain conditions within the current conversation
3. **`Http Connector`** requests/sends data to a Rest API and makes the json response available within the conversation (e.g for Output\*\*`)`\*\*
4. **`Output`** to answer the user's request based on **actions** triggered by behavior rules
5. **`Workflow`** to define which **\`LifecycleTasks**\` (such as the parser, behavior rules, rest api connector, output generation, ...) should be executed in order by how they are defined
6. **`Agent`** to define which packages should be executed in this agent

### Example of a resource reference

`eddi://ai.labs.regulardictionary/regulardictionarystore/regulardictionaries/ID?version=VERSION`

`eddi://` URI resources starting with this protocol are to be related with in EDDI

`ai.labs.regulardictionary` Type of resource

`/regulardictionarystore/regulardictionaries` API path

`ID` ID of the resources

`VERSION` Read-only version of the resource (each change is a new version)

Version of this resource (each update operation will create a new version of the resource)


# Create a "Hello World" agent

*Prerequisites: Up and Running instance of **EDDI** (see:* [*Getting started*](/getting-started/getting-started)*)*

## Let's get started

Follow these steps to create the configuration files you will need:

### 1. Creating Output

> [See also Output Configuration.](/agent-configuration/output-configuration)

You have guessed it correctly, another **`POST`** to **`/outputstore/outputsets`** creates the agent's `Output` with a JSON in the body like this:

```javascript
{
  "outputSet": [
    {
      "action": "CONVERSATION_START",
      "timesOccurred": 0,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "Hello World!"
            }
          ]
        }
      ]
    }
  ]
}
```

You should again get a return code of **`201`** with a **`URI`** in the **`location` header** referencing the newly created output :

`eddi://ai.labs.output/outputstore/outputsets/`**`<UNIQUE_OUTPUTSET_ID>`**`?version=`**`<OUTPUTSET_VERSION>`**

Example :

`eddi://ai.labs.output/outputstore/outputsets/5a26d97417312628b46119fc?version=1`

### 4. Creating the Workflow

Now we will align the just created `LifecycleTasks` in the `Workflow`. Make a **`POST`** to **`/packagestore/packages`** with a JSON in the body like this:

```javascript
{
  "packageExtensions": [
    {
      "type": "eddi://ai.labs.output",
      "config": {
        "uri": "eddi://ai.labs.output/outputstore/outputsets/<UNIQUE_OUTPUTSET_ID>?version=<OUTPUTSET_VERSION>"
      }
    }
  ]
}
```

### Workflow parameters

| Name                         | Description                                          | Required |
| ---------------------------- | ---------------------------------------------------- | -------- |
| packageextensions            | `Array` of `WorkflowExtension`                       |          |
| WorkflowExtension.type       | possible values, see table below "`Extension Types`" |          |
| WorkflowExtension.extensions | `Array` of `Object`                                  | False    |
| WorkflowExtension.config     | `Config` object, but can be empty.                   | True     |

Extension Types in this examples

| Extension             | Config                                                                                                                                             |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| eddi://ai.labs.output | Object Config contains param `uri` with Link to output set, e.g. `eddi://ai.labs.output/outputstore/outputsets/5a26d97417312628b46119fc?version=1` |

>

`eddi://ai.labs.package/packagestore/packages/<UNIQUE_WORKFLOW_ID>?version=<WORKFLOW_VERSION>`

Example

`eddi://ai.labs.package/packagestore/packages/5a2ae60f17312624f8b8a445?version=1`

> See also the API documentation at <http://localhost:7070/q/swagger-ui>

### 5. Creating an Agent

Make a **`POST`** to **`/agentstore/agents`** with a JSON like this:

```javascript
{
     "packages": [
          "eddi://ai.labs.package/packagestore/packages/<UNIQUE_WORKFLOW_ID>?version=<WORKFLOW_VERSION>"
     ]
}
```

### Agent parameters

| Name     | Description                                    |
| -------- | ---------------------------------------------- |
| packages | `Array` of `String`, references to `Workflows` |

b. You should again get a return code of **`201`** with a `URI` in the `location` header referencing the newly created agent :

`eddi://ai.labs.agent/agentstore/agents/`**`<UNIQUE_AGENT_ID>`**`?version=`**`<AGENT_VERSION>`**

Example:

`eddi://ai.labs.agent/agentstore/agents/5a2ae68a17312624f8b8a446?version=1`

> See also the API documentation at <http://localhost:7070/q/swagger-ui>

### 6. Launching the Agent

Finally, we are ready to let the agent fly. From here on, you have the possibility to let an UI do it for you or you do it step by step.

The UI that automates these steps can be reached here: `/chat/production/`**`<UNIQUE_AGENT_ID>`**

Otherwise via REST:

1. Deploy the Agent:

   Make a **`POST`** to `/administration/production/deploy/`**`<UNIQUE_AGENT_ID>`**`?version=`**`<AGENT_VERSION>`**

   You will receive a `202` http code.
2. Since deployment could take a while it has been made **asynchronous**.
3. Make a **`GET`** to `/administration/production/deploymentstatus/`**`<UNIQUE_AGENT_ID>`**`?version=`**`<AGENT_VERSION>`** to find out the status of deployment.

**`NOT_FOUND`**, **`IN_PROGRESS`**, **`ERROR` and `READY`** is what you can expect to be returned in the body.

1. As soon as the Agent is deployed and has `READY` status, make a **`POST`** to `/agents/`**`<UNIQUE_AGENT_ID>`**/start
   1. You will receive a `201` with the `URI` for the newly created Conversation, like this:
      1. e.g.

         `eddi://ai.labs.conversation/conversationstore/conversations/`**`<UNIQUE_CONVERSATION_ID>`**
2. Now it's time to start talking to our Agent 1. Make a **`POST`** to `/agents/`**`<UNIQUE_AGENT_ID>`**/start`/`**`<UNIQUE_CONVERSATION_ID>`**

**Option 1:** is to hand over the input text as `contentType text/plain`. Include the User Input in the body as `text/plain` (e.g. Hello)

**Option 2:** is to hand over the input as `contentType application/json`, which also allows you to handover context information that you can use with the eddi configurations 1. Include the User Input in the body as application/json (e.g. Hello)

```json
{
  "input": "some user input"
}
```

1. You have two query params you can use to config the returned output 1. `returnDetailed` - default is false - will return all sub results of the entire conversation steps, otherwise only public ones such as input, action, output & quickreplies 2. `returnCurrentStepOnly` - default is true - will return only the latest conversation step that has just been processed, otherwise returns all conversation steps since the beginning of this conversation
2. The output from the agent will be returned as JSON
3. If you are interested in fetching the **`conversationmemory`** at any given time, make a **`GET`** to `/agents/`**`<UNIQUE_AGENT_ID>`**/start`/`**`<UNIQUE_CONVERSATION_ID>`**`?returnDetailed=true` (the query param is optional, default is false)

> If you made it till here, CONGRATULATIONS, you have created your first Agent with **EDDI** !

By the way you can use the attached **postman collection** below to do all of the steps mentioned above by clicking send on each request in postman.

1. Create outputSet
2. Creating package
3. Creating agent
4. Deploy the agent
5. Create conversation
6. Say Hello to the agent

Download the [Postman collection](https://github.com/labsai/EDDI/blob/main/docs/.gitbook/assets/Creating%20and%20chatting%20with%20a%20bot.postman_collection.json) to run through all the steps above.

### External Links

[Using collections in postman](https://thinkster.io/tutorials/testing-backend-apis-with-postman/using-collections-in-postman)


# Create an agent that reacts to user inputs

*Prerequisites: Up and Running instance of **EDDI** (see:* [*Getting started*](/getting-started/getting-started)*)*

## Let's get started

Follow these steps to create the configuration files you will need:

### **1. Creating a Regular Dictionary inside Parser**

> See also [Semantic Parser](/agent-configuration/semantic-parser)

Create regular dictionaries in order to store custom words and phrases. A dictionary is there to map user input to expressions, which are later used in `Behavior Rules`. A **`POST`** to **`/regulardictionarystore/regulardictionaries`** with a JSON in the body like this:

```javascript
{
  "words": [
    {
      "word": "hello",
      "expressions": "greeting(hello)",
      "frequency": 0
    },
    {
      "word": "hi",
      "expressions": "greeting(hi)",
      "frequency": 0
    },
    {
      "word": "bye",
      "expressions": "goodbye(bye)",
      "frequency": 0
    },
    {
      "word": "thanks",
      "expressions": "thanks(thanks)",
      "frequency": 0
    }
  ],
  "phrases": [
    {
      "phrase": "good afternoon",
      "expressions": "greeting(good_afternoon)"
    },
    {
      "phrase": "how are you",
      "expressions": "how_are_you"
    }
  ]
}
```

Example using **`CURL`**:

```bash
curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{ \
"language" : "en", \
"words" : [ \
{ \
"word" : "hello", \
"expressions" : "greeting(hello)", \
"frequency" : 0 \
}, \
{ \
"word" : "hi", \
"expressions" : "greeting(hi)", \
"frequency" : 0 \
}, \
{ \
"word" : "bye", \
"expressions" : "goodbye(bye)", \
"frequency" : 0 \
}, \
{ \
"word" : "thanks", \
"expressions" : "thanks(thanks)", \
"frequency" : 0 \
} \
], \
"phrases" : [ \
{ \
"phrase" : "good afternoon", \
"expressions" : "greeting(good_afternoon)" \
}, \
{ \
"phrase" : "how are you", \
"expressions" : "how_are_you" \
} \
] \
}' 'http://localhost:7070/regulardictionarystore/regulardictionaries'
```

### Dictionary parameters

| Name               | Description                                                                                          | Required |
| ------------------ | ---------------------------------------------------------------------------------------------------- | -------- |
| words              | `Array` of `Word`                                                                                    |          |
| phrases            | `Array` of `Phrase`                                                                                  |          |
| Word.word          | `String`, single word, no spaces.                                                                    | True     |
| Word.expressions   | `String`, "greeting(hello)": "greeting" is the category of this expression and "hello" is an entity. |          |
| Word.frequency     | `int`, Used for a randomizer                                                                         |          |
| Phrase.phrase      | `String`, Spaces allowed                                                                             | True     |
| Phrase.expressions | `String`, "greeting(hello)": "greeting" is the category of this expression and "hello" is an entity. |          |

> The returned URI is a reference for this specific resource. This resource will be referenced in the agent definition.

### **2. Creating Behavior Rules**

> See also Behavior Rules

Next, create a `behaviorRule` resource to configure the decision making a. Make a **`POST`** to **`/behaviorstore/behaviorsets`** with a JSON in the body like this:

```javascript
{
  "behaviorGroups": [
    {
      "name": "Smalltalk",
      "behaviorRules": [
        {
          "name": "Welcome",
          "actions": [
            "welcome"
          ],
          "conditions": [
            {
              "type": "occurrence",
              "configs": {
                "maxTimesOccurred": "0",
                "behaviorRuleName": "Welcome"
              }
            }
          ]
        },
        {
          "name": "Greeting",
          "actions": [
            "greet"
          ],
          "conditions": [
            {
              "type": "inputmatcher",
              "configs": {
                "expressions": "greeting(*)",
                "occurrence": "currentStep"
              }
            }
          ]
        },
        {
          "name": "Goodbye",
          "actions": [
            "say_goodbye",
            "CONVERSATION_END"
          ],
          "conditions": [
            {
              "type": "inputmatcher",
              "configs": {
                "expressions": "goodbye(*)"
              }
            }
          ]
        },
        {
          "name": "Thank",
          "actions": [
            "thank"
          ],
          "conditions": [
            {
              "type": "inputmatcher",
              "configs": {
                "expressions": "thank(*)"
              }
            }
          ]
        },
        {
          "name": "how are you",
          "actions": [
            "how_are_you"
          ],
          "conditions": [
            {
              "type": "inputmatcher",
              "configs": {
                "expressions": "how_are_you"
              }
            }
          ]
        }
      ]
    }
  ]
}
```

### Behavior Rules parameters

| Name                         | Description                                                                                                                                                                                                                                                                                                                                     |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| BehaviorRule.name            | `String`, e.g. "Smalltalk"                                                                                                                                                                                                                                                                                                                      |
| BehaviourGroup.behaviorRules | `Array` of `BehaviorRule`                                                                                                                                                                                                                                                                                                                       |
| BehaviorRule.name            | `String`, e.g. "Greeting"                                                                                                                                                                                                                                                                                                                       |
| BehaviorRule.actions         | `Array` of `String`, e.g. "greet" or "CONVERSATION\_END"                                                                                                                                                                                                                                                                                        |
| BehaviorRule.conditions      | `Array` of `RuleChild`                                                                                                                                                                                                                                                                                                                          |
| RuleChild.type               | <p><code>String</code>, allowed values:</p><p>—>"<code>inputmatcher</code>" (has params: "<code>expressions</code>" (<code>Array</code> of <code>String</code>( and "<code>occurrence</code>")</p><p>—>"<code>negation</code>" (<code>BehaviorExtension</code> object, has params: "<code>conditions</code>" and "<code>occurrence</code>")</p> |
| RuleChild.values             | <p><code>HashMap</code>, allowed values:</p><p>—>"<code>expressions</code>": <code>String</code>, mandatory. Expression e.g. "greeting(\*)" or "how\_are\_you"</p><p>—>"<code>occurrence</code>": <code>String</code>, optional. Allowed values "<code>currentStep</code>"</p>                                                                  |
| Negation.conditons           | `Array` of `NegationChild`                                                                                                                                                                                                                                                                                                                      |
| NegationChild.type           | `String` e.g. "`occurrence`"                                                                                                                                                                                                                                                                                                                    |
| NegationChild.values         | <p>HashMap, allowed values:</p><p>—>"<code>maxTimesOccurred</code>": <code>String</code>, e.g. 1</p><p>—>"<code>minTimesOccurred</code>": <code>String</code>, e.g. 1</p><p>—>"<code>behaviorRuleName</code>": <code>String</code></p>                                                                                                          |

You should again get a return code of **`201`** with a **`URI`** in the **`location` header** referencing the newly created `Behavior Rules`:

`eddi://ai.labs.behavior/behaviorstore/behaviorsets/`**`<UNIQUE_BEHAVIOR_ID>`**`?version=`**`<BEHAVIOR_VERSION>`**

Example:

`eddi://ai.labs.behavior/behaviorstore/behaviorsets/5a26d8fd17312628b46119fb?version=1`

### 3. Creating Output

> [See also Output Configuration.](/agent-configuration/output-configuration)

You have guessed it correctly, another **`POST`** to **`/outputstore/outputsets`** creates the agent's `Output` with a JSON in the body like this:

```javascript
{
  "outputSet": [
    {
      "action": "welcome",
      "timesOccurred": 0,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "Welcome!"
            }
          ]
        },
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "My name is E.D.D.I"
            }
          ]
        }
      ],
      "quickReplies": [
        {
          "value": "Hi EDDI",
          "expressions": "greeting(hi)"
        },
        {
          "value": "Bye EDDI",
          "expressions": "goodbye(bye)"
        }
      ]
    },
    {
      "action": "greet",
      "timesOccurred": 0,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "Hi there! Nice to meet up! :-)"
            },
            {
              "type": "text",
              "text": "Hey you!"
            }
          ]
        }
      ]
    },
    {
      "action": "greet",
      "timesOccurred": 1,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "Did we already say hi ?! Well, twice is better than not at all! ;-)"
            }
          ]
        }
      ]
    },
    {
      "action": "say_goodbye",
      "timesOccurred": 0,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "See you soon!"
            }
          ]
        }
      ]
    },
    {
      "action": "thank",
      "timesOccurred": 0,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "Your Welcome!"
            }
          ]
        }
      ]
    },
    {
      "action": "how_are_you",
      "timesOccurred": 0,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "Pretty good.. having lovely conversations all day long.. :-D"
            }
          ]
        }
      ]
    }
  ]
}
```

You should again get a return code of **`201`** with a **`URI`** in the **`location` header** referencing the newly created output :

`eddi://ai.labs.output/outputstore/outputsets/`**`<UNIQUE_OUTPUTSET_ID>`**`?version=`**`<OUTPUTSET_VERSION>`**

Example :

`eddi://ai.labs.output/outputstore/outputsets/5a26d97417312628b46119fc?version=1`

### 4. Creating the Workflow

Now we will align the just created `LifecycleTasks` in the `Workflow`. Make a **`POST`** to **`/packagestore/packages`** with a JSON in the body like this:

```javascript
{
  "packageExtensions": [
    {
      "type": "eddi://ai.labs.parser",
      "extensions": {
        "dictionaries": [
          {
            "type": "eddi://ai.labs.parser.dictionaries.integer"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.decimal"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.punctuation"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.email"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.time"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.ordinalNumber"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.regular",
            "config": {
              "uri": "eddi://ai.labs.regulardictionary/regulardictionarystore/regulardictionaries/<UNIQUE_DICTIONARY_ID>?version=<DICTIONARY_VERSION>"
            }
          }
        ],
        "corrections": [
          {
            "type": "eddi://ai.labs.parser.corrections.stemming",
            "config": {
              "language": "english",
              "lookupIfKnown": "false"
            }
          },
          {
            "type": "eddi://ai.labs.parser.corrections.levenshtein",
            "config": {
              "distance": "2"
            }
          },
          {
            "type": "eddi://ai.labs.parser.corrections.mergedTerms"
          }
        ]
      },
      "config": {}
    },
    {
      "type": "eddi://ai.labs.behavior",
      "config": {
        "uri": "eddi://ai.labs.behavior/behaviorstore/behaviorsets/<UNIQUE_BEHAVIOR_ID>?version=<BEHAVIOR_VERSION>"
      }
    },
    {
      "type": "eddi://ai.labs.output",
      "config": {
        "uri": "eddi://ai.labs.output/outputstore/outputsets/<UNIQUE_OUTPUTSET_ID>?version=<OUTPUTSET_VERSION>"
      }
    }
  ]
}
```

### Workflow parameters

| Name                         | Description                                          | Required |
| ---------------------------- | ---------------------------------------------------- | -------- |
| packageextensions            | `Array` of `WorkflowExtension`                       |          |
| WorkflowExtension.type       | possible values, see table below "`Extension Types`" |          |
| WorkflowExtension.extensions | `Array` of `Object`                                  | False    |
| WorkflowExtension.config     | `Config` object, but can be empty.                   | True     |

Extension Types

| Extension               | Config                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| eddi://ai.labs.parser   | <p><code>Dictionaries</code> and/or corrections</p><p>Object "<code>extensions</code>" can contain "<code>dictionaries</code>" (<code>Array</code> of <code>Dictionary</code>) and/or "<code>corrections</code>" (<code>Array</code> of <code>Correction</code>)</p><p>Object "<code>Dictionary</code>" has params "<code>type</code>" and "<code>config</code>" (optional)</p><p><code>Dictionary.type</code> can reference <code>Regular-Dictionaries</code> "<code>eddi://ai.labs.parser.dictionaries.regular</code>" (needs param "<code>config.uri</code>") or be one of the <strong>EDDI</strong> out of the box types:</p><p>—>"<code>eddi://ai.labs.parser.dictionaries.integer</code>"</p><p>—>"<code>eddi://ai.labs.parser.dictionaries.decimal</code>"</p><p>—>"<code>eddi://ai.labs.parser.dictionaries.punctuation</code>"</p><p>—>"<code>eddi://ai.labs.parser.dictionaries.email</code>"</p><p>—>"<code>eddi://ai.labs.parser.dictionaries.time</code>"</p><p>—>"<code>eddi://ai.labs.parser.dictionaries.ordinalNumber</code>"</p><p>Object "<code>Correction</code>" has params "<code>type</code>" and "<code>config</code>" (optional)</p><p><code>Correction.type</code> can reference one of the EDDI out of the box types:</p><p>—>"<code>eddi://ai.labs.parser.corrections.stemming</code>": Object "<code>config</code>" has params "<code>language</code>" (<code>String</code> e.g. "english") and "<code>lookupIfKnown</code>" (<code>Boolean</code>)</p><p>—>"<code>eddi://ai.labs.parser.corrections.levenshtein</code>": Object "<code>config</code>" has param "<code>distance</code>" (Integer, e.g. 2)</p><p>—>"<code>eddi://ai.labs.parser.corrections.mergedTerms</code>"</p> |
| eddi://ai.labs.behavior | Object `Config` contains param `uri` with Link to a behavior set, e.g. `eddi://ai.labs.behavior/behaviorstore/behaviorsets/5a26d8fd17312628b46119fb?version=1`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| eddi://ai.labs.output   | Object Config contains param `uri` with Link to output set, e.g. `eddi://ai.labs.output/outputstore/outputsets/5a26d97417312628b46119fc?version=1`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |

> New

Now you can use the new feature of defining properties in the package definition : This can be used by introducing an extension with `type` `eddi://ai.labs.property` which has the `config` model as follows:

```javascript
{
  "type": "eddi://ai.labs.property",
  "config": {
    "setOnActions": [
      {
        "actions": "string",
        "setProperties": [
          {
            "name": "string",
            "fromObjectPath": "string",
            "scope": "string"
          }
        ]
      }
    ]
  }
}
```

### Description of eddi://ai.labs.property model

| Name                                      | Description                                                                                            |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| setOnActions.actions                      | (`string`) defines which for which actions (triggered by BehaviorRules) these Properties should be set |
| setOnActions.setProperties                | (`Array` <`Property`>: ) must respect the `Property`model: `name`, `fromObjectPath` and `scope`.       |
| setOnActions.setProperties.name           | (`string`) name of the `Property`.                                                                     |
| setOnActions.setProperties.fromObjectPath | (`string`) path to the json object.                                                                    |
| setOnActions.setProperties.scope          | (`string`) Possible values `step`, `conversation` and `longTerm` .                                     |

#### Example of eddi://ai.labs.property

```javascript
{
  "packageExtensions": [
   ...
    {
      "type": "eddi://ai.labs.property",
      "config": {
        "setOnActions": [
          {
            "actions": "currentWeather",
            "setProperties": [
              {
                "name": "city",
                "fromObjectPath": "memory.current.input",
                "scope": "longTerm"
              }
            ]
          }
        ]
      }
    },
   ...
    {
      "type": "eddi://ai.labs.property",
      "config": {
        "setOnActions": [
          {
            "actions": "currentWeather",
            "setProperties": [
              {
                "name": "currentWeather",
                "fromObjectPath": "memory.current.httpCalls.currentWeather",
                "scope": "conversation"
              }
            ]
          }
        ]
      }
    },
   ...
  ]
}
```

You should again get a return code of `201` with an `URI` in the location header referencing the newly created package format

`eddi://ai.labs.package/packagestore/packages/<UNIQUE_WORKFLOW_ID>?version=<WORKFLOW_VERSION>`

Example

`eddi://ai.labs.package/packagestore/packages/5a2ae60f17312624f8b8a445?version=1`

> See also the API documentation at <http://localhost:7070/q/swagger-ui>

### 5. Creating an Agent

Make a **`POST`** to **`/agentstore/agents`** with a JSON like this:

```javascript
{
"packages": [
"eddi://ai.labs.package/packagestore/packages/<UNIQUE_WORKFLOW_ID>?version=<WORKFLOW_VERSION>"
],
"channels": []
}
```

### Agent parameters

| Name           | Description                                                                                                                                           |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| packages       | `Array` of `String`, references to `Workflows`                                                                                                        |
| channels       | `Array` of `Channel`,                                                                                                                                 |
| Channel.type   | `String`, e.g. `"eddi://ai.labs.channel.facebook"`                                                                                                    |
| Channel.config | `Config` Object. For "Facebook" this object has the params "`appSecret`" (`String`), "`verificationToken`" (`String`), "`pageAccessToken`" (`String`) |

b. You should again get a return code of **`201`** with a `URI` in the `location` header referencing the newly created agent :

`eddi://ai.labs.agent/agentstore/agents/`**`<UNIQUE_AGENT_ID>`**`?version=`**`<AGENT_VERSION>`**

Example:

`eddi://ai.labs.agent/agentstore/agents/5a2ae68a17312624f8b8a446?version=1`

> See also the API documentation at <http://localhost:7070/q/swagger-ui>

### 6. Launching the Agent

Finally, we are ready to let the agent fly. From here on, you have the possibility to let an UI do it for you or you do it step by step.

The UI that automates these steps can be reached here: `/chat/production/`**`<UNIQUE_AGENT_ID>`**

Otherwise via REST:

1. Deploy the Agent:

   Make a **`POST`** to `/administration/production/deploy/`**`<UNIQUE_AGENT_ID>`**`?version=`**`<AGENT_VERSION>`**

   You will receive a `202` http code.
2. Since deployment could take a while it has been made **asynchronous**.
3. Make a **`GET`** to `/administration/production/deploymentstatus/`**`<UNIQUE_AGENT_ID>`**`?version=`**`<AGENT_VERSION>`** to find out the status of deployment.

**`NOT_FOUND`**, **`IN_PROGRESS`**, **`ERROR` and `READY`** is what you can expect to be returned in the body.

1. As soon as the Agent is deployed and has `READY` status, make a **`POST`** to `/agents/`**`<UNIQUE_AGENT_ID>`**/start
   1. You will receive a `201` with the `URI` for the newly created Conversation, like this:
      1. e.g.

         `eddi://ai.labs.conversation/conversationstore/conversations/`**`<UNIQUE_CONVERSATION_ID>`**
2. Now it's time to start talking to our Agent 1. Make a **`POST`** to `/agents/`**`<UNIQUE_AGENT_ID>`**/start`/`**`<UNIQUE_CONVERSATION_ID>`**

**Option 1:** is to hand over the input text as `contentType text/plain`. Include the User Input in the body as `text/plain` (e.g. Hello)

**Option 2:** is to hand over the input as `contentType application/json`, which also allows you to handover context information that you can use with the eddi configurations 1. Include the User Input in the body as application/json (e.g. Hello)

```
{
     "input": "some user input"
}
```

1. You have two query params you can use to config the returned output 1. `returnDetailed` - default is false - will return all sub results of the entire conversation steps, otherwise only public ones such as input, action, output & quickreplies 2. `returnCurrentStepOnly` - default is true - will return only the latest conversation step that has just been processed, otherwise returns all conversation steps since the beginning of this conversation
2. The output from the agent will be returned as JSON
3. If you are interested in fetching the **`conversationmemory`** at any given time, make a **`GET`** to `/agents/`**`<UNIQUE_AGENT_ID>`**/start`/`**`<UNIQUE_CONVERSATION_ID>`**`?returnDetailed=true` (the query param is optional, default is false)

> If you made it till here, CONGRATULATIONS, you have created your first Agent with **EDDI** !

By the way you can use the attached **postman collection** below to do all of the steps mentioned above by clicking send on each request in postman.

1. Create dictionary (greetings)
2. Create behaviourSet
3. Create outputSet
4. Creating package
5. Creating agent
6. Deploy the agent
7. Create conversation
8. Say Hello to the agent

Download the [Postman collection](https://github.com/labsai/EDDI/blob/main/docs/.gitbook/assets/Creating%20and%20chatting%20with%20a%20bot.postman_collection.json) to run through all the steps above.

### External Links

[Using collections in postman](https://thinkster.io/tutorials/testing-backend-apis-with-postman/using-collections-in-postman)


# Your first agent

How to use the godfather agent

After EDDI has started point your browser to this URL:

> Default URL: <http://localhost:7070/>

You should be seeing this on the page

<figure><img src="/files/HVAYZHFS5hUDuAOKOVYL" alt=""><figcaption><p>EDDI start screen without agents deployed</p></figcaption></figure>

Click on "Deploy Example Agent". This will deploy the Agent Father agent, to help you create your own agents utilizing ChatGPT.

<figure><img src="/files/Vm8E0JULdHDWGKEvON7q" alt=""><figcaption><p>EDDI start screen with Agent Father agent deployed</p></figcaption></figure>

This agent walks you through the process of creating a new agent that talks to ChatGPT. In order to create that agent, you will need to get an API key from ChatGPT. This key can be obtained here:

> Get an OpenAI API key at [platform.openai.com/account/api-keys](https://platform.openai.com/account/api-keys)
>
> **Note:** In v6, Agent Father supports any of the 12 LLM providers (OpenAI, Anthropic, Gemini, Mistral, etc.). API keys are stored securely in the Secrets Vault.

As soon as you have your key, click on Open and the first conversation with the Agent Father will start.

<figure><img src="/files/LoFcYVqOWFD9uyv8LwLB" alt=""><figcaption><p>Agent Father conversation start</p></figcaption></figure>

Click on "Let's get started!" to start the process.

<figure><img src="/files/JvMpP72mo9xCuyMZrzzJ" alt=""><figcaption><p>Provide a name for the agent</p></figcaption></figure>

First you need to type the name of your agent.

<figure><img src="/files/qeWcd98at5EJXMtV4Bbh" alt=""><figcaption><p>Define the purpose of the agent</p></figcaption></figure>

Next, you need to define the purpose of the agent.

<figure><img src="/files/NGGR8U6zo0HF7hwvQxwf" alt=""><figcaption><p>Introduction prompt</p></figcaption></figure>

Every agent needs an introduction to set the tone and the context of the agent for ChatGPT. This is the first prompt. This defines how ChatGPT is going to reply.

<figure><img src="/files/WOnL1IsDbOFEbmtYmPZ4" alt=""><figcaption><p>Enter ChatGPT API Key</p></figcaption></figure>

Now, the Agent Father needs the ChatGPT API key. Please enter the key here without any leading or trailing blanks.

<figure><img src="/files/tDnjPBD7osaK32UmkpKK" alt=""><figcaption><p>Agent creation screen</p></figcaption></figure>

Everything is now set up. When clicking on "Create the agent!" the Agent Father will create a new agent and you can start talking to it.

<figure><img src="/files/dKViv3LC7dGAv3HefxyK" alt=""><figcaption><p>EDDI Dashboard with deployed first agent</p></figcaption></figure>

The new agent is deployed and by clicking on open, you can instantly talk to the agent.


# Understanding your first agent

The components of the generated agents

The Agent Father generates EDDI specific configuration in order to deploy an agent that utilises the ChatGPT API. EDDI is an middleware and therefore it enables the user to create a structured flow for parsing and manipulating user input, to be used by different agent APIs. The generated agents enable you to take the user input and to prepare the input to use the ChatGPT API without the need to have any knowledge of the API. This approach can be used with any agent engine. Therefore EDDI becomes the abstraction layer of your agent infrastructure.

In order to see the configuration necessary for an agent, we are going to open the agent manager that can be found on the dashboard.

<figure><img src="/files/bHIG5wDgVFw6PAzQPTlt" alt=""><figcaption><p>EDDI Dashboard with deployed demo agent</p></figcaption></figure>

After opening the agent manager a new tab opens that shows and overview of all currently configured agents.

<figure><img src="/files/l7ZrcFdrhXPcEb6uTJhD" alt=""><figcaption><p>EDDI agent manager overview</p></figcaption></figure>

Click on your created agent ("My first agent") in this example to see the agent configuration

<figure><img src="/files/zARvSx8E8gHp4RHeY1Lg" alt=""><figcaption><p>Configuration overview of the example agent</p></figcaption></figure>

The agent consists of different resources. These resources separate the different functions that are necessary to create a flow. The execution sequence of the packages is:

1. Property (eddi://ai.labs.property)
2. Parser (eddi://ai.labs.parser)
3. Behavior (eddi://ai.labs.parser)
4. HttpCalls (eddi://ai.labs.httpcalls)
5. Output (eddi://ai.labs.output)
6. Templating (eddi://ai.labs.templating)

When a user enters an input all resources are executed in the sequence above. After the execution of all the resources the agent output is generated. The user can then add another input, which triggers the next execution sequence to generate the next output.

### Property

This resource holds all necessary properties to be stored for usage. For the case of generated agent these are:

* chatGptApi - the URL of the ChatGPT API
* chatGptModel - the model of ChatGPT that should be used
* chatGptToken - the API key of ChatGPT
* chatGptSystemPrompt - the prompt used to configure ChatGPT
* chatGptIntroPrompt - the promt that is displayed to the user on starting the conversation\\

### Parser

This resource parses the user input. It can be configured to understand different phrases. In the example agent there is no parser configuration necessary.

### Behavior

This resource defines behaviors depending on user input, properties, http calls or context through behavior rules. In our example it is configured to take any input and create and action called "send\_message".

### HTTP Calls

This resource represents http calls to APIs. It is configured to call the ChatGPT API using the API key, whenever a user input has triggered the "send\_message" action. As all user input triggers the "send\_message" action, the package always calls the ChatGPT API with the user input.

It is also configured to build the output. The ChatGPT API responds with a JSON response. This is automatically parsed and the human readable output of the API is found and converted into an output. This output will be put in the chat.

### Output

All output that is not handled via ChatGPT is created in this resource. In the example agent the only output necessary is the conversation start prompt.

### Templating

This is an internal resource that enables EDDI to substitute template strings with values from the conversation.


# Putting It All Together

**Version: 6.2.0**

This guide shows how all of EDDI's components work together to create a complete, functional agent. We'll build a real-world example step-by-step, explaining how each piece connects.

## The Big Picture

EDDI agents are composed of interconnected components that flow through the Lifecycle Pipeline:

```
Dictionary → Parser → Behavior Rules → Actions → HTTP Calls / LLM → Output → User
    ↓          ↓           ↓              ↓            ↓              ↓
  Define    Extract    Decide what   Triggers    Fetch data    Format    Response
  words     meaning    to do        execution    or call AI   response
```

Each component is a **separate configuration** that's **combined into packages**, which are **assembled into agents**.

## Real-World Example: Hotel Booking Agent

Let's build an agent that helps users book hotel rooms. It will:

1. Greet users
2. Ask for city and dates
3. Check availability via API
4. Show options
5. Confirm booking via API

### Component Overview

We'll need:

* **Dictionary**: Define hotel-related vocabulary
* **Parser**: Extract entities (cities, dates)
* **Behavior Rules**: Conversation flow logic
* **Properties**: Store user inputs
* **HTTP Calls**: Check availability and create bookings
* **Output Templates**: Display results dynamically
* **Workflow**: Combine everything
* **Agent**: Reference the package

## Step 1: Create the Dictionary

**Purpose**: Teach the agent hotel-related language

```bash
curl -X POST http://localhost:7070/regulardictionarystore/regulardictionaries \
  -H "Content-Type: application/json" \
  -d '{
    "language": "en",
    "words": [
      {
        "word": "hotel",
        "expressions": "entity(hotel)",
        "frequency": 0
      },
      {
        "word": "room",
        "expressions": "entity(room)",
        "frequency": 0
      },
      {
        "word": "book",
        "expressions": "intent(book)",
        "frequency": 0
      },
      {
        "word": "reserve",
        "expressions": "intent(book)",
        "frequency": 0
      },
      {
        "word": "availability",
        "expressions": "intent(check_availability)",
        "frequency": 0
      }
    ],
    "phrases": [
      {
        "phrase": "check availability",
        "expressions": "intent(check_availability)"
      },
      {
        "phrase": "I want to book",
        "expressions": "intent(book)"
      }
    ]
  }'
```

**Returns**: `eddi://ai.labs.regulardictionary/regulardictionarystore/regulardictionaries/DICT_ID?version=1`

**How it connects**: Parser will use this dictionary to convert "I want to book a hotel" → `["intent(book)", "entity(hotel)"]`

## Step 2: Create Behavior Rules

**Purpose**: Define conversation logic and when to trigger actions

```bash
curl -X POST http://localhost:7070/behaviorstore/behaviorsets \
  -H "Content-Type: application/json" \
  -d '{
    "behaviorGroups": [
      {
        "name": "Onboarding",
        "behaviorRules": [
          {
            "name": "Welcome",
            "conditions": [
              {
                "type": "occurrence",
                "configs": {
                  "maxTimesOccurred": "0",
                  "behaviorRuleName": "Welcome"
                }
              }
            ],
            "actions": ["welcome"]
          }
        ]
      },
      {
        "name": "Booking Flow",
        "behaviorRules": [
          {
            "name": "Check Availability",
            "conditions": [
              {
                "type": "inputmatcher",
                "configs": {
                  "expressions": "intent(check_availability)",
                  "occurrence": "currentStep"
                }
              },
              {
                "type": "contextmatcher",
                "configs": {
                  "contextKey": "city",
                  "contextType": "string"
                }
              }
            ],
            "actions": ["httpcall(check-availability)"]
          },
          {
            "name": "Book Room",
            "conditions": [
              {
                "type": "inputmatcher",
                "configs": {
                  "expressions": "intent(book)",
                  "occurrence": "currentStep"
                }
              },
              {
                "type": "contextmatcher",
                "configs": {
                  "contextKey": "selectedRoom",
                  "contextType": "string"
                }
              }
            ],
            "actions": ["httpcall(create-booking)", "booking_confirmed"]
          }
        ]
      }
    ]
  }'
```

**Returns**: `eddi://ai.labs.behavior/behaviorstore/behaviorsets/BEHAVIOR_ID?version=1`

**How it connects**:

* Welcome rule triggers on first message → shows welcome output
* Check Availability rule triggers when user asks about availability AND city is in context → calls API
* Book Room rule triggers when user wants to book AND room is selected → creates booking

## Step 3: Create Property Configuration

**Purpose**: Extract and store user-provided data

```bash
curl -X POST http://localhost:7070/propertysetterstore/propertysetters \
  -H "Content-Type: application/json" \
  -d '{
    "propertyInstructions": [
      {
        "name": "city",
        "fromObjectPath": "input",
        "scope": "conversation"
      },
      {
        "name": "selectedRoom",
        "fromObjectPath": "input",
        "scope": "conversation"
      }
    ]
  }'
```

**Returns**: `eddi://ai.labs.property/propertysetterstore/propertysetters/PROPERTY_ID?version=1`

**How it connects**: When user says "Paris", property extractor saves it as `context.city` for use in behavior rules and HTTP calls

## Step 4: Create HTTP Calls

**Purpose**: Integrate with hotel booking API

```bash
curl -X POST http://localhost:7070/httpcallsstore/httpcalls \
  -H "Content-Type: application/json" \
  -d '{
    "targetServerUrl": "https://api.hotels.example.com",
    "httpCalls": [
      {
        "name": "check-availability",
        "saveResponse": true,
        "responseObjectName": "availableRooms",
        "actions": ["httpcall(check-availability)"],
        "request": {
          "method": "GET",
          "path": "/availability",
          "queryParams": {
            "city": "{context.city}",
            "checkIn": "{context.checkInDate}",
            "checkOut": "{context.checkOutDate}"
          }
        },
        "postResponse": {
          "qrBuildInstruction": {
            "pathToTargetArray": "availableRooms.rooms",
            "iterationObjectName": "room",
            "quickReplyValue": "{room.name}",
            "quickReplyExpressions": "property(room_id({room.id}))"
          }
        }
      },
      {
        "name": "create-booking",
        "saveResponse": true,
        "responseObjectName": "bookingConfirmation",
        "actions": ["httpcall(create-booking)"],
        "request": {
          "method": "POST",
          "path": "/bookings",
          "contentType": "application/json",
          "body": "{\\\"roomId\\\": \\\"{context.selectedRoom}\\\", \\\"userId\\\": \\\"{context.userId}\\\", \\\"checkIn\\\": \\\"{context.checkInDate}\\\", \\\"checkOut\\\": \\\"{context.checkOutDate}\\\"}"
        },
        "postResponse": {
          "propertyInstructions": [
            {
              "name": "bookingId",
              "fromObjectPath": "bookingConfirmation.bookingId",
              "scope": "conversation"
            },
            {
              "name": "totalPrice",
              "fromObjectPath": "bookingConfirmation.totalPrice",
              "scope": "conversation"
            }
          ]
        }
      }
    ]
  }'
```

**Returns**: `eddi://ai.labs.httpcalls/httpcallsstore/httpcalls/HTTP_ID?version=1`

**How it connects**:

* `check-availability` call is triggered by behavior rule → fetches available rooms → creates quick reply buttons
* `create-booking` call is triggered after user selects room → creates booking → stores booking ID and price

## Step 5: Create Output Templates

**Purpose**: Define agent responses with dynamic data

```bash
curl -X POST http://localhost:7070/outputstore/outputsets \
  -H "Content-Type: application/json" \
  -d '{
    "outputSet": [
      {
        "action": "welcome",
        "outputs": [
          {
            "valueAlternatives": [
              {
                "type": "text",
                "text": "Welcome to Hotel Booking Agent! I can help you find and book hotel rooms. Which city are you interested in?"
              }
            ]
          }
        ]
      },
      {
        "action": "httpcall(check-availability)",
        "outputs": [
          {
            "valueAlternatives": [
              {
                "type": "text",
                "text": "Great! I found {memory.current.httpCalls.availableRooms.rooms.size()} available rooms in {context.city}. Here are your options:"
              }
            ]
          }
        ]
      },
      {
        "action": "booking_confirmed",
        "outputs": [
          {
            "valueAlternatives": [
              {
                "type": "text",
                "text": "🎉 Booking confirmed! Your booking ID is {context.bookingId}. Total price: ${context.totalPrice}. We'\''ve sent a confirmation email. Have a great stay!"
              }
            ]
          }
        ]
      }
    ]
  }'
```

**Returns**: `eddi://ai.labs.output/outputstore/outputsets/OUTPUT_ID?version=1`

**How it connects**:

* `welcome` action → shows greeting
* `httpcall(check-availability)` action → shows room count dynamically from API response
* `booking_confirmed` action → shows booking details from stored properties

## Step 6: Create Workflow

**Purpose**: Bundle all components together

```bash
curl -X POST http://localhost:7070/packagestore/packages \
  -H "Content-Type: application/json" \
  -d '{
    "packageExtensions": [
      {
        "type": "eddi://ai.labs.parser.dictionaries.regular",
        "extensions": {
          "uri": "eddi://ai.labs.regulardictionary/regulardictionarystore/regulardictionaries/DICT_ID?version=1"
        }
      },
      {
        "type": "eddi://ai.labs.behavior",
        "extensions": {
          "uri": "eddi://ai.labs.behavior/behaviorstore/behaviorsets/BEHAVIOR_ID?version=1"
        },
        "config": {
          "appendActions": true
        }
      },
      {
        "type": "eddi://ai.labs.property",
        "extensions": {
          "uri": "eddi://ai.labs.property/propertysetterstore/propertysetters/PROPERTY_ID?version=1"
        }
      },
      {
        "type": "eddi://ai.labs.httpcalls",
        "extensions": {
          "uri": "eddi://ai.labs.httpcalls/httpcallsstore/httpcalls/HTTP_ID?version=1"
        }
      },
      {
        "type": "eddi://ai.labs.output",
        "extensions": {
          "uri": "eddi://ai.labs.output/outputstore/outputsets/OUTPUT_ID?version=1"
        }
      },
      {
        "type": "eddi://ai.labs.templating"
      }
    ]
  }'
```

**Returns**: `eddi://ai.labs.package/packagestore/packages/WORKFLOW_ID?version=1`

**How it connects**: Workflow defines the order of lifecycle tasks and loads all configurations

## Step 7: Create Agent

**Purpose**: Create the top-level agent entity

```bash
curl -X POST http://localhost:7070/agentstore/agents \
  -H "Content-Type: application/json" \
  -d '{
    "packages": [
      "eddi://ai.labs.package/packagestore/packages/WORKFLOW_ID?version=1"
    ]
  }'
```

**Returns**: Agent ID (e.g., `AGENT_ID`)

**How it connects**: Agent references the package, which contains all the components

## Step 8: Deploy Agent

```bash
curl -X POST "http://localhost:7070/administration/production/deploy/AGENT_ID?version=1&autoDeploy=true"
```

**Result**: Agent is now active and ready to handle conversations!

## Step 9: Test the Agent

### Initial Conversation

```bash
curl -X POST "http://localhost:7070/agents/AGENT_ID/start" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Response**:

```json
{
  "conversationId": "CONV_ID",
  "conversationOutputs": [
    {
      "output": [
        "Welcome to Hotel Booking Agent! I can help you find and book hotel rooms. Which city are you interested in?"
      ]
    }
  ]
}
```

### Provide City and Check Availability

```bash
curl -X POST "http://localhost:7070/agents/CONV_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "check availability in Paris",
    "context": {
      "city": {"type": "string", "value": "Paris"},
      "checkInDate": {"type": "string", "value": "2025-06-01"},
      "checkOutDate": {"type": "string", "value": "2025-06-05"}
    }
  }'
```

**What happens internally**:

1. **Parser**: "check availability in Paris" → `["intent(check_availability)", "entity(hotel)"]`
2. **Behavior Rules**: Matches "Check Availability" rule (has intent + city in context)
3. **Actions**: Triggers `httpcall(check-availability)`
4. **HTTP Call**: `GET https://api.hotels.example.com/availability?city=Paris&checkIn=2025-06-01&checkOut=2025-06-05`
5. **Response Processing**: Creates quick reply buttons from room list
6. **Output**: Shows available rooms with dynamic count
7. **Memory**: Stores API response for later use

**Response**:

```json
{
  "conversationOutputs": [
    {
      "output": [
        "Great! I found 5 available rooms in Paris. Here are your options:"
      ],
      "quickReplies": [
        { "value": "Deluxe Suite", "expressions": "property(room_id(101))" },
        { "value": "Standard Room", "expressions": "property(room_id(102))" },
        { "value": "Executive Suite", "expressions": "property(room_id(103))" }
      ]
    }
  ]
}
```

### Book a Room

```bash
curl -X POST "http://localhost:7070/agents/CONV_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "book Deluxe Suite",
    "context": {
      "selectedRoom": {"type": "string", "value": "101"},
      "userId": {"type": "string", "value": "user-789"}
    }
  }'
```

**What happens internally**:

1. **Parser**: "book Deluxe Suite" → `["intent(book)", "entity(room)"]`
2. **Behavior Rules**: Matches "Book Room" rule (has intent + selectedRoom)
3. **Actions**: Triggers `httpcall(create-booking)` and `booking_confirmed`
4. **HTTP Call**: `POST https://api.hotels.example.com/bookings` with room details
5. **Response Processing**: Extracts bookingId and totalPrice, stores in properties
6. **Output**: Shows confirmation with dynamic booking details

**Response**:

```json
{
  "conversationOutputs": [
    {
      "output": [
        "🎉 Booking confirmed! Your booking ID is BK-12345. Total price: $450. We've sent a confirmation email. Have a great stay!"
      ]
    }
  ]
}
```

## How the Components Connect: Visual Flow

```
User: "check availability in Paris"
    ↓
┌─────────────────────────────────────────────────────────────┐
│ 1. PARSER (uses Dictionary)                                 │
│    Input: "check availability in Paris"                     │
│    Output: ["intent(check_availability)"]                   │
└─────────────────────────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────────────────────────┐
│ 2. BEHAVIOR RULES                                            │
│    Condition: intent(check_availability) + context.city     │
│    Match: YES                                                │
│    Action: httpcall(check-availability)                      │
└─────────────────────────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────────────────────────┐
│ 3. HTTP CALLS                                                │
│    Name: check-availability                                  │
│    URL: GET /availability?city=Paris                         │
│    Response: {rooms: [{id: 101, name: "Deluxe"}, ...]}      │
│    Stores: memory.current.httpCalls.availableRooms          │
└─────────────────────────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────────────────────────┐
│ 4. QUICK REPLY BUILDER                                       │
│    Iterates: availableRooms.rooms                           │
│    Creates: Quick reply buttons for each room               │
└─────────────────────────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────────────────────────┐
│ 5. OUTPUT TEMPLATING                                         │
│    Template: "I found {availableRooms.rooms.size()}           │
│              rooms in {context.city}"                         │
│    Result: "I found 5 rooms in Paris"                        │
└─────────────────────────────────────────────────────────────┘
    ↓
Response to User with output + quick replies
```

## Key Takeaways

### 1. Components are Modular

Each component (dictionary, behavior rules, HTTP calls, outputs) is:

* Created independently via API
* Versioned separately
* Reusable across multiple agents
* Testable in isolation

### 2. Workflows Define Execution Order

The order in the package matters:

```
Parser → Behavior Rules → Properties → HTTP Calls → Output → Templating
```

This is the lifecycle pipeline order.

### 3. Behavior Rules are the Orchestrator

Behavior rules decide:

* WHEN to call APIs (`httpcall(check-availability)`)
* WHEN to show outputs (`welcome`, `booking_confirmed`)
* WHICH actions to trigger based on conditions

### 4. Memory is the Connector

Everything stores data in and reads from conversation memory:

* HTTP Calls store responses: `memory.current.httpCalls.availableRooms`
* Properties store extracted data: `context.city`
* Outputs read data: `{context.bookingId}`

### 5. Context Bridges External Systems

Your application passes context to inject real-world data:

* User IDs
* Session tokens
* Business state
* Configuration

## Common Patterns

### Pattern 1: Progressive Data Collection

```
Step 1: Ask for city → Store in property
Step 2: Ask for dates → Store in property
Step 3: When all data present → Trigger API call
```

### Pattern 2: API-Then-LLM

```
Step 1: Fetch data via HTTP Call
Step 2: Pass data to LLM with context
Step 3: LLM formats response naturally
```

### Pattern 3: Multi-Step Confirmation

```
Step 1: Show options (quick replies)
Step 2: User selects → Store selection
Step 3: Confirm selection → Trigger action
```

## Next Steps

* **Add LLM Integration**: Use OpenAI to handle natural language queries
* **Add Error Handling**: Create behavior rules for failed API calls
* **Add Validation**: Check date formats, availability before booking
* **Add Conversation Memory**: Store booking history across conversations
* **Export for Reuse**: Export the agent and share with team

## Related Documentation

* [Architecture Overview](/architecture-and-concepts/architecture) - Understand the big picture
* [Developer Quickstart](/getting-started/developer-quickstart) - Quick start guide
* [Behavior Rules](/agent-configuration/behavior-rules) - Master decision logic
* [HTTP Calls](/agent-configuration/httpcalls) - API integration details
* [Output Templating](/agent-configuration/output-templating) - Dynamic responses
* [Conversation Memory](/architecture-and-concepts/conversation-memory) - State management


# Import/Export an Agent

## Overview

**Import/Export** functionality allows you to package entire agents (including all their dependencies) into portable ZIP files. This is essential for agent lifecycle management, collaboration, and deployment automation.

### Why Import/Export?

**Use Cases**:

* **Backup & Restore**: Protect your agent configurations from accidental deletion or corruption
* **Version Control**: Store agent configurations alongside code in Git
* **Environment Migration**: Move agents from development → staging → production
* **Continuous Sync**: Keep agents synchronized across environments with **merge imports**
* **Team Collaboration**: Share agents with team members or customers
* **Disaster Recovery**: Quickly restore agents after system failures
* **Agent Templates**: Create reusable agent templates for similar use cases
* **CI/CD Integration**: Automate agent deployment in your pipeline

### What Gets Exported?

When you export an agent, EDDI packages:

* ✅ Agent configuration (package references)
* ✅ All packages used by the agent
* ✅ All extensions (behavior rules, dictionaries, HTTP calls, outputs, etc.)
* ✅ Version information
* ✅ Configuration metadata
* ✅ **Origin IDs** (resource identifiers for merge tracking)

**Note**: Conversations and conversation history are **NOT** exported (only configurations).

### Import Strategies

EDDI supports three import strategies:

| Strategy             | Behavior                                                             | Use Case                             |
| -------------------- | -------------------------------------------------------------------- | ------------------------------------ |
| **Create** (default) | Always creates a new agent with new IDs                              | First-time import, creating copies   |
| **Merge**            | Updates existing resources by matching origin IDs                    | Syncing changes across environments  |
| **Upgrade**          | Updates existing agent by structural matching (no origin IDs needed) | Syncing independently created agents |

### Export/Import Workflow

**First-time import (Create):**

```
DEVELOPMENT EDDI
    ↓
1. Export Agent
   POST /backup/export/agent123?agentVersion=1
   ← Returns: agent123-1.zip
    ↓
2. Download ZIP file
   GET /backup/export/agent123-1.zip
   ← Receives: agent123-1.zip file
    ↓
3. Store in version control / backup / transfer
    ↓
PRODUCTION EDDI
    ↓
4. Upload ZIP file
   POST /backup/import
   Body: (application/zip with ZIP file)
   ← Returns: New agent ID (Location header)
    ↓
5. Deploy imported agent
   POST /administration/production/deploy/{newAgentId}?version=1
```

**Subsequent imports (Merge/Sync):**

```
DEVELOPMENT EDDI  (agent updated since last sync)
    ↓
1. Export latest version
   POST /backup/export/agent123?agentVersion=2
    ↓
2. Download ZIP
    ↓
PRODUCTION EDDI  (has the agent from first import)
    ↓
3. Preview what would change
   POST /backup/import/preview
   ← Returns: list of resources with CREATE/UPDATE/SKIP actions
    ↓
4. Review changes, optionally deselect resources
    ↓
5. Merge import (updates existing, no duplicates)
   POST /backup/import?strategy=merge&selectedResources=origin1,origin2
   ← Returns: Same agent ID, incremented version
    ↓
6. Deploy updated agent
```

### Best Practices

* **Version Your Exports**: Include version numbers in filenames: `customer-support-agent-v2.3.zip`
* **Preview Before Merge**: Always use the preview endpoint before merging to review changes
* **Selective Merge**: Only merge the resources that actually changed to minimize risk
* **Document Changes**: Keep a changelog of what changed between exports
* **Regular Backups**: Schedule automated exports of production agents
* **Test Imports**: Always test imported agents in a test environment first
* **Store Securely**: Keep exports in secure, version-controlled storage (e.g., Git LFS, S3)

### Common Scenarios

**Scenario 1: Promoting to Production (first time)**

```bash
# 1. Export from test environment
curl -X POST http://test.eddi.com/backup/export/agent123?agentVersion=1

# 2. Download the ZIP
curl -O http://test.eddi.com/backup/export/agent123-1.zip

# 3. Import to production (creates new agent)
curl -X POST -H "Content-Type: application/zip" \
  --data-binary @agent123-1.zip http://prod.eddi.com/backup/import

# 4. Deploy in production
curl -X POST http://prod.eddi.com/administration/production/deploy/{newAgentId}?version=1
```

**Scenario 2: Syncing Updates (merge)**

```bash
# 1. Export updated agent from dev
curl -X POST http://dev.eddi.com/backup/export/agent123?agentVersion=3
curl -O http://dev.eddi.com/backup/export/agent123-3.zip

# 2. Preview what would change in production
curl -X POST -H "Content-Type: application/zip" \
  --data-binary @agent123-3.zip http://prod.eddi.com/backup/import/preview

# 3. Merge import — updates existing resources, no duplicates
curl -X POST -H "Content-Type: application/zip" \
  --data-binary @agent123-3.zip "http://prod.eddi.com/backup/import?strategy=merge"

# 4. Redeploy
curl -X POST http://prod.eddi.com/administration/production/deploy/{agentId}?version=2
```

**Scenario 3: Selective Merge (only specific resources)**

```bash
# Preview first to get the origin IDs
curl -X POST -H "Content-Type: application/zip" \
  --data-binary @agent123-3.zip http://prod.eddi.com/backup/import/preview
# Response includes originId for each resource

# Merge only the behavior rules and HTTP calls (by origin ID)
curl -X POST -H "Content-Type: application/zip" \
  --data-binary @agent123-3.zip \
  "http://prod.eddi.com/backup/import?strategy=merge&selectedResources=origin-beh-1,origin-http-1"
```

**Scenario 4: Disaster Recovery**

```bash
# Regular automated backup (cron job)
#!/bin/bash
DATE=$(date +%Y%m%d)
curl -X POST http://prod.eddi.com/backup/export/agent123?agentVersion=1
curl -O http://prod.eddi.com/backup/export/agent123-1.zip
mv agent123-1.zip "backups/agent123-$DATE.zip"
aws s3 cp "backups/agent123-$DATE.zip" s3://agent-backups/

# Restore after failure
aws s3 cp s3://agent-backups/agent123-20250103.zip ./
curl -X POST -H "Content-Type: application/zip" \
  --data-binary @agent123-20250103.zip http://prod.eddi.com/backup/import
```

***

## Using the Manager UI

The EDDI Manager provides a guided import wizard accessible from the **Agents** page:

1. **Upload** — Drag-and-drop or browse for a `.zip` export file
2. **Choose Strategy**:
   * **Create New Agent** — Always creates a fresh agent (default)
   * **Merge / Sync** — Updates an existing agent if one with matching origin IDs exists
3. **Preview** (merge only) — Shows a table of all resources with their planned action:
   * 🟢 **New** — Resource doesn't exist locally, will be created
   * 🔵 **Update** — Resource exists locally, will be updated to the imported version
   * ⚪ **Skip** — Resource is identical, no changes needed
4. **Select Resources** — Checkboxes let you pick which resources to merge (all selected by default)
5. **Confirm** — Executes the import

***

## How Merge Tracking Works

When an agent is first imported into an EDDI instance, EDDI stores the **origin ID** of each resource (the ID it had on the source system) in the `DocumentDescriptor`. On subsequent imports with `strategy=merge`:

1. EDDI reads each resource from the ZIP
2. Looks up the origin ID in the local descriptor store (`findByOriginId`)
3. If found → **updates** the existing resource (creating a new version)
4. If not found → **creates** a new resource
5. The agent itself is updated with references to the (possibly new) resource versions

This means the **agent ID stays the same** across merge imports — only the version increments. Deployments, triggers, and integrations that reference the agent ID continue to work without reconfiguration.

***

## API Reference

### Exporting an Agent

Send a **`POST`** request to export. The response `Location` header contains the download URL.

| Element      | Value                                             |
| ------------ | ------------------------------------------------- |
| HTTP Method  | `POST`                                            |
| API Endpoint | `/backup/export/{agentId}?agentVersion={version}` |
| Response     | `Location` header with ZIP download URL           |

**Example:**

```bash
curl -X POST http://localhost:7070/backup/export/agent123?agentVersion=1
# Response Header: Location: /backup/export/agent123-1.zip

curl -O http://localhost:7070/backup/export/agent123-1.zip
```

### Importing an Agent (Create)

Upload a ZIP file to create a new agent.

| Element      | Value                                |
| ------------ | ------------------------------------ |
| HTTP Method  | `POST`                               |
| API Endpoint | `/backup/import`                     |
| Content-Type | `application/zip`                    |
| Request Body | ZIP file binary                      |
| Response     | `Location` header with new agent URI |

**Example:**

```bash
curl -X POST -H "Content-Type: application/zip" \
  --data-binary @agent-export.zip http://localhost:7070/backup/import
```

### Preview Merge Import

Dry-run analysis: returns what would change without modifying any data.

| Element      | Value                            |
| ------------ | -------------------------------- |
| HTTP Method  | `POST`                           |
| API Endpoint | `/backup/import/preview`         |
| Content-Type | `application/zip`                |
| Request Body | ZIP file binary                  |
| Response     | JSON with resource diff analysis |

**Response format:**

```json
{
  "agentOriginId": "original-agent-id-from-source",
  "agentName": "My Agent",
  "resources": [
    {
      "originId": "original-resource-id",
      "resourceType": "agent",
      "name": "My Agent",
      "action": "UPDATE",
      "localId": "local-agent-id",
      "localVersion": 1
    },
    {
      "originId": "original-behavior-id",
      "resourceType": "behavior",
      "name": "Greeting Rules",
      "action": "CREATE",
      "localId": null,
      "localVersion": null
    }
  ]
}
```

**Actions:**

* `CREATE` — No matching local resource found; will be created
* `UPDATE` — Matching local resource found; will be updated
* `SKIP` — Resource is unchanged; will be skipped

### Importing an Agent (Merge)

Update an existing agent by matching origin IDs.

| Element      | Value                                                      |
| ------------ | ---------------------------------------------------------- |
| HTTP Method  | `POST`                                                     |
| API Endpoint | `/backup/import?strategy=merge`                            |
| Content-Type | `application/zip`                                          |
| Request Body | ZIP file binary                                            |
| Query Params | `strategy=merge`, optional `selectedResources=id1,id2,...` |
| Response     | `Location` header with updated agent URI                   |

**Example (merge all):**

```bash
curl -X POST -H "Content-Type: application/zip" \
  --data-binary @agent-export.zip "http://localhost:7070/backup/import?strategy=merge"
```

**Example (selective merge):**

```bash
curl -X POST -H "Content-Type: application/zip" \
  --data-binary @agent-export.zip \
  "http://localhost:7070/backup/import?strategy=merge&selectedResources=origin-id-1,origin-id-2"
```

> **Important:** The agent will not be deployed after import — you must deploy it yourself using the [Deployment API](/conversations-and-orchestration/deployment-management-of-agents).

***

## Upgrade Strategy

In addition to `create` (new agent) and `merge` (by origin ID), EDDI supports an **`upgrade`** strategy that uses structural matching to sync content into an existing agent — even if the agents were created independently (no shared origin IDs):

```bash
# Preview what would change
curl -X POST -H "Content-Type: application/zip" \
  --data-binary @agent-export.zip \
  "http://localhost:7070/backup/import/preview?targetAgentId=local-agent-id"

# Execute upgrade
curl -X POST -H "Content-Type: application/zip" \
  --data-binary @agent-export.zip \
  "http://localhost:7070/backup/import?strategy=upgrade&targetAgentId=local-agent-id"
```

The upgrade strategy matches resources by **structure** (workflow position, extension type, snippet name) rather than by origin ID. See [Agent Sync](/conversations-and-orchestration/agent-sync-guide) for details on how structural matching works.

## Live Sync (Without ZIP)

If both EDDI instances are reachable over HTTP, you can skip the ZIP step entirely and sync directly between instances. See the [**Agent Sync Guide**](/conversations-and-orchestration/agent-sync-guide) for the full workflow.

***

## See Also

* [Agent Sync Guide](/conversations-and-orchestration/agent-sync-guide) — Live instance-to-instance sync and upgrade imports
* [Deployment Management](/conversations-and-orchestration/deployment-management-of-agents) — Deploying agents after import
* [Secrets Vault](/security-and-compliance/secrets-vault) — How API keys are scrubbed and re-vaulted during import


# Architecture Overview

**Version: 6.2.0**

This document provides a comprehensive overview of EDDI's architecture, design principles, and internal workflow.

## Table of Contents

1. [Overview](#overview)
2. [What EDDI Is (and Isn't)](#what-eddi-is-and-isnt)
3. [Core Architecture](#core-architecture)
4. [The Lifecycle Pipeline](#the-lifecycle-pipeline)
5. [Conversation Flow](#conversation-flow)
6. [Agent Composition Model](#agent-composition-model)
7. [Key Components](#key-components)
8. [Technology Stack](#technology-stack)
9. [Multi-Agent Orchestration](#multi-agent-orchestration)
10. [MCP Integration (Bilateral)](#mcp-integration-bilateral)
11. [Persistent User Memory](#persistent-user-memory)
12. [Agent Sync & Portability](#agent-sync--portability)

***

## Overview

E.D.D.I. (Enhanced Dialog Driven Interface) is a **multi-agent orchestration middleware** for conversational AI systems, not a standalone agent or language model. It sits between user-facing applications and multiple AI agents (LLMs like OpenAI, Claude, Gemini, or traditional REST APIs), intelligently routing requests, coordinating responses, and maintaining conversation state across agent interactions.

**Core Purpose**: Orchestrate multiple AI agents and business systems in complex conversational workflows without writing code.

## What EDDI Is (and Isn't)

* **A Multi-Agent Orchestration Middleware**: Coordinates multiple AI agents (LLMs, APIs) in complex workflows
* **An Intelligent Router**: Directs requests to appropriate agents based on patterns, rules, and context
* **A Conversation Coordinator**: Maintains stateful conversations across multiple agent interactions
* **A Configuration Engine**: Agent orchestration defined through JSON configurations, not code
* **A Middleware Service**: Acts as an intermediary that adds intelligence and control to conversation flows
* **Business System Integrator**: Connects AI agents with your existing APIs, databases, and services
* **Cloud-Native**: Built with Quarkus for fast startup, low memory footprint, and containerized deployment
* **Stateful**: Maintains complete conversation history and context throughout interactions

### EDDI Is Not:

* **Not a standalone LLM**: It doesn't train or run machine learning models
* **Not a chatbot platform**: It's the infrastructure that powers conversational agents
* **Not just a proxy**: It provides orchestration, state management, and complex behavior rules beyond simple API forwarding

***

## Core Architecture

### Architectural Principles

EDDI's architecture is built on several key principles:

1. **Modularity**: Every component is pluggable and replaceable
2. **Composability**: Agents are assembled from reusable workflows and extensions
3. **Asynchronous Processing**: Non-blocking I/O for handling concurrent conversations
4. **State-Driven**: All operations transform or query the conversation state
5. **Cloud-Native**: Designed for containerized, distributed deployments

### High-Level Architecture Diagram

```
┌─────────────────────────────────────────────────────────────┐
│                      User Application                        │
│                  (Web, Mobile, Chat Client)                  │
└────────────────────────────┬────────────────────────────────┘
                             │ HTTP/REST API
                             ▼
┌─────────────────────────────────────────────────────────────┐
│                      RestAgentEngine                           │
│          (Entry Point - JAX-RS AsyncResponse)                │
└────────────────────────────┬────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────┐
│                  ConversationCoordinator                     │
│           (Ensures Sequential Processing per                 │
│            Conversation, Concurrent Across)                  │
└────────────────────────────┬────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────┐
│                   IConversationMemory                        │
│       (Stateful Object - Complete Conversation Context)      │
└────────────────────────────┬────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────┐
│                     LifecycleManager                         │
│          (Executes Sequential Pipeline of Tasks)             │
└────────────────────────────┬────────────────────────────────┘
                             │
        ┌────────────────────┼────────────────────┐
        ▼                    ▼                    ▼
┌──────────────┐   ┌──────────────┐    ┌──────────────┐
│Input Parsing │   │Behavior Rules│    │LLM/API Calls │
│  (NLP, etc)  │   │(IF-THEN Logic│    │(LangChain4j, │
│              │   │              │    │ HTTP Calls)  │
└──────────────┘   └──────────────┘    └──────────────┘
        │                    │                    │
        └────────────────────┼────────────────────┘
                             ▼
                   ┌──────────────────┐
                   │Output Generation │
                   │  (Templating)    │
                   └──────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────┐
│                    MongoDB + Cache                           │
│         (Persistent Storage + Fast Retrieval)                │
└─────────────────────────────────────────────────────────────┘
```

***

## The Lifecycle Pipeline

The **Lifecycle** is EDDI's most distinctive architectural feature. Instead of hard-coded agent logic, EDDI processes every user interaction through a **configurable, sequential pipeline of tasks** called the **Lifecycle**.

### How the Lifecycle Works

1. **Pipeline Composition**: Each agent defines a sequence of `ILifecycleTask` components
2. **Sequential Execution**: Tasks execute one after another, each transforming the `IConversationMemory`
3. **Stateless Tasks**: Each task is stateless; all state resides in the memory object passed through
4. **Interruptible**: The pipeline can be stopped early based on conditions (e.g., `STOP_CONVERSATION`)

### Standard Lifecycle Tasks

A typical agent lifecycle includes these task types:

| Task Type               | Purpose                                         | Example                                   |
| ----------------------- | ----------------------------------------------- | ----------------------------------------- |
| **Input Parsing**       | Normalizes and understands user input           | Extracting entities, intents from text    |
| **Semantic Parsing**    | Uses dictionaries to parse expressions          | Matching "hello" → `greeting(hello)`      |
| **Behavior Rules**      | Evaluates IF-THEN rules to decide actions       | "If `greeting(*)` then `action(welcome)`" |
| **Property Extraction** | Extracts and stores data in conversation memory | Saving user name, preferences             |
| **HTTP Calls**          | Calls external REST APIs                        | Weather API, CRM systems                  |
| **LangChain Task**      | Invokes LLM APIs (OpenAI, Claude, etc.)         | Conversational AI responses               |
| **Output Generation**   | Formats final response using templates          | Qute templating with conversation data    |

### Lifecycle Task Interface

```java
public interface ILifecycleTask {
    TaskId getId();
    String getType();
    void execute(IConversationMemory memory, Object component)
        throws LifecycleException;
}
```

Every task receives:

* **IConversationMemory**: Complete conversation state
* **component**: Task-specific configuration/resources

***

## Conversation Flow

### Step-by-Step: User Interaction Flow

Here's what happens when a user sends a message to an EDDI agent:

#### 1. API Request

```
POST /agents/{conversationId}
Body: { "input": "Hello, what's the weather?", "context": {...} }
```

#### 2. RestAgentEngine Receives Request

* Validates agent ID and environment
* Wraps response in `AsyncResponse` for non-blocking processing
* Increments metrics counters

#### 3. ConversationCoordinator Queues Message

* Ensures messages for the same conversation are processed sequentially
* Allows different conversations to process concurrently
* Prevents race conditions in conversation state

#### 4. IConversationMemory Loaded/Created

* If existing conversation: Loads from MongoDB
* If new conversation: Creates fresh memory object
* Includes all previous steps, user data, context

#### 5. LifecycleManager Executes Pipeline

```
Input → Parser → Behavior Rules → API/LLM → Output → Save
```

Each task in sequence:

* Reads current conversation state
* Performs its operation (parsing, rule evaluation, API call, etc.)
* Writes results back to conversation memory
* Passes control to next task

#### 6. State Persistence

* Updated `IConversationMemory` saved to MongoDB
* Cache updated with latest conversation state
* Metrics recorded (duration, success/failure)

#### 7. Response Returned

```json
{
  "conversationState": "READY",
  "conversationOutputs": [
    {
      "output": ["The weather today is sunny with a high of 75°F"],
      "actions": ["weather_response"]
    }
  ]
}
```

***

## Agent Composition Model

EDDI agents are **not monolithic**. They are **composite objects** assembled from version-controlled, reusable components.

### Hierarchy: Agent → Workflow → Extensions

```
Agent (.agent.json)
  ├─ Workflow 1 (.workflow.json)
  │   ├─ Behavior Rules Extension (.behavior.json)
  │   ├─ HTTP Calls Extension (.httpcalls.json)
  │   └─ Output Extension (.output.json)
  ├─ Workflow 2 (.workflow.json)
  │   ├─ Dictionary Extension (.dictionary.json)
  │   └─ LangChain Extension (.langchain.json)
  └─ Workflow 3 (.workflow.json)
      └─ Property Extension (.property.json)
```

### 1. Agent Level

**File**: `{agentId}.agent.json`

A agent is simply a **list of workflow references**:

```json
{
  "workflows": [
    "eddi://ai.labs.workflow/workflowstore/workflows/{workflowId}?version={version}",
    "eddi://ai.labs.workflow/workflowstore/workflows/{anotherWorkflowId}?version={version}"
  ]
}
```

### 2. Workflow Level

**File**: `{workflowId}.workflow.json`

A workflow is a **container of functionality** with a list of steps:

```json
{
  "workflowSteps": [
    {
      "type": "eddi://ai.labs.behavior",
      "extensions": {
        "uri": "eddi://ai.labs.behavior/behaviorstore/behaviorsets/{behaviorId}?version={version}"
      },
      "config": {
        "appendActions": true
      }
    },
    {
      "type": "eddi://ai.labs.httpcalls",
      "extensions": {
        "uri": "eddi://ai.labs.httpcalls/httpcallsstore/httpcalls/{httpCallsId}?version={version}"
      }
    }
  ]
}
```

### 3. Extension Level

**Files**: `{extensionId}.{type}.json`

Extensions are the **actual agent logic**:

#### Behavior Rules Extension

```json
{
  "behaviorGroups": [
    {
      "name": "Greetings",
      "behaviorRules": [
        {
          "name": "Welcome User",
          "conditions": [
            {
              "type": "inputmatcher",
              "configs": {
                "expressions": "greeting(*)",
                "occurrence": "currentStep"
              }
            }
          ],
          "actions": ["welcome_action"]
        }
      ]
    }
  ]
}
```

#### HTTP Calls Extension

```json
{
  "targetServerUrl": "https://api.weather.com",
  "httpCalls": [
    {
      "name": "getWeather",
      "actions": ["fetch_weather"],
      "request": {
        "method": "GET",
        "path": "/current?location=${context.userLocation}"
      },
      "postResponse": {
        "propertyInstructions": [
          {
            "name": "currentWeather",
            "fromObjectPath": "weatherResponse.temperature",
            "scope": "conversation"
          }
        ]
      }
    }
  ]
}
```

#### LangChain Extension

```json
{
  "tasks": [
    {
      "actions": ["send_to_ai"],
      "id": "openaiChat",
      "type": "openai",
      "parameters": {
        "apiKey": "...",
        "modelName": "gpt-4o",
        "systemMessage": "You are a helpful assistant",
        "sendConversation": "true",
        "addToOutput": "true"
      }
    }
  ]
}
```

### What Lives Where: A Decision Guide

When adding a new feature, use this guide to decide where configuration belongs:

| Question                                                  | Config Level                                                  | Example                                |
| --------------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------- |
| Does it affect the entire agent across all conversations? | **Agent level** (`AgentConfiguration`)                        | `enableMemoryTools`, `enableStreaming` |
| Does it control how a pipeline step behaves?              | **Extension level** (e.g., `langchain.json`, `property.json`) | LLM parameters, property instructions  |
| Does it define which extensions run and in what order?    | **Workflow level** (`workflow.json`)                          | Extension types and URIs               |
| Is it a user-facing runtime setting?                      | **Agent level**                                               | User memory config, audit settings     |
| Is it a tool/capability the LLM can use?                  | **Extension level** (in `langchain.json`)                     | `builtInToolsWhitelist`                |

**Rule of thumb**: If a feature is a **cross-conversation concern** (e.g., persistent memory, user preferences, GDPR compliance), it belongs at the **agent level**. If it's a **per-turn processing concern** (e.g., LLM parameters, HTTP call config), it belongs at the **extension level**.

***

## Key Components

### RestAgentEngine

**Location**: `ai.labs.eddi.engine.internal.RestAgentEngine`

**Purpose**: Main entry point for all agent interactions

**Responsibilities**:

* Receives HTTP requests via JAX-RS
* Validates agent and conversation IDs
* Handles async responses
* Records metrics
* Coordinates with `IConversationCoordinator`

### ConversationCoordinator

**Location**: `ai.labs.eddi.engine.runtime.internal.ConversationCoordinator`

**Purpose**: Ensures proper message ordering and concurrency control

**Key Feature**: Uses a queue system to guarantee that:

* Messages within the same conversation are processed sequentially
* Different conversations can be processed in parallel
* No race conditions occur in conversation state updates

### IConversationMemory

**Location**: `ai.labs.eddi.engine.memory.IConversationMemory`

**Purpose**: The stateful object representing a complete conversation

**Contains**:

* Conversation ID, agent ID, user ID
* All previous conversation steps (history)
* Current step being processed
* User properties (name, preferences, etc.)
* Context data (passed with each request)
* Actions and outputs generated

**Key Methods**:

```java
String getConversationId();
IWritableConversationStep getCurrentStep();
IConversationStepStack getPreviousSteps();
ConversationState getConversationState();
void undoLastStep();
void redoLastStep();
```

### LifecycleManager

**Location**: `ai.labs.eddi.engine.lifecycle.internal.LifecycleManager`

**Purpose**: Executes the lifecycle pipeline

**Key Method**:

```java
void executeLifecycle(
    IConversationMemory conversationMemory,
    List<String> lifecycleTaskTypes
) throws LifecycleException
```

**How It Works**:

1. Iterates through registered `ILifecycleTask` instances
2. For each task, calls `task.execute(conversationMemory, component)`
3. Checks for interruption or stop conditions
4. Continues until all tasks complete or stop condition is met

### WorkflowConfiguration

**Location**: `ai.labs.eddi.configs.workflows.model.WorkflowConfiguration`

**Purpose**: Defines the structure of an agent workflow

**Model**:

```java
public class WorkflowConfiguration {
    private List<WorkflowStep> workflowSteps;

    public static class WorkflowStep {
        private URI type;
        private Map<String, Object> extensions;
        private Map<String, Object> config;
    }
}
```

### ToolExecutionService

**Location**: `ai.labs.eddi.modules.langchain.tools.ToolExecutionService`

**Purpose**: Unified execution pipeline for all AI agent tool invocations

**Pipeline**:

```
Tool Call ──▶ Rate Limiter ──▶ Cache Check ──▶ Execute ──▶ Cost Tracker ──▶ Result
```

**Features**:

* Token-bucket rate limiting per tool (configurable per-tool or global default)
* Smart caching — deduplicates calls with identical arguments
* Cost tracking with per-conversation budgets and automatic eviction
* Security: tools that accept URLs are validated against private/internal addresses (SSRF protection via `UrlValidationUtils`)
* Security: math expressions are evaluated in a sandboxed parser (`SafeMathParser`)

See the [Security documentation](/security-and-compliance/security) for details.

### System Prompt Modifiers

**Location**: `ai.labs.eddi.modules.llm.impl`

Two services modify the system prompt before it is sent to the LLM. Both are configured per-task in the LLM configuration (`langchain.json`).

| Service                      | Purpose                                                            | Config Key             |
| ---------------------------- | ------------------------------------------------------------------ | ---------------------- |
| **`IdentityMaskingService`** | Prepends identity concealment rules (agent name, refusal patterns) | `task.identityMasking` |
| **`CounterweightService`**   | Appends behavioral safety instructions (cautious/strict presets)   | `task.counterweight`   |

**Execution order**: Identity masking → Counterweight → LLM call.

Counterweight presets are resolved from [Prompt Snippets](/agent-configuration/prompt-snippets-guide) first (`counterweight-cautious`, `counterweight-strict`), falling back to built-in defaults. This ensures admins can customize safety language without code changes.

See [LLM Integration — Behavioral Safety](/agent-configuration/langchain#behavioral-safety-counterweight--identity-masking) for configuration details.

### Attachment Storage

**Location**: `ai.labs.eddi.engine.attachments`

The attachment subsystem handles binary file storage for multimodal conversations:

| Component                       | Purpose                                                                                                                             |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **`IAttachmentStore`**          | Interface for storing/loading binary attachments (GridFS for MongoDB, BLOB for PostgreSQL)                                          |
| **`MimeValidator`**             | Magic-byte detection (16+ formats) and declared-vs-detected MIME compatibility checking                                             |
| **`MultimodalMessageEnhancer`** | Converts stored attachments into langchain4j `Content` objects (images → `ImageContent` via base64 data URI, others → text markers) |

***

## Technology Stack

### Core Framework

* **Quarkus**: Supersonic, subatomic Java framework
  * Fast startup times (\~0.05s)
  * Low memory footprint
  * Native compilation support
  * Built-in observability (metrics, health checks)

### Language & Runtime

* **Java 25**: Latest LTS with modern language features
* **GraalVM**: Optional native compilation for even faster startup

### Dependency Injection

* **CDI (Contexts and Dependency Injection)**: Jakarta EE standard
* **@ApplicationScoped, @Inject**: Clean, testable component wiring

### REST Framework

* **JAX-RS**: Jakarta REST API standard
* **AsyncResponse**: Non-blocking, scalable request handling
* **JSON-B**: JSON binding for serialization/deserialization

### Database (DB-Agnostic)

* **MongoDB 6.0+** (default): Document store for agent configurations and conversation logs
* **PostgreSQL** (alternative): JDBC + JSONB storage, switchable via `eddi.datastore.type=postgres`
* Both backends support:
  * Agent, workflow, and extension configuration storage
  * Conversation history persistence
  * Version control of agent components
  * Automatic schema migration on startup

### Caching

* **Caffeine**: High-performance in-memory cache (replaced Infinispan in v6)
  * Caches conversation state and agent configurations
  * Configurable size limits per cache type
  * Zero external dependencies — provided transitively by `quarkus-cache`

### LLM Integration

* **LangChain4j**: Java library for LLM orchestration
  * Unified interface to multiple LLM providers
  * Supports OpenAI, Claude, Gemini, Ollama, Hugging Face, etc.
  * Handles chat message formatting, streaming, tool calling

### Observability

* **Micrometer**: Metrics collection
* **Prometheus**: Metrics exposition
* **Kubernetes Probes**: Liveness and readiness endpoints

### Security

* **OAuth 2.0**: Authentication and authorization
* **Keycloak**: Identity and access management

### Templating

* **Qute**: Output templating engine
  * Dynamic output generation
  * Access to conversation memory in templates
  * Expression language support

***

## Design Patterns Used

### 1. Strategy Pattern

* **Where**: Lifecycle tasks
* **Why**: Different behaviors (parsing, rules, API calls) implement the same `ILifecycleTask` interface

### 2. Chain of Responsibility

* **Where**: Lifecycle pipeline
* **Why**: Each task processes the memory object and passes it to the next task

### 3. Composite Pattern

* **Where**: Agent composition (Agent → Workflows → Extensions)
* **Why**: Agents are built from hierarchical, reusable components

### 4. Repository Pattern

* **Where**: Data access (stores: agentstore, workflowstore, etc.)
* **Why**: Abstracts data persistence from business logic

### 5. Factory Pattern

* **Where**: `IAgentFactory`
* **Why**: Complex agent instantiation from multiple workflows and configurations

### 6. Coordinator Pattern

* **Where**: `ConversationCoordinator`
* **Why**: Manages concurrent access to shared conversation state

***

## Performance Characteristics

### Startup Time

* **JVM mode**: < 2 seconds
* **Native mode**: < 50ms (with GraalVM)

### Memory Footprint

* **JVM mode**: \~200MB baseline
* **Native mode**: \~50MB baseline

### Request Latency

* **Without LLM**: 10-50ms (parsing, rules, simple API calls)
* **With LLM**: 500-5000ms (depends on LLM provider)

### Scalability

* **Vertical**: Handles thousands of concurrent conversations per instance
* **Horizontal**: Stateless design allows infinite horizontal scaling
* **Agenttleneck**: MongoDB becomes agenttleneck; use replica sets and sharding

***

## Cloud-Native Features

### Containerization

* Official Docker images: `labsai/eddi`
* Certified by IBM/Red Hat
* Multi-stage builds for minimal image size

### Orchestration

* Kubernetes-ready
* OpenShift certified
* Health checks built-in

### Configuration

* Externalized configuration via environment variables
* ConfigMaps and Secrets support
* No rebuild needed for configuration changes

### Observability

* Prometheus metrics endpoint: `/q/metrics`
* Health checks: `/q/health/live`, `/q/health/ready`
* Structured logging with correlation IDs

***

## Case Study: The "Agent Father"

The **Agent Father** is a meta-agent that demonstrates EDDI's architecture in action. It's an agent that creates other agents.

> **For a comprehensive, step-by-step walkthrough of Agent Father, see** [**Agent Father: A Deep Dive**](/advanced-concepts/agent-father-deep-dive)

### How It Works

1. **Conversation Start**: User starts chat with Agent Father
2. **Information Gathering**: Agent Father asks questions:
   * "What do you want to call your agent?"
   * "What should it do?"
   * "Which LLM API should it use?"
3. **Memory Storage**: Property setters save answers to conversation memory:
   * `context.agentName`
   * `context.agentDescription`
   * `context.llmType`
4. **Condition Triggers**: Behavior rule monitors memory:

   ```json
   {
     "conditions": [
       {
         "type": "contextmatcher",
         "configs": {
           "contextKey": "agentName",
           "contextType": "string"
         }
       }
     ],
     "actions": ["httpcall(create-agent)"]
   }
   ```
5. **API Call Execution**: HTTP Calls extension triggers:

   ```json
   {
     "name": "create-agent",
     "request": {
       "method": "POST",
       "path": "/agentstore/agents",
       "body": "{\"agentName\": \"${context.agentName}\"}"
     }
   }
   ```
6. **Self-Modification**: Agent Father calls EDDI's own API to create a new agent configuration

### Key Insight

Agent Father isn't special code—it's a **regular EDDI agent** that uses:

* Behavior rules to control conversation flow
* Property extraction to gather data
* HTTP Calls to invoke EDDI's REST API
* Output templates to guide the user

This demonstrates EDDI's power: **the same architecture that powers conversational agents can orchestrate complex, multi-step workflows**, even self-modifying the system itself.

**See the** [**Agent Father Deep Dive**](/advanced-concepts/agent-father-deep-dive) **for complete implementation details, code examples, and real-world applications.**

***

## Summary

EDDI's architecture is built on principles of **modularity**, **composability**, and **orchestration**. It's not a chatbot—it's the **infrastructure for building sophisticated conversational AI systems** that can:

* Orchestrate multiple APIs and LLMs
* Apply complex business logic through configurable rules
* Maintain stateful, context-aware conversations
* Scale horizontally in cloud environments
* Be assembled from reusable, version-controlled components

The **Lifecycle Pipeline** is the heart of this architecture, providing a flexible, pluggable system where agent behavior is configuration, not code.

***

## Configuration Model Deep Dive

EDDI's configuration model is a 4-level tree:

```mermaid
graph TD
    Agent["🤖 Agent (.agent.json)"] --> P1["📦 Workflow 1"]
    Agent --> P2["📦 Workflow 2"]
    Agent --> PN["📦 Workflow N"]
    P1 --> Parser1["🔤 Parser (dictionaries)"]
    P1 --> Behavior1["🧠 Behavior Rules"]
    P1 --> Property1["📝 Property Setter"]
    P1 --> Output1["💬 Output Templates"]
    P2 --> Parser2["🔤 Parser"]
    P2 --> Behavior2["🧠 Behavior Rules"]
    P2 --> HttpCalls2["🌐 HTTP Calls"]
    P2 --> Property2["📝 Property Setter"]
    P2 --> Output2["💬 Output Templates"]
```

### Agent → Workflows → Extensions

| Level          | Purpose                                                                                                                        |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Agent**      | List of workflow URIs + channels. The top-level container.                                                                     |
| **Workflow**   | Ordered list of workflow extensions — each extension = one lifecycle task type. **Order matters**: tasks execute sequentially. |
| **Extension**  | The actual configuration that drives each `ILifecycleTask`. Referenced by URI from the workflow.                               |
| **Descriptor** | Metadata (name, description, timestamps) for any resource. Not functional, purely for UI/management.                           |

### URI-Based References

Every resource references its dependencies by `eddi://` URI:

```
Agent → Workflow: "eddi://ai.labs.workflow/workflowstore/workflows/{id}?version=1"
Workflow → Rules: "eddi://ai.labs.rules/rulestore/rulesets/{id}?version=1"
Workflow → ApiCalls: "eddi://ai.labs.apicalls/apicallstore/apicalls/{id}?version=1"
Workflow → LLM: "eddi://ai.labs.llm/llmstore/llms/{id}?version=1"
```

### Extension Types & Their Pipeline Role

Each workflow runs its extensions in order: **Parser → Behavior → Property → HttpCalls → LLM → Output** (typical order).

| Extension Type       | Input                                     | Output                                               | Key Feature                                                      |
| -------------------- | ----------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------- |
| **Parser**           | Raw user text                             | Expressions (semantic representation)                | `expressionsAsActions: true` — parser expressions become actions |
| **Behavior Rules**   | Actions and expressions                   | New actions that drive subsequent tasks              | IF-THEN condition engine — the routing logic                     |
| **Property Setter**  | Current memory data                       | Stored properties (conversation-scoped or long-term) | Slot-filling using `{memory.current.input}` templates            |
| **HTTP Calls**       | Actions, template variables               | Response data stored in memory                       | Pre/post request property instructions, retry support            |
| **LLM**              | Conversation memory, system prompt, tools | LLM response text                                    | Legacy chat (simple) or Agent mode (tool-calling loop)           |
| **Output Templates** | Actions from current step                 | Text responses + quickReplies                        | Template variables, response variation via `valueAlternatives`   |

### Parser & Expression System

The parser uses a recursive expression model with Prolog heritage:

```
greeting                         → simple expression (no args)
greeting(hello)                  → expression with sub-expression
intent(weather, location(NYC))   → nested sub-expressions
*                                → wildcard (matches anything)
```

**QuickReply → Expression → Action flow**: When a user clicks a quickReply, the parser matches the text against the previous step's quickReply `value` fields, extracts the corresponding `expressions`, and (if `expressionsAsActions` is enabled) converts them to actions that drive behavior rules.

### Available NLP Extensions

| Type                 | Extensions                                                                                                                                                        |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Dictionaries** (7) | `RegularDictionary`, `IntegerDictionary`, `DecimalDictionary`, `EmailDictionary`, `TimeExpressionDictionary`, `OrdinalNumbersDictionary`, `PunctuationDictionary` |
| **Normalizers** (4)  | `ContractedWordNormalizer`, `ConvertSpecialCharacterNormalizer`, `PunctuationNormalizer`, `RemoveUndefinedCharacterNormalizer`                                    |
| **Corrections** (3)  | `DamerauLevenshteinCorrection`, `MergedTermsCorrection`, `PhoneticCorrection`                                                                                     |

***

## Database Architecture

EDDI's data layer is fully DB-agnostic via the `IResourceStorageFactory` SPI:

```
REST API → Store Interface (IResourceStore<T>)
         → HistorizedResourceStore<T> (versioning, history, soft-delete)
         → IResourceStorage<T> (SPI — Storage Provider Interface)
         ├── MongoResourceStorage<T> (MongoDB implementation)
         └── PostgresResourceStorage<T> (PostgreSQL + JSONB implementation)
```

Switching databases requires only a config change:

```properties
eddi.datastore.type=mongodb   # default
# eddi.datastore.type=postgres  # alternative
```

***

## Multi-Agent Orchestration

Beyond single-agent conversations, EDDI supports **group conversations** — structured multi-agent discussions where multiple agents collaborate on a question under the governance of a moderator agent.

A `GroupConversationService` orchestrates discussions through configurable phases. Each participating agent runs through its normal lifecycle pipeline — agents are group-unaware by design. The moderator serializes all contributions, preventing concurrent writes to shared state.

**Key capabilities:**

* **6 built-in discussion styles**: Round Table, Peer Review, Devil's Advocate, Delphi, Debate, and Task Force — each with distinct phase flows and turn-taking rules. Task Force uses a 4-phase pipeline (PLAN→EXECUTE→VERIFY→SYNTHESIS) for structured task decomposition and parallel execution
* **Custom phases**: Define your own phase sequences with configurable context scopes (independent, full transcript, anonymous, own-feedback-only)
* **Group-of-groups**: Members can themselves be groups, enabling hierarchical multi-agent composition with configurable depth limits
* **Fault tolerance**: Per-agent timeouts, configurable failure policies (skip, retry, abort), and graceful degradation when members are unavailable
* **Dynamic agents**: Agents can create, recruit, delegate to, and teardown new agents at runtime during discussions, with configurable guardrails (provider/model whitelists, per-discussion caps, lifecycle policies)

See [Group Conversations](/conversations-and-orchestration/group-conversations) for full configuration reference, and [A2A Protocol](/protocols-and-integration/a2a-protocol) for peer-to-peer agent communication.

***

## MCP Integration (Bilateral)

EDDI provides **bilateral** Model Context Protocol (MCP) integration — it is both an MCP Server and an MCP Client simultaneously.

**As MCP Server:** EDDI exposes its full API surface (conversations, administration, diagnostics, scheduling, group discussions) as MCP tools. This enables AI assistants (Claude Desktop, IDE plugins, custom MCP clients) to interact with deployed agents and manage the platform programmatically. Documentation is also exposed as MCP resources (`eddi://docs/{name}`).

**As MCP Client:** Individual agents can consume external MCP servers as tool providers. MCP server connections are configured as `mcpcalls` workflow extensions (versioned configuration resources, the MCP equivalent of `httpcalls`), support vault-based API key resolution, and are subject to the same rate limiting, caching, and cost tracking as built-in tools. The LLM auto-discovers them from the workflow (`enableMcpCallTools`, default `true`); behavior rules can also trigger specific MCP tools deterministically. Failed MCP connections degrade gracefully — they never kill the pipeline.

See [MCP Server](/protocols-and-integration/mcp-server) for the full tool reference and client configuration.

***

## Persistent User Memory

EDDI's memory model extends beyond single conversations. The `IUserMemoryStore` provides persistent key-value memory scoped per user, per agent, with visibility controls (`self`, `group`, `global`).

**How it integrates with the pipeline:**

* At **conversation init**, visible user memories are loaded as `longTerm` properties and made available in all templates via `{properties.key}`
* During the pipeline, the LLM can autonomously store and recall facts using built-in memory tools (when enabled)
* At **conversation teardown**, `longTerm` properties are persisted back to the user memory store
* **Background consolidation** (the "Dream" service) performs stale pruning, contradiction detection, and optional LLM-driven summarization. It runs on the same cluster-aware schedule machinery as every other background job — a `ScheduleConfiguration` whose `metadata` carries `{"dreamType": "dream_consolidation"}` is claimed by `SchedulePollerService` and dispatched by `ScheduleFireExecutor` to `DreamService`, which reads the agent's `userMemoryConfig.dream` block and runs one cycle. The target agent and user come from the schedule's **top-level** `agentId` / `agentVersion` / `userId` fields — `metadata` carries only the `dreamType` marker. Spend is bounded per cycle by `dream.maxCostPerRun` (US dollars), and because Dream has no parent LLM task to inherit credentials from, its model credentials come from `dream.parameters` (which resolves `${vault:…}` and `${vars:…}` like any LLM task's parameters). A cycle that cannot run — no `userId`, dream disabled on the agent, or a failing LLM call — is logged at ERROR and marked FAILED on the fire log, so it retries with backoff and dead-letters rather than silently doing nothing

**Creating a Dream schedule** — use the raw REST body, `POST /schedulestore/schedules`, with the cron expression from `dream.schedule`:

```json
{
  "name": "nightly dream — alice",
  "agentId": "5a8b1c2d3e4f5a6b7c8d9e0f",
  "agentVersion": 0,
  "triggerType": "CRON",
  "cronExpression": "0 3 * * *",
  "timeZone": "UTC",
  "userId": "alice",
  "message": "dream",
  "metadata": { "dreamType": "dream_consolidation" },
  "enabled": true
}
```

Three things this body does that are easy to get wrong:

* **The `create_schedule` MCP tool cannot do this.** Its arguments (`agentId`, `triggerType`, `cron`, `heartbeatIntervalSeconds`, `message`, `name`, `timeZone`, `conversationStrategy`, `userId`, `environment`) contain no `metadata`, so a schedule created that way has `metadata == null`, `DreamService.isDreamSchedule(…)` returns `false`, and the schedule fires an ordinary chat turn against the agent on the dream cron forever — logged COMPLETED, consolidating nothing. REST is the only route that produces a working Dream schedule today.
* **`message` is required even though Dream never reads it.** `RestScheduleStore.validateSchedule` rejects a CRON schedule without a non-blank `message`; the Dream fast-path bypasses `say()` entirely, so the value is inert — supply any placeholder.
* **`userId` must name the real user whose memories are consolidated.** Left unset it defaults to `system:scheduler`, which `DreamService` rejects (the cycle is marked FAILED rather than consolidating an empty memory set).

Memory visibility is enforced at the storage level — agents can only see memories matching their visibility scope, preventing cross-tenant memory leaks.

See [Persistent User Memory](/architecture-and-concepts/user-memory) for configuration, LLM tools, REST API, and the Dream consolidation service.

***

## Agent Sync & Portability

Agent configurations are fully portable — exportable, importable, and synchronizable between EDDI instances.

**The sync pipeline:**

```
IResourceSource (transport) → StructuralMatcher (analysis) → UpgradeExecutor (write)
```

1. **Transport abstraction**: `IResourceSource` abstracts the source — either a ZIP file (`ZipResourceSource`) or a live remote instance (`RemoteApiResourceSource`)
2. **Structural matching**: Resources are paired deterministically by position, type, and name — not by ID. This works even for independently-created agents
3. **Content sync**: The `UpgradeExecutor` updates target resources in-place, preserving IDs and URI references. Version numbers increment; no broken links
4. **Preview before apply**: All sync operations support a preview step showing exactly what will be created, updated, or skipped

Agent ZIP exports automatically scrub secrets before packaging to prevent credential leaks during transfer.

See [Agent Sync Architecture](/reference/agent-sync-architecture) for the matching algorithm and data flow, and [Agent Sync Guide](/conversations-and-orchestration/agent-sync-guide) for REST API usage.

***

## Security Architecture

EDDI enforces security at multiple layers so individual failures don't result in full compromise.

### SSRF Prevention (3-Layer Model)

Outbound HTTP from LLM tools is the primary attack surface. Three layers prevent Server-Side Request Forgery:

| Layer                            | Component                            | What It Blocks                                                                                                                                                                                                                  |
| -------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Layer 1: URL Validation**      | `UrlValidationUtils.validateUrl()`   | Private IPs (`10.x`, `172.16-31.x`, `192.168.x`), loopback (`127.x`, `::1`), link-local (`169.254.x`, `fe80::`), cloud metadata (`169.254.169.254`), non-HTTP schemes (`file://`, `ftp://`), hostnames resolving to private IPs |
| **Layer 2: Redirect Validation** | `SafeHttpClient.sendWithRedirects()` | Each redirect hop is validated against Layer 1 rules. `HttpClient.Redirect.NEVER` prevents the JDK from following redirects silently. Maximum 5 hops                                                                            |
| **Layer 3: Network Policy**      | Kubernetes `NetworkPolicy`           | Restricts egress at the cluster level (optional, operator-configured)                                                                                                                                                           |

**Usage pattern:**

```java
@Inject SafeHttpClient httpClient;

// For user-controlled URLs (LLM tools, web scraping):
httpClient.sendValidated(request, bodyHandler);  // validates initial + redirect targets

// For config-controlled URLs (known APIs):
httpClient.send(request, bodyHandler);            // validates redirect targets only
```

**DNS Rebinding:** EDDI validates hostnames at request time. A TOCTOU (time-of-check-time-of-use) gap exists between DNS validation and TCP connect. This is an accepted risk — exploitation requires a cooperating DNS server AND a successful race condition AND bypassing Layer 2 redirect validation. Defense-in-depth makes this impractical in practice.

### Vault Encryption Model

Secrets (API keys, credentials) never appear as plaintext in the database:

```
Master Key (env var EDDI_VAULT_MASTER_KEY)
  └→ PBKDF2-HMAC-SHA256 (600k iterations, per-deployment salt via VaultSaltManager)
       └→ Key Encryption Key (KEK)
            └→ AES-256-GCM encrypt/decrypt
                 └→ Data Encryption Key (DEK, per-secret)
                      └→ AES-256-GCM encrypt/decrypt
                           └→ Secret plaintext
```

* **Per-deployment salt**: `VaultSaltManager` generates and stores a unique 32-byte salt per EDDI instance
* **Envelope encryption**: Rotating the master key re-wraps KEK→DEK without touching individual secrets
* **Export scrubbing**: Agent export/sync automatically strips secrets from ZIP files

### Cryptographic Agent Identity

EDDI agents can sign their inter-agent messages using Ed25519 digital signatures. This protects multi-agent group conversations against identity spoofing, message tampering, and provides non-repudiation for audit trails.

**Key lifecycle:**

1. **Key generation**: `POST /agentstore/{id}/signing/keys` → `AgentSigningService.generateKeyPair()` creates an Ed25519 keypair. Public key stored in `AgentConfiguration.identity.publicKey`, private key encrypted in the Secrets Vault
2. **Key rotation**: `AgentPublicKey` records support versioned keys with `validFromMs`/`validUntilMs` windows. Old and new keys overlap during rotation. Private keys use versioned vault paths (`agent-signing-key:{agentId}:v{version}`)
3. **Signing**: When `security.signInterAgentMessages=true`, the `GroupConversationService` creates a `SignedEnvelope` for each agent response. The envelope contains the message payload, a UUID nonce, and an epoch timestamp. The canonical JSON form (RFC 8785 via `JacksonCanonicalizer`) is signed with Ed25519
4. **Self-verification**: Immediately after signing, the service verifies its own signature against the agent's public key. If self-verification fails, the signature is discarded (fail-safe to unsigned)
5. **Replay protection**: The `NonceCacheService` registers each nonce with freshness (5min default) and clock-skew (30s default) checks. Duplicate nonces are rejected
6. **Peer verification**: When `security.requirePeerVerification=true` on a receiving agent, the service reconstructs envelopes from stored `TranscriptEntry` fields and verifies each speaker's signature against their public key before sending context

**What is NOT covered:** MCP invocation signing is not yet implemented — the `signMcpInvocations` config field has been removed until the feature is built.

### Authentication Model

| Environment    | OIDC Enabled          | Behavior                                                |
| -------------- | --------------------- | ------------------------------------------------------- |
| **Dev mode**   | No                    | Allowed — info log on startup                           |
| **Dev mode**   | Yes                   | Full auth with configured Keycloak                      |
| **Production** | No + no opt-out       | `AuthStartupGuard` **fails startup** with clear error   |
| **Production** | No + explicit opt-out | Starts, but logs ERROR every 60s as a constant reminder |
| **Production** | Yes                   | Full OIDC with Keycloak multi-tenant support            |

The escape hatch (`EDDI_SECURITY_ALLOW_UNAUTHENTICATED=true`) exists for air-gapped deployments and quick demos. The periodic ERROR log ensures operators remain aware.

### CI Security Scanning

| Tool                  | Trigger                      | What It Checks                                                           |
| --------------------- | ---------------------------- | ------------------------------------------------------------------------ |
| **CodeQL**            | Every PR + weekly schedule   | Java semantic analysis (injection, SSRF, crypto misuse)                  |
| **Trivy**             | Docker image build           | CVEs in OS packages and Java dependencies                                |
| **Dependency Review** | Every PR                     | License compliance and known vulnerabilities in new dependencies         |
| **Jackson 3 Ban**     | Every build (Maven Enforcer) | Prevents accidental Jackson 3.x introduction (incompatible with Quarkus) |

### Security Headers

Production response headers (configured via `application.properties`):

* `X-Content-Type-Options: nosniff`
* `X-Frame-Options: DENY`
* `Content-Security-Policy: default-src 'self'; ...`
* `Strict-Transport-Security: max-age=31536000` (when TLS is configured)

## Related Documentation

* [Getting Started](/getting-started/getting-started) - Setup and installation
* [Conversation Memory & State Management](/architecture-and-concepts/conversation-memory) - Deep dive into conversation state
* [Agent Father: A Deep Dive](/advanced-concepts/agent-father-deep-dive) - Complete walkthrough of a real-world example
* [Behavior Rules](/agent-configuration/behavior-rules) - Configure decision logic
* [HTTP Calls](/agent-configuration/httpcalls) - External API integration
* [LLM Integration](/agent-configuration/langchain) - Connect to LLM APIs
* [Extensions](/architecture-and-concepts/extensions) - Available agent components
* [Security](/security-and-compliance/security) - Authentication, authorization, and tool security
* [Secrets Vault](/security-and-compliance/secrets-vault) - Encrypted secret management
* [Audit Ledger](/security-and-compliance/audit-ledger) - EU AI Act compliance
* [MCP Server](/protocols-and-integration/mcp-server) - Model Context Protocol integration
* [Group Conversations](/conversations-and-orchestration/group-conversations) - Multi-agent structured discussions
* [Persistent User Memory](/architecture-and-concepts/user-memory) - Cross-session memory and Dream consolidation
* [Agent Sync](/reference/agent-sync-architecture) - Import, export, and live instance sync
* [Memory Policy](/architecture-and-concepts/memory-policy) - Commit flags and strict write discipline
* [Prompt Snippets](/agent-configuration/prompt-snippets-guide) - Reusable system prompt building blocks
* [Model Cascade](/agent-configuration/model-cascade) - Multi-model sequential escalation
* [Scheduling](/conversations-and-orchestration/scheduling) - Cron and heartbeat agent triggers
* [A2A Protocol](/protocols-and-integration/a2a-protocol) - Agent-to-Agent peer communication
* [GDPR Compliance](/security-and-compliance/gdpr-compliance) - Data subject rights and retention
* [HIPAA Compliance](/security-and-compliance/hipaa-compliance) - Healthcare deployment guide
* [EU AI Act Compliance](/security-and-compliance/eu-ai-act-compliance) - AI decision audit requirements


# Project Philosophy

> **The Overarching Directive for All Development in EDDI**
>
> This document defines the foundational principles that govern every architectural decision, feature implementation, and design trade-off across the EDDI ecosystem. Every contributor — human or AI — must internalize these principles before writing code.
>
> This is not a technical specification. Implementation details belong in [`architecture.md`](/architecture-and-concepts/architecture) and [`AGENTS.md`](https://github.com/labsai/EDDI/blob/main/AGENTS.md). This document answers **why** — those documents answer **how**.

***

## Identity Statement

**EDDI is the "Grown-Up" Enterprise AI Orchestrator.**

While competitors were built for rapid prototyping and are now frantically reverse-engineering enterprise qualities into architectures that resist them, EDDI approaches from the opposite direction: **a deterministic engine built to safely govern non-deterministic AI — from single agents to multi-agent orchestration.**

EDDI's competitive moat is structural, not feature-based. It emerges from the combination of:

* **JVM-native concurrency** — true parallelism without language-level barriers
* **Configuration-driven logic** — agent behavior is data, not compiled code
* **Strict pipeline architecture** — deterministic execution of probabilistic components
* **Multi-agent orchestration** — coordinated reasoning across agent groups
* **Security & compliance by default** — baked into the architecture, not bolted on

***

## The Nine Pillars

### Pillar 1: Configuration Is Logic, Java Is the Engine

> *"Agent behavior belongs in configuration. Code builds the components that read and execute those configurations."*

EDDI is a **config-driven engine**, not a monolithic application. The intelligence of an agent — its routing rules, API calls, LLM prompts, output templates — is defined in versioned JSON documents. Code provides the **infrastructure components** that interpret and execute those configurations at runtime.

When designing a new feature, always ask: *"Should this be configurable by the agent designer?"* If yes, expose it as a config field with sensible defaults — don't hardcode behavior.

**Why this matters:** Competitors that embed logic in code suffer from deployment friction (every change requires recompilation), security vulnerabilities (dynamic code execution), and operational opacity (logic can't be versioned or rolled back independently).

**The escape hatch:** When an agent designer genuinely needs custom code, EDDI provides bilateral protocol integration — it can both expose its capabilities to and consume capabilities from external services. Custom logic runs in isolated containers outside the EDDI perimeter.

***

### Pillar 2: Deterministic Governance of Non-Deterministic AI

> *"The engine is strict so the AI can be creative."*

LLMs are inherently probabilistic — they hallucinate, loop infinitely, and burn through token budgets unpredictably. EDDI's role is to provide **deterministic guardrails** around this non-determinism: budget controls, error containment, governance controls, and immutable audit trails.

**Concrete expectations:**

* Every tool call must pass through rate limiting, caching, and cost tracking — no unmetered execution paths
* Framework errors must never leak into LLM conversation history — map failures to structured signals in the pipeline
* Multi-agent interactions must be serialized through a governance mechanism — no concurrent, uncoordinated writes to shared state
* External tool integrations must be classified by risk level, with state-changing operations requiring explicit approval

***

### Pillar 3: The Engine, Not the Application

> *"EDDI provides the components. The admin configures the intelligence."*

EDDI is middleware — it sits between user-facing channels and AI providers. It does not contain business logic; it contains the **machinery** to execute business logic defined as configuration. Adding a new capability means adding a new component type, not modifying existing ones.

**Why this matters:** This architecture enables multi-tenancy (same engine, different agents), instant iteration (edit config, not code), and clean extensibility (auto-discovery, no registration).

**Anti-patterns:**

* Building custom schedulers or background job infrastructure — reuse the existing scheduling framework
* Creating new pipeline components for session-level concerns — extend the session lifecycle instead
* Calling components directly from other components — use event-based orchestration for all inter-component communication

***

### Pillar 4: Security & Compliance as Architecture, Not Afterthought

> *"If a security measure can be bypassed by changing a configuration, it is not a security measure."*

Security and regulatory compliance are enforced at the **architectural level**: the type system, the classpath, the network topology, and startup checks. They are never delegated to the LLM, the prompt, or the admin's good judgment.

**Security principles:**

* No dynamic code execution — ever. Expression evaluation uses safe, sandboxed parsers
* No plaintext secrets in storage — all credentials use vault references resolved at runtime
* No trusting LLM output for access control — tenant isolation is enforced by code, not by prompt
* External URLs are validated against SSRF — private IPs and internal hostnames are blocked
* Agent exports are sanitized — secrets are scrubbed before packaging

**Compliance principles:**

* Data subject rights (erasure, portability, restriction) must be enforceable through the architecture, not just documented
* AI decision audit trails must be immutable and write-once — they are evidence, not logs
* Compliance requirements must be enforced at startup — if a required capability is missing, the system should fail fast rather than run without it

***

### Pillar 5: Enterprise-Grade Concurrency

> *"The JVM's concurrency model is our unfair structural advantage."*

Language-level concurrency barriers (Python's GIL, Node.js's single-threaded event loop) are fundamental obstacles to scaling multi-agent AI workloads. EDDI leverages the JVM's thread model to achieve true parallelism without blocking, without heartbeat starvation, and without the serialization panics that plague competitors.

**Principles:**

* Pipeline components are **stateless singletons** — all conversational state lives in a dedicated memory object
* No raw infrastructure objects (DB connections, HTTP clients) in conversational state — transient resources are scoped appropriately
* No unbounded in-memory collections — all caches have strict size limits and TTLs
* Messaging infrastructure threads must never be blocked by application logic

***

### Pillar 6: Transparent Observability

> *"If you can't see why the AI made a decision, you can't fix it, audit it, or trust it."*

Every conversation turn must produce a **complete, immutable trace** of exactly what happened: the compiled prompt, the retrieved context, the tool calls, the memory state, and the cost. This is not debugging infrastructure — it is the product.

Observability serves two masters: **developers** who need to understand why an agent behaved a certain way, and **regulators** who need evidence of what the AI decided and why. Both must be served by the same infrastructure — not by separate logging systems.

**Vision:** A "Time-Traveling IDE" experience — step-through replay of any conversation turn with exact compiled prompts, memory state snapshots, and pause/edit/resume controls.

***

### Pillar 7: Progressive Disclosure in UX

> *"Easy things should be easy. Hard things should be possible."*

The management UI must serve two audiences simultaneously: **business users** who want visual forms and guided wizards, and **power users** who want raw configuration editing with validation and autocomplete. Both views are **identical state representations** — editing one updates the other.

**Principles:**

* No spaghetti node graphs — use structured layouts with wires only for macro-routing
* No modals — use side-sheet inspectors so the main view remains visible
* Trusted vs. untrusted data must be visually distinguishable
* Surface actionable metrics (resolution rate, cost per agent), not vanity metrics

***

### Pillar 8: Persistent Memory & Cross-Session Intelligence

> *"An agent that forgets everything between sessions is not an intelligent agent."*

Conversational intelligence requires memory that outlives individual sessions. EDDI provides a **layered memory architecture** where short-term pipeline data, medium-term conversation properties, and long-term persistent memories each serve distinct purposes — and the boundaries between them are explicit, configurable, and secure.

**Principles:**

* Memory has two audiences: pipeline components see everything; the LLM sees only a windowed, curated view. Context management strategies must respect this distinction
* Persistent state is a **session concern** — it is loaded at session start and saved at session end, not managed by pipeline components
* Memory visibility is enforced at the storage level, not by prompt filtering — an agent cannot leak cross-tenant memories through prompt tricks
* Background memory operations (consolidation, summarization) use the platform's scheduling infrastructure, not custom jobs
* Failed pipeline data must be containable — error output should not pollute future LLM context

***

### Pillar 9: Agent Portability & Sync

> *"An agent locked to one instance is an agent locked to one vendor."*

Agent configurations must be fully portable — exportable, importable, diffable, and synchronizable between instances without loss of fidelity. This is the foundation of multi-environment workflows (dev → staging → production) and prevents vendor lock-in.

**Principles:**

* Sync operations are always pull-based from the target's perspective — the source is never modified
* Content-identical resources are detected and skipped automatically — no unnecessary version churn
* Secrets are never included in exports — they are scrubbed at the export boundary
* Sync must support preview-before-apply — operators must see exactly what will change before committing
* Partial failures in batch operations don't roll back successful ones — each resource syncs independently

***

## Strategic Positioning

EDDI occupies a unique position as the **only JVM-native, config-driven AI orchestration platform** in a market dominated by Python/Node.js solutions. While competitors offer either visual orchestration (without enterprise qualities) or enterprise frameworks (without visual configuration), EDDI provides both.

**Three strategic pitches:**

1. **Escape the Prototype Trap** — transition fragile prototypes to robust JVM production with true multi-agent orchestration
2. **Agility Through Configuration** — update AI logic in seconds without recompilation, sync changes across environments instantly
3. **Cloud-Native Scale** — JVM virtual threads deliver minimal footprint and maximum throughput without the concurrency compromises of competing language runtimes

***

## Document Governance

This document is the **supreme directive** for EDDI development. When a technical decision conflicts with these principles, the principles win. When a new feature doesn't fit within these pillars, either the feature must be redesigned or a new pillar must be proposed and approved.

**Versioning:** This document evolves with the project. Changes require explicit approval from project leadership and must be documented in the changelog.


# Conversation Memory & State Management

**Version: 6.2.0**

## Overview

**Conversation Memory** (`IConversationMemory`) is the heart of EDDI's stateful architecture. It's a Java object that represents the complete state of a conversation, including history, user data, context, and intermediate processing results. This object is passed through the entire Lifecycle Pipeline, with each task reading from and writing to it.

## What is Conversation Memory?

Think of Conversation Memory as a **living document** that captures everything about a conversation:

* **Who**: User ID and agent ID
* **What**: All messages exchanged (both user inputs and agent outputs)
* **When**: Timestamp of each interaction
* **Context**: Data passed from external systems (user profile, session info, etc.)
* **State**: Current processing stage (READY, IN\_PROGRESS, ENDED, etc.)
* **Properties**: Extracted and stored data (user preferences, entities, variables)
* **History**: Complete record of all previous conversation steps

## Key Concepts

### 1. Conversation Steps

A conversation is divided into **steps**, where each step represents one complete interaction cycle:

```
Step 1: User says "Hello" → Agent responds "Hi, how can I help?"
Step 2: User says "What's the weather?" → Agent responds "The weather is sunny, 75°F"
Step 3: ...
```

Each step contains:

* **Input**: What the user said
* **Actions**: Actions triggered by behavior rules
* **Data**: Results from lifecycle tasks (parsed expressions, API responses, LLM outputs)
* **Output**: Agent's response

### 2. Current Step vs Previous Steps

```java
IWritableConversationStep getCurrentStep();  // The step being processed right now
IConversationStepStack getPreviousSteps();    // All completed steps (history)
```

* **Current Step**: Writable, being built during lifecycle execution
* **Previous Steps**: Read-only, provides conversation history

### 3. Memory Scopes

EDDI supports different scopes for storing data:

| Scope          | Lifetime             | Use Case                                                     |
| -------------- | -------------------- | ------------------------------------------------------------ |
| `step`         | Single interaction   | Temporary data needed only for this response                 |
| `conversation` | Entire conversation  | User preferences, extracted entities (persists across steps) |
| `longTerm`     | Across conversations | User profile data that should persist between sessions       |

### 4. Undo/Redo Support

Conversation Memory supports undo/redo operations:

```java
void undoLastStep();       // Go back to previous step
boolean isUndoAvailable(); // Check if undo is possible
void redoLastStep();       // Re-apply undone step
boolean isRedoAvailable(); // Check if redo is possible
```

This enables scenarios like:

* User makes a mistake and wants to go back
* Testing different conversation paths
* Debugging agent behavior

## Conversation Memory Structure

### Core Properties

```java
public interface IConversationMemory {
    // Identity
    String getConversationId();
    String getAgentId();
    Integer getAgentVersion();
    String getUserId();

    // State
    ConversationState getConversationState();
    void setConversationState(ConversationState state);

    // Steps
    IWritableConversationStep getCurrentStep();
    IConversationStepStack getPreviousSteps();
    IConversationStepStack getAllSteps();
    int size();  // Total number of steps

    // Properties
    IConversationProperties getConversationProperties();

    // Output
    List<ConversationOutput> getConversationOutputs();

    // History management
    void undoLastStep();
    void redoLastStep();
    Stack<IConversationStep> getRedoCache();
}
```

### Conversation States

```java
public enum ConversationState {
    READY,           // Agent is ready to process next input
    IN_PROGRESS,     // Currently processing a message
    EXECUTION_INTERRUPTED,  // Processing was interrupted
    ERROR,           // An error occurred
    ENDED            // Conversation has ended
}
```

## How Lifecycle Tasks Use Memory

Each lifecycle task follows this pattern:

```java
@Override
public void execute(IConversationMemory memory, Object component) {
    // 1. Read from memory
    String userInput = memory.getCurrentStep().getLatestData("input").getResult();

    // 2. Perform task logic
    String processed = process(userInput);

    // 3. Write results back to memory
    IData<String> data = dataFactory.createData("output", processed);
    memory.getCurrentStep().storeData(data);
}
```

### Example: Behavior Rules Task

```java
// Reads conversation memory to check conditions
IData<List<String>> expressionsData =
    memory.getCurrentStep().getLatestData("expressions");

// If conditions match, stores actions in memory
memory.getCurrentStep().storeData(
    dataFactory.createData("actions", List.of("welcome_action"))
);
```

### Example: LangChain Task

```java
// Reads conversation history
List<IConversationStep> history = memory.getPreviousSteps().getAllSteps();

// Calls LLM with history
String llmResponse = langChainService.chat(history, currentInput);

// Stores LLM response in memory
memory.getCurrentStep().storeData(
    dataFactory.createData("llmResponse", llmResponse)
);
```

### Example: HTTP Calls Task

```java
// Reads context from memory for request
String userId = memory.getConversationProperties()
    .get("context.userId");

// Makes API call
JsonObject response = httpClient.get("/users/" + userId);

// Stores response for use in output templates
memory.getCurrentStep().storeData(
    dataFactory.createData("userProfile", response)
);
```

## Accessing Memory in Configurations

### In Output Templates (Qute)

```html
<!-- Access current input -->
You said: {memory.current.input}

<!-- Access previous step data -->
Previously, you mentioned: {memory.previous.userPreference}

<!-- Access context data -->
Welcome, {memory.current.context.userName}!

<!-- Access HTTP call response -->
The weather is: {memory.current.httpCalls.weatherResponse.temperature}

<!-- Access LLM response -->
AI says: {memory.current.llmResponse}
```

### In HTTP Call Body Templates

```json
{
  "userId": "{memory.current.context.userId}",
  "message": "{memory.current.input}",
  "conversationId": "{memory.conversationId}"
}
```

### In Behavior Rule Conditions

```json
{
  "type": "contextmatcher",
  "configs": {
    "contextKey": "userName",
    "contextType": "string"
  }
}
```

## Memory Persistence

### Storage Mechanism

1. **During Processing**: Memory resides in Java heap (fast access)
2. **After Each Step**: Memory is serialized and saved to MongoDB
3. **On Next Request**: Memory is loaded from MongoDB and cached

### Caching Strategy

```
Request → Check Cache → If Miss: Load from MongoDB → Execute Lifecycle → Save to MongoDB + Update Cache
```

EDDI uses **Caffeine** for in-process caching:

* Fast retrieval of frequently accessed conversations
* Reduced MongoDB load
* Size-based eviction with configurable maximum entries

### MongoDB Structure

```javascript
{
  "_id": "conversationId",
  "agentId": "agent-123",
  "agentVersion": 1,
  "userId": "user-456",
  "conversationState": "READY",
  "conversationSteps": [
    {
      "timestamp": 1699824000000,
      "data": [
        {"key": "input", "value": "Hello"},
        {"key": "expressions", "value": ["greeting(hello)"]},
        {"key": "actions", "value": ["welcome_action"]},
        {"key": "output", "value": ["Hi! How can I help you?"]}
      ]
    },
    // ... more steps
  ],
  "conversationProperties": {
    "userName": "John",
    "userPreference": "concise"
  },
  "redoCache": []
}
```

## Best Practices

### 1. Use Appropriate Scopes

```java
// ❌ Don't store temporary data in conversation scope
propertyInstruction.setScope("conversation");  // This persists!

// ✅ Use step scope for temporary data
propertyInstruction.setScope("step");  // Cleaned after this step
```

### 2. Clean Up Large Data

If you store large API responses, consider cleaning them after use:

```json
{
  "postResponse": {
    "propertyInstructions": [
      {
        "name": "temperature",
        "fromObjectPath": "weatherResponse.current.temperature",
        "scope": "conversation"
      }
    ]
  }
}
```

Extract only what you need instead of storing the entire response.

### 3. Leverage History for Context

When calling LLMs, you can control how much history is sent:

```json
{
  "parameters": {
    "sendConversation": "true",
    "includeFirstAgentMessage": "true",
    "logSizeLimit": "10" // Only last 10 messages
  }
}
```

### 4. Use Context for External Data

Pass data from your application via context instead of hardcoding:

```javascript
// API Request
POST /agents/prod/myagent/conversation123
{
  "input": "What's my order status?",
  "context": {
    "userId": "user-789",
    "sessionId": "session-xyz"
  }
}
```

Then access in agent logic:

```
{context.userId}
```

## Memory Flow Example

Let's trace how memory flows through a complete conversation step:

### 1. User Request

```json
POST /agents/prod/weatheragent/conv-123
{
  "input": "What's the weather in Paris?",
  "context": {
    "userId": "john-doe"
  }
}
```

### 2. Memory Initialization

```java
IConversationMemory memory = loadOrCreateMemory("conv-123");
memory.getCurrentStep().storeData(
    dataFactory.createData("input", "What's the weather in Paris?")
);
memory.getConversationProperties().put(
    "context.userId", "john-doe"
);
```

### 3. Parser Task Execution

```java
// Reads input
String input = memory.getCurrentStep().getLatestData("input").getResult();

// Parses input
List<String> expressions = parse(input);
// Result: ["question(what)", "entity(weather)", "location(paris)"]

// Stores in memory
memory.getCurrentStep().storeData(
    dataFactory.createData("expressions", expressions)
);
```

### 4. Behavior Rules Execution

```java
// Reads expressions
List<String> expressions = memory.getCurrentStep()
    .getLatestData("expressions").getResult();

// Evaluates: if expressions contains "entity(weather)" → trigger "fetch_weather"
if (matchesRule(expressions, "entity(weather)")) {
    memory.getCurrentStep().storeData(
        dataFactory.createData("actions", List.of("fetch_weather"))
    );
}
```

### 5. HTTP Call Execution

```java
// Reads action
List<String> actions = memory.getCurrentStep()
    .getLatestData("actions").getResult();

if (actions.contains("fetch_weather")) {
    // Extract location from expressions
    String location = extractLocation(expressions);  // "paris"

    // Make API call
    JsonObject weather = weatherApi.get(location);

    // Store response
    memory.getCurrentStep().storeData(
        dataFactory.createData("weatherData", weather)
    );
}
```

### 6. Output Generation

```java
// Reads weather data
JsonObject weather = memory.getCurrentStep()
    .getLatestData("weatherData").getResult();

// Applies template
String output = applyTemplate(
    "The weather in {weatherData.location} is {weatherData.description}",
    memory
);
// Result: "The weather in Paris is sunny with 22°C"

// Stores output
memory.getCurrentStep().storeData(
    dataFactory.createData("output", List.of(output))
);
```

### 7. Memory Persistence

```java
// Save to MongoDB
conversationMemoryStore.save(memory);

// Update cache
cache.put("conv-123", memory.getConversationState());
```

### 8. Response to User

```json
{
  "conversationState": "READY",
  "conversationOutputs": [
    {
      "output": ["The weather in Paris is sunny with 22°C"],
      "actions": ["fetch_weather"]
    }
  ]
}
```

## Advanced Topics

### Accessing Nested Data

```java
// In Java
IData<JsonObject> httpData = memory.getCurrentStep()
    .getLatestData("httpCalls.userProfile");
String userName = httpData.getResult().getString("name");

// In Qute
{memory.current.httpCalls.userProfile.name}
```

### Iterating Over History

```java
IConversationStepStack previousSteps = memory.getPreviousSteps();
for (IConversationStep step : previousSteps) {
    IData<String> inputData = step.getLatestData("input");
    if (inputData != null) {
        String pastInput = inputData.getResult();
        // Process historical input
    }
}
```

### Conditional Memory Access

```
{#if memory.current.weatherData}
  Temperature: {memory.current.weatherData.temperature}
{#else}
  N/A
{/if}
```

***

## Template Variable Reference

When tasks process templates (system prompts, HTTP call bodies, property instructions, output templates), `MemoryItemConverter.convert(memory)` produces a map with these top-level keys:

| Key                | Type                                         | Source                                                                                                                  | Example Access                                   |
| ------------------ | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `context`          | `Map<String, Object>`                        | Input context variables set per turn                                                                                    | `{context.language}`                             |
| `properties`       | `Map<String, Object>` (**raw values**)       | **All conversation properties** — includes both session-scoped and `longTerm` properties loaded from persistent storage | `{properties.preferred_language}`                |
| `memory`           | `Map` with `current`, `last`, `past`         | Conversation step data from the pipeline                                                                                | `{memory.current.output}`, `{memory.last.input}` |
| `snippets`         | `Map<String, Object>`                        | Prompt Snippets — auto-injected from `PromptSnippetService`                                                             | `{snippets.cautious_mode}`                       |
| `vars`             | `Map<String, Object>`                        | Global Variables — deployment-wide config from `GlobalVariableResolver`                                                 | `{vars.default-model}`                           |
| `userInfo`         | `Map` with `userId`                          | Authenticated user identity                                                                                             | `{userInfo.userId}`                              |
| `conversationInfo` | `Map` with `conversationId`, `agentId`, etc. | Conversation metadata                                                                                                   | `{conversationInfo.agentId}`                     |
| `conversationLog`  | `String`                                     | Formatted conversation history                                                                                          | `{conversationLog}`                              |

> **Key insight**: `longTerm` properties are loaded into `conversationProperties` at conversation init and are immediately available via `{properties.key}` in any template. You do NOT need a separate template namespace for persistent data — properties IS the namespace.

> ⚠️ **`properties` holds raw values, not `Property` objects.** `MemoryItemConverter.convert()` inserts `ConversationProperties.toMap()`, and `toMap()` returns the unwrapped Java value (`String`, `Integer`, `Boolean`, `List`, `Map`) that was stored — the `Property` wrapper is gone by the time a template sees it. Write `{properties.preferred_language}`; `{properties.preferred_language.valueString}` resolves against a `String` and fails at render time. See AGENTS.md §5.1 for the authoritative template data model.

### When to Use Which

| Need                        | Use                          | Why                                            |
| --------------------------- | ---------------------------- | ---------------------------------------------- |
| Data from your application  | `{context.X}`                | Per-request, set by caller                     |
| Persistent user preferences | `{properties.X}`             | Survives across conversations (scope=longTerm) |
| Current turn's input/output | `{memory.current.X}`         | Step-level data from the pipeline              |
| Previous turn's data        | `{memory.last.X}`            | One step back                                  |
| Who the user is             | `{userInfo.userId}`          | Authenticated identity                         |
| Which agent/conversation    | `{conversationInfo.agentId}` | Conversation metadata                          |
| Full conversation history   | `{conversationLog}`          | Formatted string of all turns                  |

***

## Conversation Lifecycle: Init and Teardown

Understanding what happens at conversation boundaries is critical for features that manage persistent state.

### Initialization (`Conversation.init()`)

When a conversation starts or continues:

```
Conversation.init()
  ├─→ Load conversation memory from store
  ├─→ loadLongTermProperties()
  │     └─→ IPropertiesHandler.loadProperties(userId)
  │     └─→ Properties loaded into conversationProperties with scope=longTerm
  │     └─→ Available as {properties.key} in all templates
  └─→ Set conversation state to IN_PROGRESS
```

### Pipeline Execution

The `LifecycleManager` runs all configured tasks in sequence:

```
LifecycleManager.executeLifecycle(memory)
  ├─→ Input Parser
  ├─→ Behavior Rules → emit actions
  ├─→ PropertySetterTask → set properties based on actions
  ├─→ ApiCallsTask → execute API calls based on actions
  ├─→ LlmTask → call LLM based on actions
  └─→ OutputGenerationTask → format response
```

### Teardown (`postConversationLifecycleTasks()`)

After the pipeline completes:

```
Conversation.postConversationLifecycleTasks()
  ├─→ storePropertiesPermanently()
  │     ├─→ All longTerm properties saved via IPropertiesHandler
  │     └─→ Secret properties scrubbed and vaulted via SecretsVault
  ├─→ Save conversation memory to store
  └─→ Set conversation state to READY
```

> **Key insight**: Persistent state is a **session concern** handled in `Conversation.java` init/teardown — NOT a pipeline task. If a feature needs to load/save cross-conversation state, it extends the Conversation init/teardown logic. The pipeline processes data for a single turn; session boundaries manage what persists between turns.

## Related Documentation

* [Architecture Overview](/architecture-and-concepts/architecture) - Understanding the big picture
* [Properties](/architecture-and-concepts/properties) - Property system, scopes, and persistence
* [Behavior Rules](/agent-configuration/behavior-rules) - Using memory in conditions
* [Output Templating](/agent-configuration/output-templating) - Accessing memory in outputs
* [HTTP Calls](/agent-configuration/httpcalls) - Storing API responses in memory
* [LLM Integration](/agent-configuration/langchain) - Using conversation history with LLMs
* [Passing Context Information](/agent-configuration/passing-context-information) - Injecting external data


# Memory Policy (Commit Flags)

## Overview

Memory Policy controls what happens when a lifecycle task fails during a conversation turn. By default, failed task output (stack traces, HTTP error bodies, raw error messages) is written to conversation memory and becomes visible to the LLM on subsequent turns. This pollutes the LLM's context with noise it can't act on.

**Strict Write Discipline** solves this by marking failed task output as **uncommitted** — excluded from the LLM's view — and injecting a concise **error digest** that the LLM can understand and react to.

## Configuration

Memory Policy is configured at the agent level in the agent configuration JSON:

```json
{
  "agentConfiguration": {
    "memoryPolicy": {
      "strictWriteDiscipline": {
        "enabled": true,
        "onFailure": "digest"
      }
    }
  }
}
```

### Options

| Field       | Type    | Default      | Description                        |
| ----------- | ------- | ------------ | ---------------------------------- |
| `enabled`   | boolean | `false`      | Enable strict write discipline     |
| `onFailure` | string  | `"keep_all"` | What to do with failed task output |

### Failure Modes

| Mode          | Behavior                                                                                                                                                    |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `digest`      | Failed task output is marked uncommitted (hidden from LLM). A concise error digest is injected so the LLM knows what failed and can adapt. **Recommended.** |
| `exclude_all` | Failed task output is marked uncommitted. No error digest is injected. The LLM sees nothing about the failure.                                              |
| `keep_all`    | Default behavior — failed task output remains committed and visible to the LLM. Backwards-compatible.                                                       |

## How It Works

### Without Strict Write Discipline (Default)

```
Turn 1: User asks "What's the weather?"
  → WeatherTool fails with HTTP 503
  → Raw error: "java.net.ConnectException: Connection refused..."
  → Error is stored in memory
  → LLM sees full stack trace on next turn
  → LLM may hallucinate about server errors or try to "fix" the code
```

### With Strict Write Discipline (`digest` mode)

```
Turn 1: User asks "What's the weather?"
  → WeatherTool fails with HTTP 503
  → Raw error is marked as UNCOMMITTED (hidden from LLM)
  → Error digest injected: {"type": "errorDigest", "taskId": "weather", "text": "Weather lookup failed"}
  → Action emitted: "task_failed_weather"
  → LLM sees concise digest on next turn
  → LLM can respond: "I'm sorry, I couldn't check the weather right now."
  → Behavior rules can react to "task_failed_weather" action
```

## Commit Flags

Every piece of data in conversation memory (`IData<T>`) carries a **committed** flag:

| Flag                         | Meaning                                                      |
| ---------------------------- | ------------------------------------------------------------ |
| `committed = true` (default) | Data is included in the LLM's context window                 |
| `committed = false`          | Data is stored in memory but excluded from the LLM's context |

When strict write discipline is enabled and a task fails:

1. All data written by the failed task during that turn is marked `committed = false`
2. The conversation output added by the failed task is rolled back
3. An error digest replaces the raw output
4. A `task_failed_<taskId>` action is emitted for behavior rule routing

## Error Digest Format

The error digest is stored as a special output type:

```json
{
  "type": "errorDigest",
  "taskId": "ai.labs.apicalls",
  "text": "API call to payment-service failed: HTTP 500"
}
```

The UI can render error digests with distinct styling (warning icon, collapsible panel). The LLM receives the concise `text` summary rather than raw error noise.

## Behavior Rule Integration

When a task fails with strict write discipline enabled, the action `task_failed_<taskId>` is emitted. You can use this in behavior rules to route to fallback logic:

```json
{
  "behaviorRules": [
    {
      "name": "Handle Weather Failure",
      "actions": ["fallback_response"],
      "conditions": [
        {
          "type": "actionMatcher",
          "values": {
            "actions": "task_failed_ai.labs.apicalls"
          }
        }
      ]
    }
  ]
}
```

## Best Practices

1. **Enable `digest` mode for production agents** — It prevents LLM context pollution while preserving observability
2. **Use behavior rules for graceful degradation** — React to `task_failed_*` actions to provide fallback responses
3. **Monitor error digests** — They appear in conversation memory for debugging even though the LLM only sees the summary
4. **Leave `keep_all` for development** — Full error output is useful during agent development and debugging

## See Also

* [Architecture](/architecture-and-concepts/architecture) — Lifecycle pipeline and conversation memory model
* [Conversation Memory](/architecture-and-concepts/conversation-memory) — How data flows through the pipeline
* [Behavior Rules](/agent-configuration/behavior-rules) — Routing based on actions and conditions


# Properties

**Version: 6.2.0**

## Overview

**Properties** are EDDI's primary mechanism for storing and retrieving state within and across conversations. They are key-value pairs that can be set by agent configuration, extracted from API responses, or written by LLM tools — and they're accessible in every template (system prompts, HTTP call bodies, output templates, property instructions).

Properties are the glue that connects the pipeline's processing steps with persistent user state. Understanding how they work is essential for building stateful agents.

## Key Concepts

### What Properties Are

A property has:

* **Name** (key): The identifier used to access the property (e.g., `preferred_language`, `company_name`)
* **Value**: Can be a `String`, `Integer`, `Float`, `Map`, `List`, or `Boolean`
* **Scope**: How long the property lives
* **Visibility** (v6): Who can see the property

### Property vs Context vs Memory

| Mechanism      | Source                                    | Lifetime                       | Access Pattern         |
| -------------- | ----------------------------------------- | ------------------------------ | ---------------------- |
| **Properties** | Agent-set (via PropertySetter, LLM tools) | Configurable (step → longTerm) | `{properties.key}`     |
| **Context**    | Your application (passed per request)     | Per request                    | `{context.key}`        |
| **Memory**     | Pipeline (each task writes data)          | Per step (current turn's data) | `{memory.current.key}` |

Use **properties** when the agent needs to remember something. Use **context** when your application injects something. Use **memory** when you need data from the current or previous pipeline step.

***

## Scopes

Properties support four scopes that control their lifetime:

| Scope          | Lifetime                         | Persistence                      | Use Case                                                            |
| -------------- | -------------------------------- | -------------------------------- | ------------------------------------------------------------------- |
| `step`         | Current conversation turn only   | Not persisted                    | Temporary data needed only for this response                        |
| `conversation` | Entire conversation session      | Persisted in conversation memory | User preferences within a session, extracted entities               |
| `longTerm`     | Across conversations             | Persisted in user property store | User profile data, preferences that should survive between sessions |
| `secret`       | Across conversations (encrypted) | Persisted via SecretsVault       | API keys, tokens, sensitive credentials                             |

### Choosing the Right Scope

```
Is this data only needed for the current response?
  → step

Will the user need this data later in the same conversation?
  → conversation

Should this data persist when the user starts a new conversation?
  → longTerm

Is this sensitive data (API keys, tokens)?
  → secret
```

***

## Visibility (v6)

Properties also have a **visibility** dimension that controls which agents can see them:

| Visibility       | Who sees it                               | Use Case                                                |
| ---------------- | ----------------------------------------- | ------------------------------------------------------- |
| `self` (default) | Only the owning agent                     | Agent-specific preferences, internal state              |
| `group`          | All agents in the same group conversation | Shared context in multi-agent orchestration             |
| `global`         | All agents for this user                  | Cross-agent user preferences (e.g., language, timezone) |

Visibility is orthogonal to scope — a property can be `longTerm` + `self` (persists across sessions, visible only to the owning agent) or `longTerm` + `global` (persists and visible to all agents).

***

## Setting Properties

### Via PropertySetter Configuration (JSON)

The PropertySetter task (`ai.labs.property`) sets properties based on triggered actions:

```json
{
  "setOnActions": ["greet_user"],
  "propertyInstructions": [
    {
      "name": "greeted",
      "valueString": "true",
      "scope": "conversation"
    },
    {
      "name": "preferred_language",
      "valueString": "{context.language}",
      "scope": "longTerm",
      "visibility": "global"
    }
  ]
}
```

When the `greet_user` action fires:

1. `greeted` is set to `"true"` for this conversation session
2. `preferred_language` is set from the input context and persisted across all conversations and agents

### Via Pre/Post Request Instructions

Properties can be set before or after any lifecycle task (HTTP calls, LLM calls):

```json
{
  "preRequest": {
    "propertyInstructions": [
      {
        "name": "requestTimestamp",
        "valueString": "{uuidUtils:generateUUID()}",
        "scope": "step"
      }
    ]
  },
  "postResponse": {
    "propertyInstructions": [
      {
        "name": "lastApiResponse",
        "fromObjectPath": "httpCalls.weatherApi",
        "scope": "conversation"
      }
    ]
  }
}
```

### Via LLM Tools (Agent-Driven)

When `enableMemoryTools` is enabled in the agent configuration, the LLM can set properties using built-in memory tools:

```
Agent: "I'll remember that you prefer dark mode."
→ Tool call: rememberFact(key="ui_preference", value="dark_mode", category="preference", visibility="self")
```

See [Persistent User Memory](/architecture-and-concepts/user-memory) for full details on the LLM memory tools, visibility scoping, and Dream consolidation.

***

## Accessing Properties in Templates

Properties are available in **all** templates via the `properties` namespace.

> ⚠️ **`properties` exposes raw values, not `Property` objects.** `MemoryItemConverter.convert()` puts `ConversationProperties.toMap()` into the template context, and `toMap()` returns the unwrapped Java value that was stored (`String`, `Integer`, `Float`, `Boolean`, `List`, `Map`) — the `Property` wrapper never reaches the template. Use `{properties.key}` directly. A `.valueString` / `.valueInt` / … suffix resolves against the raw value (a `String` has no `valueString` property) and fails at render time. The `valueString`, `valueInt`, … names are **write-side** field names of the JSON property-setter config only. AGENTS.md §5.1 is the authoritative reference for the template data model.

### In Output Templates

```
Hello {properties.userName}! Your preferred language is {properties.preferred_language}.
```

### In System Prompts (LLM)

```
You are a helpful assistant. The user's name is {properties.userName}.
They prefer {properties.preferred_language} responses.
```

### In HTTP Call Bodies

```json
{
  "userId": "{properties.userId}",
  "language": "{properties.preferred_language}"
}
```

### Reading the Different Value Types

The property-setter config picks the value type by which `value*` field you write (`valueString`, `valueInt`, `valueFloat`, `valueObject`, `valueList`, `valueBoolean`). In templates, all of them are read the same way — through the property name:

| Written as     | Read in a template             | Example                                   |
| -------------- | ------------------------------ | ----------------------------------------- |
| `valueString`  | `{properties.name}`            | `Hello {properties.name}`                 |
| `valueInt`     | `{properties.age}`             | `You are {properties.age}`                |
| `valueFloat`   | `{properties.score}`           | `Score: {properties.score}`               |
| `valueObject`  | `{properties.profile.<field>}` | `{properties.profile.email}`              |
| `valueList`    | `{properties.tags}`            | `{#for item in properties.tags}...{/for}` |
| `valueBoolean` | `{properties.isPremium}`       | `{#if properties.isPremium}...{/if}`      |

***

## Property Lifecycle

Properties are managed by `Conversation.java` at session boundaries — NOT by pipeline tasks.

### 1. Conversation Init

```
Conversation.init()
  └─→ loadUserProperties()
      └─→ IPropertiesHandler.getUserMemoryStore()
      └─→ IUserMemoryStore.getVisibleEntries(userId, agentId, groupIds, recallOrder, maxEntries)
      └─→ Entries converted to Property objects with scope=longTerm
      └─→ Loaded into conversationProperties
      └─→ Available as {properties.key} in all templates
```

Recall order (`most_recent` or `most_accessed`) and the maximum number of recalled entries come from the agent's `userMemoryConfig`. The documented defaults are **`most_recent` ordering and 50 entries** — the field defaults of `AgentConfiguration.UserMemoryConfig`, which apply as soon as the agent declares a `userMemoryConfig` block, however small.

> **Known inconsistency:** an agent with *no* `userMemoryConfig` block at all falls back to a separate hard-coded default in `Conversation` (`DEFAULT_MAX_RECALL_ENTRIES = 1000`) rather than to the 50 above, so the effective cap silently changes by a factor of 20 depending on whether the block is present. Treat 50 as the intended default and set `maxRecallEntries` explicitly if the number matters to you; unifying the two constants is tracked as a code fix.

### 2. Pipeline Execution

```
LifecycleManager runs pipeline
  └─→ PropertySetterTask sets properties based on actions
      └─→ scope=step (cleared after this turn)
      └─→ scope=conversation (lives for the session)
      └─→ scope=longTerm (persisted across conversations)
      └─→ scope=secret (auto-vaulted via SecretsVault)
```

### 3. Conversation Teardown

```
Conversation.postConversationLifecycleTasks()
  └─→ storePropertiesPermanently()
      └─→ All longTerm properties saved via IUserMemoryStore.upsert()
      └─→ Visibility applied at persistence boundary:
          - Explicit visibility on property → used as-is
          - No visibility set → defaults to agent's defaultVisibility (or `global`)
```

> **Key insight**: Persistent state is a **session concern** handled at init/teardown — NOT in the pipeline. This means properties "just work" without any task ordering dependencies.

> **Storage**: In v6, all persistent properties are stored in the unified `usermemories` collection (MongoDB) or `usermemories` table (PostgreSQL). The legacy `properties` collection has been removed. See [Persistent User Memory](/architecture-and-concepts/user-memory) for the full unified memory model.

***

## Secret Properties

Properties with `scope=secret` are automatically handled by the SecretsVault:

1. During pipeline execution, the secret value is available in memory normally
2. At teardown, the value is encrypted and stored in SecretsVault
3. The in-memory property value is scrubbed (replaced with a vault reference)
4. On next conversation init, the value is loaded from the vault and decrypted

```json
{
  "name": "api_token",
  "valueString": "sk-abc123...",
  "scope": "secret"
}
```

See [Secrets Vault](/security-and-compliance/secrets-vault) for full documentation.

***

## Best Practices

### 1. Use Appropriate Scopes

```json
// ❌ Don't persist temporary data
{"name": "tempResult", "scope": "longTerm"}

// ✅ Use step scope for temporary data
{"name": "tempResult", "scope": "step"}
```

### 2. Use fromObjectPath for Extraction

Instead of storing entire API responses, extract only what you need:

```json
{
  "name": "temperature",
  "fromObjectPath": "httpCalls.weatherApi.current.temperature",
  "scope": "conversation"
}
```

### 3. Use Visibility for Multi-Agent Scenarios

```json
// Agent-specific memory
{"name": "internal_state", "scope": "longTerm", "visibility": "self"}

// Shared within group conversation
{"name": "group_context", "scope": "longTerm", "visibility": "group"}

// Cross-agent user preference
{"name": "language", "scope": "longTerm", "visibility": "global"}
```

### 4. Naming Conventions

* Use `snake_case` for property names
* Use descriptive, specific names (`user_preferred_timezone` not `tz`)
* Prefix agent-specific properties with the agent's domain (`support_ticket_id`, `onboarding_step`)

***

## Related Documentation

* [Conversation Memory](/architecture-and-concepts/conversation-memory) - How memory flows through the pipeline
* [Secrets Vault](/security-and-compliance/secrets-vault) - Encrypted property storage
* [Passing Context Information](/agent-configuration/passing-context-information) - External data injection
* [Output Templating](/agent-configuration/output-templating) - Using properties in templates
* [LLM Integration](/agent-configuration/langchain) - Using properties in LLM prompts
* [HTTP Calls](/agent-configuration/httpcalls) - Using properties in API requests


# Persistent User Memory

Persistent User Memory enables EDDI agents to remember facts, preferences, and context about individual users **across conversations**. Unlike conversation-scoped properties that are lost when a conversation ends, persistent memories survive indefinitely and are automatically loaded into every new conversation with the same user.

## Overview

| Feature             | Description                                                                                  |
| ------------------- | -------------------------------------------------------------------------------------------- |
| **Scope**           | Per-user, per-agent (or globally shared)                                                     |
| **Storage**         | MongoDB (`usermemories` collection) or PostgreSQL (`usermemories` table)                     |
| **LLM Integration** | 4 built-in tools for autonomous memory management                                            |
| **Visibility**      | `self`, `group`, `global` scoping                                                            |
| **Guardrails**      | Configurable key/value limits, write-rate limits, capacity caps                              |
| **GDPR**            | Full right-to-erasure support via REST API and MCP tools                                     |
| **Maintenance**     | Background "Dream" consolidation (stale pruning, contradiction detection, LLM summarization) |

## Architecture

```
┌─────────────────────────────────────────────────────┐
│                   Conversation Pipeline              │
│                                                      │
│  LLM ──→ UserMemoryTool ──→ IUserMemoryStore        │
│            ↑                       ↑                 │
│            │                       │                 │
│      AgentOrchestrator     MongoUserMemoryStore      │
│      (per-invocation)      PostgresUserMemoryStore   │
│                                                      │
│  REST API ───────────────────→ IUserMemoryStore      │
│  MCP Tools ──────────────────→ IUserMemoryStore      │
│  DreamService (background) ─→ IUserMemoryStore      │
└─────────────────────────────────────────────────────┘
```

## Agent Configuration

Enable advanced memory features (LLM tools, Dream consolidation, guardrails, recall settings) in your agent's configuration:

```json
{
  "name": "My Agent",
  "enableMemoryTools": true,
  "userMemoryConfig": {
    "maxEntriesPerUser": 500,
    "maxRecallEntries": 50,
    "recallOrder": "most_recent",
    "onCapReached": "evict_oldest",
    "guardrails": {
      "maxKeyLength": 100,
      "maxValueLength": 1000,
      "maxWritesPerTurn": 10,
      "allowedCategories": ["preference", "fact", "context"]
    },
    "dream": {
      "enabled": true,
      "pruneStaleAfterDays": 90,
      "detectContradictions": true,
      "summarizeInteractions": true,
      "summarizeMinEntries": 5,
      "summarizeTargetEntries": 2,
      "summarizeGroupBy": "category",
      "preserveAgentProvenance": false,
      "maxCostPerRun": 0.50
    }
  },
  "builtInToolsWhitelist": ["usermemory"]
}
```

> **Note:** Basic `longTerm` property persistence (via `PropertySetterTask`) works for **all** agents regardless of `enableMemoryTools`. The flag only gates advanced features: LLM UserMemoryTool, Dream consolidation, write guardrails, and custom recall settings.

### Configuration Reference

| Field               | Type     | Default          | Description                                                                   |
| ------------------- | -------- | ---------------- | ----------------------------------------------------------------------------- |
| `maxEntriesPerUser` | `int`    | `500`            | Maximum memory entries per user                                               |
| `maxRecallEntries`  | `int`    | `50`             | Maximum entries returned by recall                                            |
| `recallOrder`       | `String` | `"most_recent"`  | `"most_recent"` (by updatedAt) or `"most_accessed"` (by accessCount)          |
| `onCapReached`      | `String` | `"evict_oldest"` | `"reject"` (block new writes) or `"evict_oldest"` (push out of recall window) |

### Guardrails

| Field               | Type           | Default                           | Description                            |
| ------------------- | -------------- | --------------------------------- | -------------------------------------- |
| `maxKeyLength`      | `int`          | `100`                             | Maximum characters for memory keys     |
| `maxValueLength`    | `int`          | `1000`                            | Maximum characters for memory values   |
| `maxWritesPerTurn`  | `int`          | `10`                              | Write-rate limit per conversation turn |
| `allowedCategories` | `List<String>` | `["preference","fact","context"]` | Allowed memory categories              |

### Dream Configuration

| Field                     | Type      | Default               | Description                                                                                                                                                                                                                                                                                      |
| ------------------------- | --------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `enabled`                 | `boolean` | `false`               | Enable background consolidation                                                                                                                                                                                                                                                                  |
| `pruneStaleAfterDays`     | `int`     | `90`                  | Remove entries not accessed in N days. Set to 0 to disable.                                                                                                                                                                                                                                      |
| `detectContradictions`    | `boolean` | `true`                | Flag entries with same key but different values                                                                                                                                                                                                                                                  |
| `summarizeInteractions`   | `boolean` | `false`               | Enable LLM-driven memory consolidation                                                                                                                                                                                                                                                           |
| `summarizeMinEntries`     | `int`     | `5`                   | Minimum entries in a group before summarization triggers                                                                                                                                                                                                                                         |
| `summarizeTargetEntries`  | `int`     | `2`                   | Target number of entries per group after consolidation                                                                                                                                                                                                                                           |
| `summarizeGroupBy`        | `String`  | `"category"`          | Grouping strategy: `"category"` or `"all"`                                                                                                                                                                                                                                                       |
| `preserveAgentProvenance` | `boolean` | `false`               | Sub-group by `sourceAgentId` (preserves per-agent provenance)                                                                                                                                                                                                                                    |
| `maxSummarizationCalls`   | `int`     | `10`                  | **Deprecated** — prefer `maxCostPerRun`. Still honoured as a secondary backstop *if you set it explicitly*, because silently dropping a bound an operator wrote is worse than enforcing a redundant one. A call count is a poor budget: consolidations differ wildly in cost.                    |
| `summarizationPrompt`     | `String`  | *(built-in)*          | Custom LLM instructions for consolidation                                                                                                                                                                                                                                                        |
| `maxCostPerRun`           | `double`  | `0.50`                | Maximum dollar cost per dream cycle — the primary ceiling                                                                                                                                                                                                                                        |
| `crossAgentMaintenance`   | `boolean` | `false`               | By default a dream cycle only touches memories the **firing agent** wrote (`sourceAgentId`). Set `true` to let it maintain the user's whole memory set across agents — otherwise agent A's retention setting would delete agent B's memories, and A's model endpoint would see B's private text. |
| `llmProvider`             | `String`  | `"anthropic"`         | LLM provider for dream operations                                                                                                                                                                                                                                                                |
| `llmModel`                | `String`  | `"claude-sonnet-4-6"` | Model for dream operations                                                                                                                                                                                                                                                                       |

## LLM Tools

When `usermemory` is in the agent's `builtInToolsWhitelist`, the LLM gets access to four tools:

### `rememberFact`

Store a fact about the user.

```
Parameters:
  key       - Short key name (e.g. "favorite_color", "dietary_restriction")
  value     - The value to remember
  category  - One of: "preference", "fact", "context"
  visibility - One of: "self", "group", "global" (default: "self")

Returns: "✅ Remembered: favorite_color = blue [preference, self]"
```

### `recallMemories`

Retrieve all memories visible to this agent for the current user.

```
Parameters: none

Returns:
  • name = Alice [fact, self]
  • favorite_color = blue [preference, self]
  • language = English [preference, global]
```

### `searchMemory`

Search for memories by keyword across keys and values.

```
Parameters:
  query - Search text (e.g. "color")

Returns: matching entries formatted as bullet list
```

### `forgetFact`

Delete a specific memory by key.

```
Parameters:
  key - The key name to forget (e.g. "favorite_color")

Returns: "✅ Forgotten: favorite_color"
```

## Visibility Scopes

| Scope    | Description                                          | Upsert Key                     |
| -------- | ---------------------------------------------------- | ------------------------------ |
| `self`   | Only the agent that stored it can see it             | `(userId, key, sourceAgentId)` |
| `group`  | All agents in the same group conversation can see it | `(userId, key, sourceAgentId)` |
| `global` | All agents for this user can see it                  | `(userId, key)`                |

### Group Memory

When agents participate in a [Group Conversation](/conversations-and-orchestration/group-conversations), the `groupId` is automatically injected into the conversation context. Memories stored with `group` visibility are visible to all agents in that group.

## REST API

Base path: `/usermemorystore/memories`

| Method   | Path                                                | Description                              |
| -------- | --------------------------------------------------- | ---------------------------------------- |
| `GET`    | `/{userId}`                                         | Get all memories for a user              |
| `GET`    | `/{userId}/visible?agentId=&groupId=&order=&limit=` | Get memories visible to a specific agent |
| `GET`    | `/{userId}/search?q=`                               | Search memories by keyword               |
| `GET`    | `/{userId}/category/{category}`                     | Get memories filtered by category        |
| `GET`    | `/{userId}/key/{key}`                               | Get a specific memory by key             |
| `PUT`    | `/`                                                 | Upsert a memory entry (JSON body)        |
| `DELETE` | `/entry/{entryId}`                                  | Delete a specific memory                 |
| `DELETE` | `/{userId}`                                         | Delete ALL memories for a user (GDPR)    |
| `GET`    | `/{userId}/count`                                   | Count memory entries                     |

### Example: Upsert a memory

```bash
curl -X PUT http://localhost:7070/usermemorystore/memories \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user-123",
    "key": "preferred_language",
    "value": "German",
    "category": "preference",
    "visibility": "global",
    "sourceAgentId": "agent-456"
  }'
```

### Example: Get visible memories

```bash
curl "http://localhost:7070/usermemorystore/memories/user-123/visible?agentId=agent-456&order=most_recent&limit=20"
```

## MCP Tools

8 MCP tools are available for external integration and administration:

| Tool                       | Role          | Description                             |
| -------------------------- | ------------- | --------------------------------------- |
| `list_user_memories`       | `eddi-viewer` | List all entries for a user             |
| `get_visible_memories`     | `eddi-viewer` | Get entries visible to a specific agent |
| `search_user_memories`     | `eddi-viewer` | Search by keyword                       |
| `get_memory_by_key`        | `eddi-viewer` | Look up by key name                     |
| `count_user_memories`      | `eddi-viewer` | Count entries                           |
| `upsert_user_memory`       | `eddi-admin`  | Insert or update an entry               |
| `delete_user_memory`       | `eddi-admin`  | Delete a specific entry                 |
| `delete_all_user_memories` | `eddi-admin`  | GDPR delete-all (requires `CONFIRM`)    |

### GDPR Compliance

The `delete_all_user_memories` MCP tool and `DELETE /{userId}` REST endpoint permanently remove **all** memory entries and legacy properties for a user. The MCP tool requires an explicit `confirmation="CONFIRM"` parameter as a safety gate.

## Dream Consolidation

The Dream service performs background maintenance on user memories:

1. **Stale Pruning** — Removes entries not accessed in `pruneStaleAfterDays` days. This is a deterministic operation with zero LLM cost.
2. **Contradiction Detection** — Identifies entries with the same key but different values (e.g., `language=English` from Agent A vs `language=German` from Agent B). V1 uses key-based matching; future versions will use LLM-driven semantic analysis.
3. **Interaction Summarization** — When `summarizeInteractions=true`, compresses multiple related facts into consolidated summaries using the configured LLM. Entries are grouped by the `summarizeGroupBy` strategy (per-category or all together), and each group above `summarizeMinEntries` is distilled into `summarizeTargetEntries` entries. Safety guarantees: new entries are inserted before originals are deleted; LLM failures or invalid responses preserve the original entries.

### Metrics

The Dream service exposes Micrometer metrics:

| Metric                       | Type    | Description                             |
| ---------------------------- | ------- | --------------------------------------- |
| `dream.users.processed`      | Counter | Users processed across all dream cycles |
| `dream.entries.pruned`       | Counter | Total entries pruned                    |
| `dream.contradictions.found` | Counter | Contradictions detected                 |
| `dream.entries.summarized`   | Counter | Entries reduced by LLM consolidation    |
| `dream.duration`             | Timer   | Duration of dream cycles                |

## Migration from Legacy Properties

In v6, the legacy `IPropertiesStore` interface and the `properties` collection have been **removed**. All user-scoped persistent data now lives in the unified `usermemories` collection.

| Aspect             | Legacy Properties (v5)             | User Memory (v6)                               |
| ------------------ | ---------------------------------- | ---------------------------------------------- |
| **Storage**        | `properties` collection (flat map) | `usermemories` collection (structured entries) |
| **Interface**      | `IPropertiesStore` (deleted in v6) | `IUserMemoryStore`                             |
| **Scoping**        | Per-user only                      | Per-user, per-agent, per-group                 |
| **LLM access**     | Via template variables only        | Direct LLM tool access                         |
| **Querying**       | Key lookup only                    | Key, category, search, visibility filtering    |
| **Administration** | No REST API                        | Full CRUD REST API + MCP tools                 |

### Backward Compatibility

Legacy flat property operations (`readProperties`, `mergeProperties`, `deleteProperties`) continue to work through `IUserMemoryStore` — they operate on `global` visibility entries in the `usermemories` collection. The REST endpoint at `/propertiesstore/properties/{userId}` is preserved.

### Startup Migration (MongoDB only)

On first startup, `PropertiesMigrationService` automatically migrates existing `properties` documents into `usermemories` as `global` entries with `category=legacy`. The old collection is renamed to `properties_migrated_v6` as a safety backup. This migration is idempotent and skipped if no legacy collection exists.

> **Note:** PostgreSQL deployments do not need migration — the `properties` table only existed in MongoDB (v5).

## Data Model

Each memory entry contains:

```json
{
  "id": "ObjectId",
  "userId": "user-123",
  "key": "preferred_language",
  "value": "German",
  "category": "preference",
  "visibility": "global",
  "sourceAgentId": "agent-456",
  "groupIds": ["group-1"],
  "sourceConversationId": "conv-789",
  "conflicted": false,
  "accessCount": 12,
  "createdAt": "2026-01-15T10:30:00Z",
  "updatedAt": "2026-03-29T14:22:00Z"
}
```


# Extensions

## Overview

**Extensions** are the building blocks of EDDI agents. In EDDI's composable architecture, agents are not monolithic applications but rather **assemblies of extensions**, each providing a specific capability. Extensions are referenced by packages, and packages are combined to form complete agents.

### The Agent Composition Hierarchy

```
Agent (.agent.json)
  └─ Workflow 1 (.package.json)
      ├─ Extension 1: Behavior Rules (eddi://ai.labs.behavior)
      ├─ Extension 2: HTTP Calls (eddi://ai.labs.httpcalls)
      └─ Extension 3: Output Sets (eddi://ai.labs.output)
  └─ Workflow 2 (.package.json)
      ├─ Extension 1: Dictionary (eddi://ai.labs.parser.dictionaries.regular)
      └─ Extension 2: LangChain (eddi://ai.labs.llm)
```

### What Extensions Do

Each extension type corresponds to a **lifecycle task** or **resource** that the agent can use:

| Extension Type                  | Purpose                          | Lifecycle Role                                        |
| ------------------------------- | -------------------------------- | ----------------------------------------------------- |
| `ai.labs.parser`                | Input parsing and normalization  | Transforms raw user input into structured expressions |
| `ai.labs.parser.dictionaries.*` | Define vocabularies and entities | Used by parser to recognize intents and entities      |
| `ai.labs.behavior`              | Define IF-THEN rules             | Decides what actions to take based on conditions      |
| `ai.labs.httpcalls`             | Configure external API calls     | Executes HTTP requests to external services           |
| `ai.labs.llm`                   | Configure LLM integrations       | Sends requests to LLM APIs (OpenAI, Claude, etc.)     |
| `ai.labs.output`                | Define output templates          | Formats responses using conversation data             |
| `ai.labs.property`              | Extract and store data           | Manages conversation memory properties                |

### EDDI Resource URIs

All EDDI's resources start with `eddi://`, which is used to distinguish EDDI-specific extensions from other resources. This URI scheme allows:

* **Version control**: Each extension can have multiple versions
* **Reusability**: The same extension can be used by multiple packages/agents
* **Clear references**: Explicit URIs make configuration transparent

Example URI:

```
eddi://ai.labs.behavior/behaviorstore/behaviorsets/673abc123?version=1
```

## Extension Discovery

In this article we will talk about **EDDI**'s **`extensions`**.

**EDDI's `extensions`** are the features that your current instance of EDDI is supporting, the latter are used in the process of configuring/developing an Agent.

The list of `extensions` will allow you to have an overview of what is enabled in your current instance of **EDDI**, the list can be retrieved by calling the API endpoint below.

### Extensions REST API Endpoint

| Element      | Value                        |
| ------------ | ---------------------------- |
| HTTP Method  | `GET`                        |
| API Endpoint | `/extensionstore/extensions` |

## Model

```javascript
[
  {
    type: "string",
    displayName: "string",
    configs: {},
    extensions: {},
  },
];
```

### Description of the model

| Element     | Value                                     |
| ----------- | ----------------------------------------- |
| type        | (`String`) The type of the extension      |
| displayName | (`String`) A given name to the extension  |
| configs     | (`Object`) Configuration of the extension |
| extensions  | (`Object`) Extensions of the extension    |

## Example

> More about regular dictionaries can be found [here](/getting-started/creating-your-first-agent#1-creating-a-regular-dictionary).

*Request URL*

`GET http://localhost:7070/extensionstore/extensions`

*Response Body*

```javascript
[
  {
    type: "ai.labs.parser",
    displayName: "Input Parser",
    configs: {
      includeUnknown: {
        displayName: "Include Unknown Expressions",
        fieldType: "BOOLEAN",
        defaultValue: true,
        optional: true,
      },
      includeUnused: {
        displayName: "Include Unused Expressions",
        fieldType: "BOOLEAN",
        defaultValue: true,
        optional: true,
      },
      appendExpressions: {
        displayName: "Append Expressions",
        fieldType: "BOOLEAN",
        defaultValue: true,
        optional: true,
      },
    },
    extensions: {
      corrections: [
        {
          type: "ai.labs.parser.corrections.levenshtein",
          displayName: "Damerau Levenshtein Correction",
          configs: {
            distance: {
              displayName: "Distance",
              fieldType: "INT",
              defaultValue: 2,
              optional: true,
            },
          },
          extensions: {},
        },
        {
          type: "ai.labs.parser.corrections.stemming",
          displayName: "Grammar Stemming Correction",
          configs: {},
          extensions: {},
        },
        {
          type: "ai.labs.parser.corrections.phonetic",
          displayName: "Phonetic Matching Correction",
          configs: {},
          extensions: {},
        },
        {
          type: "ai.labs.parser.corrections.mergedTerms",
          displayName: "Merged Terms Correction",
          configs: {},
          extensions: {},
        },
      ],
      normalizer: [
        {
          type: "ai.labs.parser.normalizers.punctuation",
          displayName: "Punctuation Normalizer",
          configs: {
            removePunctuation: {
              displayName: "Remove Punctuation",
              fieldType: "BOOLEAN",
              defaultValue: false,
              optional: true,
            },
            punctuationRegexPattern: {
              displayName: "Punctuation RegEx Pattern",
              fieldType: "STRING",
              defaultValue: "!?:.,;",
              optional: true,
            },
          },
          extensions: {},
        },
        {
          type: "ai.labs.parser.normalizers.specialCharacter",
          displayName: "Convert Special Character Normalizer",
          configs: {},
          extensions: {},
        },
        {
          type: "ai.labs.parser.normalizers.contractedWords",
          displayName: "Contracted Word Normalizer",
          configs: {},
          extensions: {},
        },
        {
          type: "ai.labs.parser.normalizers.allowedCharacter",
          displayName: "Remove Undefined Character Normalizer",
          configs: {},
          extensions: {},
        },
      ],
      dictionaries: [
        {
          type: "ai.labs.parser.dictionaries.integer",
          displayName: "Integer Dictionary",
          configs: {},
          extensions: {},
        },
        {
          type: "ai.labs.parser.dictionaries.decimal",
          displayName: "Decimal Dictionary",
          configs: {},
          extensions: {},
        },
        {
          type: "ai.labs.parser.dictionaries.ordinalNumber",
          displayName: "Ordinal Numbers Dictionary",
          configs: {},
          extensions: {},
        },
        {
          type: "ai.labs.parser.dictionaries.punctuation",
          displayName: "Punctuation Dictionary",
          configs: {},
          extensions: {},
        },
        {
          type: "ai.labs.parser.dictionaries.time",
          displayName: "Time Expression Dictionary",
          configs: {},
          extensions: {},
        },
        {
          type: "ai.labs.parser.dictionaries.email",
          displayName: "Email Dictionary",
          configs: {},
          extensions: {},
        },
        {
          type: "ai.labs.parser.dictionaries.regular",
          displayName: "Regular Dictionary",
          configs: {
            uri: {
              displayName: "Resource URI",
              fieldType: "URI",
              optional: false,
            },
          },
          extensions: {},
        },
      ],
    },
  },
  {
    type: "ai.labs.behavior",
    displayName: "Behavior Rules",
    configs: {
      appendActions: {
        displayName: "Append Actions",
        fieldType: "BOOLEAN",
        defaultValue: true,
        optional: false,
      },
      uri: {
        displayName: "Resource URI",
        fieldType: "URI",
        optional: false,
      },
    },
    extensions: {},
  },
  {
    type: "ai.labs.output",
    displayName: "Output Generation",
    configs: {
      uri: {
        displayName: "Resource URI",
        fieldType: "URI",
        optional: false,
      },
    },
    extensions: {},
  },
  {
    type: "ai.labs.templating",
    displayName: "Templating",
    configs: {},
    extensions: {},
  },
  {
    type: "ai.labs.property",
    displayName: "Property Extraction",
    configs: {},
    extensions: {},
  },
  {
    type: "ai.labs.callback",
    displayName: "External Callback",
    configs: {
      callbackUri: {
        displayName: "Callback URI",
        fieldType: "URI",
        optional: false,
      },
      callOnActions: {
        displayName: "Call on Actions",
        fieldType: "STRING",
        defaultValue: "",
        optional: true,
      },
      timeoutInMillis: {
        displayName: "Timeout in Milliseconds",
        fieldType: "URI",
        defaultValue: 10000,
        optional: true,
      },
    },
    extensions: {},
  },
  {
    type: "ai.labs.httpcalls",
    displayName: "Http Calls",
    configs: {
      uri: {
        displayName: "Resource URI",
        fieldType: "URI",
        optional: false,
      },
    },
    extensions: {},
  },
];
```

*Response Code*

`200`


# Behavior Rules

## Overview

**Behavior Rules** are the decision-making engine in EDDI's Lifecycle Pipeline. They are IF-THEN rules that evaluate conversation state and trigger actions based on conditions. This is where you define **when** to call an LLM, **when** to invoke an API, and **how** your agent responds to user inputs.

### Role in the Lifecycle

In EDDI's processing pipeline, Behavior Rules sit between input parsing and action execution:

```
User Input → Parser → Behavior Rules → API/LLM Calls → Output Generation
```

Behavior Rules examine the conversation memory (including parsed input, context data, and conversation history) and decide:

* Which actions to trigger
* Whether to call an LLM or skip it
* Whether to make external API calls
* What output to generate

### Key Concepts

* **Rules are IF-THEN logic**: If all conditions match, execute the specified actions
* **Rules are grouped**: Multiple rules can be organized into groups for better structure
* **Sequential execution**: Rules within a group execute in order until one succeeds
* **First match wins**: Once a rule in a group succeeds, remaining rules in that group are skipped
* **Actions trigger other lifecycle tasks**: Actions like `httpcall(weather-api)` or `send_to_llm` activate other parts of the pipeline

## Behavior Rules Structure

`Behavior Rules` are very flexible in structure to cover most use cases that you will come across. `Behavior Rules` are clustered in `Groups`. `Behavior Rules` are executed sequentially within each `Group`. As soon as one `Behavior Rule` succeeds, all remaining `Behavior Rules` in this `Group` will be skipped.

## **Groups**

```javascript
{
  "behaviorGroups": [
    {
      "name": "GroupName",
      "behaviorRules": [
        {
          "name": "RuleName",
          "actions": [
            "action-to-be-triggered"
          ],
          "conditions": [
            <CONDITIONS>
          ]
        },
        {
          "name": "DifferentRule",
          "actions": [
            "another-action-to-be-triggered"
          ],
          "conditions": [
            <CONDITIONS>
          ]
        },
        <MORE_RULES>
      ]
    }
  ]
}
```

## Type of Conditions

Each `Behavior Rule` has a list of `conditions`, that, depending on the `condition` , might have a list of `sub-conditions`.

> **If all conditions are true, then the Behavior Rule is successful and it will trigger predefined actions**.

### List of available conditions:

* [Input Matcher](#input-matcher)
* [Context Matcher](#context-matcher)
* [Connector](#connector)
* [Negation](#negation)
* [Occurrence](#occurrence)
* [Dependency](#dependency)
* [Action Matcher](#action-matcher)
* [Dynamic Value Matcher](#dynamic-value-matcher)

### General Structure

`conditions` are always children of either a `Behavior Rule` or another `condition`. It will always follows that same structure.

### Description of condition structure

### Input Matcher

The `inputmatcher` is used to match **user inputs**. Not directly the real input of the user, but the meaning of it, represented by `expressions` that are **resolved** from by the `parser`.

### Description

| Element | Value          | Description                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| type    | `inputmatcher` |                                                                                                                                                                                                                                                                                                                                                                                                                                |
| configs | `expressions`  | <p>comma separated list of</p><p><code>expressions</code> such as:</p><p><code>expression(value),expression2(value2),</code></p><p><code>yetAnotherExpressions(anotherValue(withASubValue))</code></p>                                                                                                                                                                                                                         |
|         | `occurrence`   | <p><code>currentStep</code> - used in case if the user said it in this <code>conversationStep</code></p><p><code>lastStep</code> - used in case if the user said it in the previous <code>conversationStep</code></p><p><code>anyStep</code> - used in case if the user said it in any step if this whole conversation</p><p><code>never</code> - used in case if the user has never said that, including the current step</p> |

If the **user** would type "hello", and the parser resolves this as expressions "`greeting(hello)`" *\[assuming it has been defined in one of the dictionaries]*, then a `condition` could look as following in order to match this user input meaning:

```javascript
(...)
  "conditions": [
    {
      "type": "inputmatcher",
      "configs": {
        "expressions": "greeting(*)",
        "occurrence": "currentStep"
      }
    }
  ]
(...)
```

This `inputmatcher` `condition` will match any `expression` of type greeting, may that be "`greeting(hello)`", "`greeting(hi)`" or anything else. Of course, if you would want to match `greeting(hello)` explicitly, you would put "`greeting(hello)`" as value for the "`expressions`" field.

### Context Matcher

The `contextmatcher` is used to match `context` data that has been handed over to **EDDI** alongside the user input. This is great to check certain `conditions` that come from another system, such as the day time or to check the existence of user data.

### Description

| Element | Value                                                                                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| type    | `contextmatcher`                                                                         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| configs | `contextKey`                                                                             | The key for this context (defined when handing over context to **EDDI**)                                                                                                                                                                                                                                                                                                                                                                                         |
|         | `contextType`                                                                            | <p><code>expressions</code></p><p><code>object</code></p><p><code>string</code></p>                                                                                                                                                                                                                                                                                                                                                                              |
|         | `expressions` (if `contextType=expressions`)                                             | A `list` of comma separated `expressions`                                                                                                                                                                                                                                                                                                                                                                                                                        |
|         | <p><code>objectKeyPath</code> (if contextType=object)</p><p><code>objectValue</code></p> | <p>Allows match via <code>Jsonpath</code>, such as "<code>profile.username</code>" (see: <a href="https://github.com/rest-assured/rest-assured/wiki/Usage"><code><https://github.com/rest-assured/rest-assured/wiki/Usage></code></a><code>)</code></p><p>Exp: <code>contextKey</code>: <code>userInfo</code> , <code>contextValue</code>: <code>{"profile":{"username":"John"}}</code> The value to be match with the extracted <code>JsonPath</code> value</p> |
|         | string                                                                                   | `string` matching (`equals`)                                                                                                                                                                                                                                                                                                                                                                                                                                     |

### Examples

```javascript
(...)
  "conditions": [
    {
      "type": "contextmatcher",
      "configs": {
        "contextType": "expressions",
        "contextKey": "someContextName",
        "expressions": "contextDataExpression(*)"
      }
    }
  ]
(...)

(...)
  "conditions": [
    {
      "type": "contextmatcher",
      "configs": {
        "contextType": "object",
        "contextKey": "userInfo",
        "objectKeyPath": "profile.username",
        "objectValue": "John"
      }
    }
  ]
(...)

(...)
  "conditions": [
    {
      "type": "contextmatcher",
      "configs": {
        "contextType": "string",
        "contextKey": "daytime",
        "string": "night"
      }
    }
  ]
(...)
```

### Limitations

* The runtime `context` you hand over to EDDI may declare `"type": "array"`, but `contextmatcher` only understands `expressions`, `object` and `string`. **An array context can never match any `contextmatcher`** — the condition always fails and the engine logs a warning naming the context key. Send the data as an `object` (and match with `objectKeyPath`) if you need to match into it.
* The configured `contextType` must equal the runtime type of the context. A `contextmatcher` configured for `string` never matches an `expressions` context, and vice versa; the mismatch is logged at DEBUG level.

### Connector

The `connector` is there to all logical `OR` conditions within rules. By default all conditions are `AND` `conditions`, but in some cases it might be suitable to connect conditions with a logical `OR`.

### Description

| Element | Value                             |
| ------- | --------------------------------- |
| type    | `connector`                       |
| values  | `operator` (either `AND` or `OR`) |

### **Examples**

```javascript
(...)
  "conditions": [
    {
      "type": "connector",
      "configs": {
        "operator": "OR"
      },
      "conditions": [
        <any other conditions>
      ]
    }
  ]
(...)
```

Two edge cases are worth knowing (they mirror the `negation` rules below):

* A `connector` **must** declare at least one nested condition. An empty `connector` is rejected at configuration validation time — an `AND` over zero conditions would succeed unconditionally and make the surrounding rule fire on every turn.
* If a child reports `NOT_EXECUTED` (e.g. a `sizematcher` where every bound is `-1`), the `AND` branch treats it as a failure, exactly like a condition placed directly on the rule. A misconfigured condition therefore decides a rule the same way whether or not it is wrapped in a `connector`.

### Negation

Inverts the overall outcome of the children conditions

In some cases it is more relevant if a `condition` is `false` than if it is `true`, this is where the `negation` `condition` comes into play. The logical result of all children together (`AND` connected), will be ***inverted***.

This is exactly what the engine does: every child is evaluated in order, the first child that *fails* makes the negation succeed (the `AND` is already false), and the negation only fails when *every* child succeeded. Multi-child negations therefore behave as documented — earlier EDDI versions only looked at the first child, which made a negation with more than one child effectively always true.

Two edge cases are worth knowing:

* A `negation` **must** declare at least one nested condition. An empty `negation` is rejected at configuration validation time.
* If a child reports `ERROR` or `NOT_EXECUTED` (e.g. a `sizematcher` where every bound is `-1`), there is nothing meaningful to invert — that state is propagated unchanged instead of being flipped.

### Example:

```bash
Child 1 - true
Child 2 - true
→ Negation = false
Child 1 - false
Child 2 - true
→ Negation = true

(...)
  "conditions": [
    {
      "type": "negation",
      "conditions": [
        <any other conditions>
      ]
    }
  ]
(...)
```

### Occurrence

Defines the occurrence/frequency of an action in a `Behavior Rule`.

```javascript
(...)
{
  "type": "occurrence",
  "configs": {
    "maxTimesOccurred": "0",
    "minTimesOccurred": "0",
    "behaviorRuleName": "Welcome"
  }
}
(...)
```

`behaviorRuleName` and at least one of `minTimesOccurred` / `maxTimesOccurred` are **required** — both are validated when the ruleset is loaded. Without a rule name there is nothing to count, and without a bound the condition matches as soon as any behavior rule has ever succeeded, which is almost never what was intended.

### Dependency

Check if another `Behavior Rule` has met it's condition or not in the same `conversationStep`. Sometimes you need to know if a rule has succeeded , `dependency` will take that rule that hasn't been executed yet in a sandbox environment as a `reference` for an other behavior rule.

```javascript
(...)
{
  "type": "dependency",
  "configs": {
    "reference": "<name-of-another-behavior-rule>"
  }
}
(...)
```

### Action Matcher

As `inputMatcher` doesn't look at expressions but it looks for actions instead, imagine a `Behavior Rule` has been triggered and you want to check if that action has been triggered before.

```javascript
(...)
{
  "type": "actionmatcher",
  "configs": {
    "actions": "show_available_products",
    "occurrence": "lastStep"
  }
}
(...)
```

### Dynamic Value Matcher

This will allow you to compile a condition based on any http request/properties or any sort of variables available in EDDI's context.

```javascript
(...)
  {
  "type": "dynamicvaluematcher",
  "configs": {
    "valuePath": "memory.current.httpCalls.someObj.errors",
    "contains": "partly matching",
    "equals": "needs to be equals"
  }
}
(...)
```

### Size Matcher

This condition type checks the size of arrays or collections in the conversation memory.

```json
(...)
  {
  "type": "sizematcher",
  "configs": {
    "valuePath": "memory.current.httpCalls.results",
    "min": "1",
    "max": "10",
    "equal": "-1"
  }
}
(...)
```

The example above matches whenever the API call stored between 1 and 10 result elements. Collections, maps and arrays report their **element count** — earlier EDDI versions ran the resolved value through `Integer.parseInt`, so a real collection silently degraded to size `0` and a rule like this one could never match.

| Config      | Type   | Description                              |
| ----------- | ------ | ---------------------------------------- |
| `valuePath` | string | Path to the array/collection to check    |
| `min`       | int    | Minimum size required (-1 to skip check) |
| `max`       | int    | Maximum size allowed (-1 to skip check)  |
| `equal`     | int    | Exact size required (-1 to skip check)   |

**How the size is determined** — the value the `valuePath` resolves to decides the rule:

| Resolved value            | Size used                                            |
| ------------------------- | ---------------------------------------------------- |
| `null` / path not found   | `0`                                                  |
| Collection, Map, or array | Its element count                                    |
| Number                    | The number itself (the value *is* the size)          |
| Numeric string            | The parsed number (kept for backwards compatibility) |
| Any other value           | The length of its textual representation             |

If `min`, `max` and `equal` are all `-1`, the condition reports `NOT_EXECUTED` — it neither succeeds nor fails, and a wrapping `negation` propagates that state instead of inverting it.

## The Behavior Rule API Endpoints

The API Endpoints below will allow you to manage the `Behavior Rule`s in your EDDI instance.

The **`{id}`** is a path parameters that indicate which behavior rule you want to alter.

### API Methods

| HTTP Method | API Endpoint                                      | Request Body          | Response              |
| ----------- | ------------------------------------------------- | --------------------- | --------------------- |
| **DELETE**  | `/behaviorstore/behaviorsets/{id}`                | N/A                   | N/A                   |
| **GET**     | `/behaviorstore/behaviorsets/{id}`                | N/A                   | **BehaviorSet model** |
| **PUT**     | `/behaviorstore/behaviorsets/{id}`                | **BehaviorSet model** | N/A                   |
| **GET**     | `/behaviorstore/behaviorsets/descriptors`         | N/A                   | **BehaviorSet model** |
| **POST**    | `/behaviorstore/behaviorsets`                     | **BehaviorSet model** | N/A                   |
| **GET**     | `/behaviorstore/behaviorsets/{id}/currentversion` | N/A                   | **BehaviorSet model** |
| **POST**    | `/behaviorstore/behaviorsets/{id}/currentversion` | **BehaviorSet model** | N/A                   |

### Example

We will demonstrate here the creation of a `BehaviorSet`

*Request URL*

`POST http://localhost:7070/behaviorstore/behaviorsets`

*Request Body*

```javascript
{
  "behaviorGroups": [
    {
      "name": "Smalltalk",
      "behaviorRules": [
        {
          "name": "Welcome",
          "actions": [
            "welcome"
          ],
          "conditions": [
            {
              "type": "negation",
              "conditions": [
                {
                  "type": "occurrence",
                  "configs": {
                    "maxTimesOccurred": "1",
                    "behaviorRuleName": "Welcome"
                  }
                }
              ]
            }
          ]
        },
        {
          "name": "Greeting",
          "actions": [
            "greet"
          ],
          "conditions": [
            {
              "type": "inputmatcher",
              "configs": {
                "expressions": "greeting(*)",
                "occurrence": "currentStep"
              }
            }
          ]
        },
        {
          "name": "Goodbye",
          "actions": [
            "say_goodbye",
            "CONVERSATION_END"
          ],
          "conditions": [
            {
              "type": "inputmatcher",
              "configs": {
                "expressions": "goodbye(*)"
              }
            }
          ]
        },
        {
          "name": "Thank",
          "actions": [
            "thank"
          ],
          "conditions": [
            {
              "type": "inputmatcher",
              "configs": {
                "expressions": "thank(*)"
              }
            }
          ]
        },
        {
          "name": "how are you",
          "actions": [
            "how_are_you"
          ],
          "conditions": [
            {
              "type": "inputmatcher",
              "configs": {
                "expressions": "how_are_you"
              }
            }
          ]
        }
      ]
    }
  ]
}
```

*Response Body*

`no content`

*Response Code*

`201`

The `Location` response header contains the URI of the newly created resource:

```
Location: eddi://ai.labs.behavior/behaviorstore/behaviorsets/{id}?version=1
```


# HTTP Calls / API Calls

## Overview

**HttpCalls** enable EDDI agents to integrate with external REST APIs, making EDDI a powerful orchestration layer that can combine conversational AI with traditional backend services. This is how agents can fetch real-time data, authenticate users, store information in external systems, or trigger business workflows.

### Role in the Lifecycle

HttpCalls are lifecycle tasks that execute during the agent's processing pipeline:

```
User Input → Parser → Behavior Rules → HttpCalls → Output Generation
```

Typically, Behavior Rules decide **when** to make an API call by triggering an action like `httpcall(weather-api)`, and the HttpCalls extension defines **how** to make that call.

### Common Use Cases

* **Fetching external data**: Weather, stock prices, product information, etc.
* **Authentication**: OAuth flows, token validation, user verification
* **CRM Integration**: Creating tickets, updating customer records, searching databases
* **Business workflows**: Processing payments, sending notifications, triggering events
* **Multi-step APIs**: First call gets auth token, second call uses it to access protected resources
* **Analytics**: Sending conversation data to external analytics platforms
* **Self-modification**: The "Agent Father" agent uses HttpCalls to create other agents via EDDI's own API

### Key Features

* **Template-based**: Use conversation memory in URLs, headers, and body (e.g., `${context.userName}`)
* **Response handling**: Save JSON responses to memory for use in outputs or subsequent calls
* **Chaining**: One HttpCall's response can be used in another HttpCall
* **Quick reply generation**: Automatically create quick reply buttons from API response arrays
* **Property extraction**: Extract specific values from responses and save them to conversation memory
* **Batch requests**: Make multiple API calls by iterating over an array
* **Fire and forget**: Optional asynchronous calls that don't wait for a response
* **Caller identity**: Call an API *as the signed-in user* with `${caller:token}` — see [Calling as the signed-in user](#calling-as-the-signed-in-user)

## Calling as the signed-in user

*Since 6.2.0.*

A header can reference the authenticated caller, so the agent calls an API with **that user's** credentials rather than one static credential baked into the config:

| Reference          | Resolves to                                |
| ------------------ | ------------------------------------------ |
| `${caller:token}`  | The caller's raw bearer token              |
| `${caller:userId}` | The caller's principal name (not a secret) |

```json
"headers": {
  "Authorization": "Bearer ${caller:token}"
}
```

This matters most when the API being called is **EDDI's own**. A static credential is the wrong shape there: an OIDC token expires within the hour, cannot be least-privilege, and collapses every action to a single synthetic principal in the audit trail. With `${caller:token}`, authorization stays EDDI's normal per-endpoint enforcement and the audit trail names a real person. This is what the EDDI-Manager Platform Operator uses.

### Rules

Resolution is deliberately narrow, and each rule fails the call loudly rather than degrading quietly:

* **Same origin only.** The token is released only when the call targets the exact `scheme://host:port` the caller addressed. That origin is read from the inbound request, not from configuration, so a config naming a third-party host cannot exfiltrate a user's token — and no allow-list is needed for this to be safe by default.
* **Headers only.** `${caller:token}` in a query parameter, request body or request path is rejected. Tokens in URLs leak through access logs, proxies and browser history, and nothing outside a header is substituted anyway — a reference there would travel to the API as literal text. `${caller:userId}` may be used in headers and query parameters.
* **Authenticated turns only.** The identity comes from the request that drove the turn, so scheduled jobs and triggers cannot satisfy `${caller:token}`.
* **Fails closed.** An unsatisfiable reference raises an error instead of resolving to an empty string, which would send `Bearer` and surface later as a confusing `401`.

The resolved token is never written to conversation memory: authorization headers are scrubbed before the request is recorded.

Set `eddi.caller-identity.enabled=false` to forbid the feature outright.

The same reference works in an **MCP server's `apiKey`**, so a tool call reaches that server as the chatting user rather than as a standing service principal. Only the tool call carries the caller — the handshake and `tools/list` do not, because the client is cached and a session opened with one user's token would be reused by everyone after them. See [`mcp-server.md`](/protocols-and-integration/mcp-server#calling-an-mcp-server-as-the-chatting-user).

### Running behind a reverse proxy

The origin is taken from the inbound request as EDDI sees it. Behind a TLS-terminating proxy or ingress, that is the *internal* hop — something like `http://10.0.0.5:8080` — while the caller addressed `https://eddi.example`. The two do not match, so `${caller:token}` fails closed and the agent reports that the call targets a different origin, with nothing obviously wrong in the config.

If EDDI runs behind a proxy, enable forwarded-header handling so the request reflects what the caller actually addressed:

```properties
quarkus.http.proxy.proxy-address-forwarding=true
quarkus.http.proxy.enable-forwarded-host=true
```

This is deliberately **not** on by default. It makes EDDI trust `X-Forwarded-*` headers, which any client can send — safe when a trusted proxy overwrites them, wrong when EDDI is directly reachable. Turn it on only together with a proxy that sets those headers itself.

## HttpCalls Configuration

In this article we will talk about EDDI's **`httpCalls`** **feature** (calling other `JSON` APIs).

The **`httpCalls`** feature allows a **Agent** to consume **3rd** party APIs and use the `JSON` response in another **`httpCall`** (for **authentication** or requesting a token for instance) or directly print the results in Agent's `Output,` this means, for example, you can call a weather API and use the `JSON` response in your Agent's output if the user asks about today's weather or the week's forecast!

We will emphasize the `httpCall` model and go through an example step by step, you can also download the example in **Postman** collection format and run the steps.

## Model and API endpoint

```javascript
{
  "targetServerUrl": "string",
  "httpCalls": [
    {
      "name": "string",
      "saveResponse": boolean,
      "fireAndForget": boolean,
      "responseObjectName": "string",
      "actions": [
        "string"
      ],
      "preRequest": {
        "batchRequests": {
          "pathToTargetArray": "string",
          "iterationObjectName": "string"
        }
      },
      "request": {
        "path": "string",
        "headers": {},
        "queryParams": {},
        "method": "string",
        "contentType": "string",
        "body": "string"
      },
      "postResponse": {
        "qrBuildInstructions": [
          {
            "pathToTargetArray": "String",
            "iterationObjectName": "String",
            "quickReplyValue": "String",
            "quickReplyExpressions": "String"
          }
        ],
        "propertyInstructions": [
          {
            "name": "string",
            "value": "string",
            "scope": "string",
            "fromObjectPath": "savedObjName.something.something",
            "override": boolean,
            "httpCodeValidator": {
              "runOnHttpCode": [
                <array of Integers>
              ],
              "skipOnHttpCode": [
                <array of Integers>
              ]
            }
          }
        ]
      }
    }
  ]
}
```

### Description

An `httpCall` is mainly composed from the `targetServer` `array` of `httpCalls`, the latter will have request where you put all details about your actual **http request** (`method`,`path`,`headers`, etc..) and postResponse where you can define what happens after the `httpCall` has been executed and a `response` has been received; such as quick replies by using `qrBuildInstruction`.

You can use ***`${memory.current.httpCalls.<responseObjectName>}`*** to access your `JSON` object, so you can use it in `output templating` or in another `httpCall`, for example an `httpCall` will get the `oAuth` `token` and another `httpCall` will use in the `http` `headers` to authenticate to an API.

### Description of the model

| Element                                                                     | Description                                                                                                                                                                                                                   |
| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| targetServerUrl                                                             | (`String`) `root/context` path of the `httpCall` (e.g `http://example.com/api)`                                                                                                                                               |
| httpCall.saveResponse                                                       | (`Boolean`) whether to save the `JSON` response into `${memory.current.httpCalls}`                                                                                                                                            |
| httpCall.fireAndForget                                                      | (`Boolean`) whether to execute the request without waiting for a response to be returned, (useful for `POST`)                                                                                                                 |
| httpCall.responseObjectName                                                 | (`String`) name of the `JSON` object so it can be accessed from other `httpCalls` or `outputsets`.                                                                                                                            |
| httpCall.actions                                                            | (`String`) name of the `output`/`behavior` set mapped to this http call.                                                                                                                                                      |
| httpCall.preRequest.batchRequests.pathToTargetArray                         | (`String`) `JSON` path to the target array to be used as body of requests e.g: "`memory.current.output`"                                                                                                                      |
| httpCall.preRequest.batchRequests.iterationObjectName                       | (`String`) name of the variable to be used for each element of array found in `pathToTargetArray`                                                                                                                             |
| httpCall.request.path                                                       | (`String`) path in the `targetServer` of the `httpCall` (e.g /`books`)                                                                                                                                                        |
| httpCall.request.headers                                                    | (`Array`:\<key, value> ) for each `httpCall HTTP header`                                                                                                                                                                      |
| httpCall.request.queryParams                                                | (`Array`: \<key, value>) for each `httpCall` query parameter                                                                                                                                                                  |
| httpCall.request.method                                                     | (`String`) `HTTP` Method of the `httpCall` (e.g `GET`,`POST`,etc...)                                                                                                                                                          |
| httpCall.request.contentType                                                | (`String`) value of the `contentType HTTP header` of the `httpCall`                                                                                                                                                           |
| httpCall.request.body                                                       | (`String`) an escaped `JSON` object that goes in the `HTTP Request` body if needed.                                                                                                                                           |
| httpCall.postResponse.qrBuildInstructions\[].pathToTargetArray              | (`String`) path to the array in your `JSON` **response data.**                                                                                                                                                                |
| httpCall.postResponse.qrBuildInstructions\[].iterationObjectName            | (`String`) a variable name that will point to the `TargetArray.`                                                                                                                                                              |
| httpCall.postResponse.qrBuildInstructions\[].quickReplyValue                | (`String`) `Qute expression` to use as a `quickReply` value.                                                                                                                                                                  |
| httpCall.postResponse.qrBuildInstructions\[].quickReplyExpressions          | (`String`) `expression` to retrieve a property from `iterationObjectName`.                                                                                                                                                    |
| httpCall.postResponse.propertyInstructions.name                             | (`String`) name of property to be used in templating                                                                                                                                                                          |
| httpCall.postResponse.propertyInstructions.value                            | (`String`) a static value can be set here if `fromObjectPath` is not defined.                                                                                                                                                 |
| httpCall.postResponse.propertyInstructions.scope                            | <p>(<code>String</code>) Can be either :</p><p><code>step</code> used for only for one user interaction</p><p><code>conversation</code> for entire conversation and</p><p><code>longTerm</code> for between conversations</p> |
| httpCall.postResponse.propertyInstructions.fromObjectPath                   | (`String`) JSON path to the saved object e.g `savedObjName.something.something`                                                                                                                                               |
| httpCall.postResponse.propertyInstructions.override                         | (`Boolean`) flag for override                                                                                                                                                                                                 |
| httpCall.postResponse.propertyInstructions.httpCodeValidator.runOnHttpCode  | (`Array`: \<Integer> ) a list of http code that enables this property instruction e.g \[`200`]                                                                                                                                |
| httpCall.postResponse.propertyInstructions.httpCodeValidator.skipOnHttpCode | (`Array`: \<Integer>) list of http code that enables this property instruction e.g \[`500,501,400`]                                                                                                                           |

### HttpCall API endpoints

| HTTP Method | API Endpoint                                    | Request Body    | Response                              |
| ----------- | ----------------------------------------------- | --------------- | ------------------------------------- |
| POST        | `/httpcallsstore/httpcalls`                     | http-call-model | N/A                                   |
| GET         | `/httpcallsstore/httpcalls/descriptors`         | N/A             | list of references to http-call-model |
| DELETE      | `/httpcallsstore/httpcalls/{id}`                | N/A             | N/A                                   |
| GET         | `/httpcallsstore/httpcalls/{id}`                | N/A             | http-call-model                       |
| PUT         | `/httpcallsstore/httpcalls/{id}`                | http-call-model | N/A                                   |
| GET         | `/httpcallsstore/httpcalls/{id}/currentversion` | N/A             | http-call-model                       |
| POST        | `/httpcallsstore/httpcalls/{id}/currentversion` | http-call-model | N/A                                   |

### httpCall Sample

```javascript
{
  "targetServerUrl": "https://api.agent-metrics.com/v1/messages",
  "httpCalls": [
    {
      "name": "sendUserMessageToAnalytics",
      "actions": [
        "send_input_to_analytics"
      ],
      "saveResponse": false,
      "fireAndForget": true,
      "request": {
        "method": "post",
        "queryParams": {
          "token": "<token>"
        },
        "contentType": "application/json",
        "body": "{\"text\": \"{memory.current.input}\",\"message_type\": \"incoming\",\"user_id\": \"{memory.current.userInfo.userId}\",\"platform\": \"eddi\"}"
      }
    },
    {
      "name": "sendAgentMessageToAnalytics",
      "actions": [
        "send_output_to_analytics"
      ],
      "saveResponse": false,
      "fireAndForget": true,
      "preRequest": {
        "batchRequests": {
          "pathToTargetArray": "memory.current.output",
          "iterationObjectName": "output"
        }
      },
      "request": {
        "method": "post",
        "queryParams": {
          "token": "<token>"
        },
        "contentType": "application/json",
        "body": "{\"text\": \"{output}\",\"message_type\": \"outgoing\",\"user_id\": \"{memory.current.userInfo.userId}\",\"platform\": \"eddi\"}"
      },
      "postResponse": {
        "propertyInstructions": [
          {
            "name": "nameOfPropertyToBeUsedInTemplating",
            "value": "StaticValueHereIfFromObjectPathIsNotDefined",
            "scope": "step",
            "fromObjectPath": "savedObjName.something.something",
            "override": true,
            "httpCodeValidator": {
              "runOnHttpCode": [
                200
              ],
              "skipOnHttpCode": [
                0,
                400,
                401,
                402,
                403,
                404,
                409,
                410,
                500,
                501,
                502
              ]
            },
            "qrBuildInstruction": {
              "pathToTargetArray": "savedObjName.data.topics",
              "iterationObjectName": "topic",
              "templateFilterExpression": "${topic.subType} != 'specialSubType'",
              "quickReplyValue": "{topic.name}",
              "quickReplyExpressions": "property(topic_id({topic.id}))"
            }
          }
        ]
      }
    }
  ]
}
```

## Step by step example

We will do a step by step example from scratch (**Agent** creation to a simple conversation that uses `httpCall`)

For the sake of simplicity we will use a free weather API to fetch weather of cities by their names ([api.openweathermap.org](http://api.openweathermap.org/)).

### 1 - Create regularDictionnary

> More about regular dictionaries can be found [here](/getting-started/creating-your-first-agent#1-creating-a-regular-dictionary).

*Request URL*

`POST` `http://localhost:7070/regulardictionarystore/regulardictionaries`

*Request Body*

```javascript
{
  "words": [
    {
      "word": "weather",
      "expressions": "trigger(current_weather)",
      "frequency": 0
    }
  ],
  "phrases": [
    {
      "phrase": "what is the weather",
      "expressions": "trigger(current_weather)"
    },
    {
      "phrase": "whats the weather",
      "expressions": "trigger(current_weather)"
    }
  ]
}
```

*Response Body*

`no content`

*Response Code*

`201`

> The `Location` header contains the resource URI, e.g. `eddi://ai.labs.regulardictionary/regulardictionarystore/regulardictionaries/<id>?version=1`

### 2 - Create the behaviorSet

> More about behaviorSets can be found in [Behavior Rules](/agent-configuration/behavior-rules)

*Request URL*

`POST` `http://localhost:7070/behaviorstore/behaviorsets`

*Response Body*

`no content`

*Response Code*

`201`

*Request Body*

```javascript
{
  "behaviorGroups": [
    {
      "name": "",
      "behaviorRules": [
        {
          "name": "Ask for City",
          "actions": [
            "ask_for_city"
          ],
          "conditions": [
            {
              "type": "inputmatcher",
              "configs": {
                "expressions": "trigger(current_weather)"
              }
            }
          ]
        },
        {
          "name": "Current Weather in City",
          "actions": [
            "current_weather_in_city"
          ],
          "conditions": [
            {
              "type": "inputmatcher",
              "configs": {
                "expressions": "trigger(current_weather)",
                "occurrence": "lastStep"
              }
            }
          ]
        }
      ]
    }
  ]
}
```

> The `Location` header contains the resource URI, e.g. `eddi://ai.labs.behavior/behaviorstore/behaviorsets/<id>?version=1`

### 3 - Create the **httpCall**

Note that we can pass user input to the http call using ***`{memory.current.input}`***

*Request URL*

`POST` `http://localhost:7070/httpcallsstore/httpcalls`

*Request Body*

```javascript
{
  "targetServerUrl": "https://api.openweathermap.org/data/2.5/weather",
  "httpCalls": [
    {
      "name": "currentWeather",
      "saveResponse": true,
      "responseObjectName": "currentWeather",
      "actions": [
        "current_weather_in_city"
      ],
      "request": {
        "path": "",
        "headers": {},
        "queryParams": {
          "APPID": "c3366d78c7c0f76d63eb4cdf1384ddbf",
          "units": "metric",
          "q": "{memory.current.input}"
        },
        "method": "get",
        "contentType": "",
        "body": ""
      }
    }
  ]
}
```

*Response Body*

`no content`

*Response Code*

`201`

> The `Location` header contains the resource URI, e.g. `eddi://ai.labs.httpcalls/httpcallsstore/httpcalls/<id>?version=1`

### 4 - Create the outputSet

> More about outputSet can be found [Output Configuration](/agent-configuration/output-configuration).
>
> Note When you set `"saveResponse" : true` in `httpCall` then you can use `{memory.current.httpCalls.<responseObjectName>}` to access the response data and use Qute ( `{#for}` ) to iterate over `JSON` `arrays` if you have them in your `JSON` response.

*Request URL*

`POST` `http://localhost:7070/outputstore/outputsets`

*Request Body*

```javascript
{
  "outputSet": [
    {
      "action": "ask_for_city",
      "timesOccurred": 0,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "Which City would you like to know the weather of?"
            }
          ]
        }
      ]
    },
    {
      "action": "current_weather_in_city",
      "timesOccurred": 0,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "The current weather situation of {memory.current.input} is {memory.current.httpCalls.currentWeather.weather[0].description} at {memory.current.httpCalls.currentWeather.main.temp} °C"
            }
          ]
        }
      ]
    }
  ]
}
```

*Response Body*

`no content`

*Response Code*

`201`

> The `Location` header contains the resource URI, e.g. `eddi://ai.labs.output/outputstore/outputsets/<id>?version=1`

### 5 - Creating the package

> More about packages can be found [here](/getting-started/creating-your-first-agent#4-creating-the-package).
>
> Important Workflow note
>
> * `ai.labs.httpcalls` & `ai.labs.output` must come after `ai.labs.behavior` in order of the package definition
> * `ai.labs.templating` has to be after `ai.labs.output`

*Request URL*

`POST` `http://localhost:7070/packagestore/packages`

*Request Body*

```javascript
{
  "packageExtensions": [
    {
      "type": "eddi://ai.labs.parser",
      "extensions": {
        "dictionaries": [
          {
            "type": "eddi://ai.labs.parser.dictionaries.integer"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.decimal"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.punctuation"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.email"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.time"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.ordinalNumber"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.regular",
            "config": {
              "uri": "eddi://ai.labs.regulardictionary/regulardictionarystore/regulardictionaries/{{dictionary_id}}?version=1"
            }
          }
        ],
        "corrections": [
          {
            "type": "eddi://ai.labs.parser.corrections.stemming",
            "config": {
              "language": "english",
              "lookupIfKnown": "false"
            }
          },
          {
            "type": "eddi://ai.labs.parser.corrections.levenshtein",
            "config": {
              "distance": "2"
            }
          },
          {
            "type": "eddi://ai.labs.parser.corrections.mergedTerms"
          }
        ]
      },
      "config": {}
    },
    {
      "type": "eddi://ai.labs.behavior",
      "config": {
        "uri": "eddi://ai.labs.behavior/behaviorstore/behaviorsets/{{behaviourset_id}}?version=1"
      }
    },
    {
      "type": "eddi://ai.labs.httpcalls",
      "config": {
        "uri": "eddi://ai.labs.httpcalls/httpcallsstore/httpcalls/{{httpcall_id}}?version=1"
      }
    },
    {
      "type": "eddi://ai.labs.output",
      "config": {
        "uri": "eddi://ai.labs.output/outputstore/outputsets/{{outputset_id}}?version=1"
      }
    },
    {
      "type": "eddi://ai.labs.templating",
      "extensions": {},
      "config": {}
    }
  ]
}
```

Response Body

`no content`

Response Code

`201`

> The `Location` header contains the resource URI, e.g. `eddi://ai.labs.package/packagestore/packages/<id>?version=1`

### 6 - Creating the agent

*Request URL*

`POST` `http://localhost:7070/agentstore/agents`

*Request Body*

```javascript
{
  "packages": [
    "eddi://ai.labs.package/packagestore/packages/{{package_id}}?version=1"
  ],
  "channels": []
}
```

*Response Body*

`no content`

*Response Code*

`201`

> The `Location` header contains the resource URI, e.g. `eddi://ai.labs.agent/agentstore/agents/<id>?version=1`

### 7 - Deploy the agent

*Request URL*

`POST` `http://localhost:7070/administration/production/deploy/**<agent_id>**?version=1&autoDeploy=true`

*Response Body*

`no content`

*Response Code*

`202`

### 8 - Create the conversation

*Request URL*

`POST` `http://localhost:7070/agents/**<env>**/**<agent_id>**`

*Response Body*

`no content`

*Response Code*

`201`

> The `Location` header contains the conversation URI.

### 9 - Say weather

*Request URL*

`POST` `http://localhost:7070/agents/<env>/<agent_id>/<conversation_id>?returnDetailed=false&returnCurrentStepOnly=true`

*Request Body*

```javascript
{
  "input": "weather"
}
```

*Response Body*

```javascript
{
  "agentId": "5af8b075ba31c023bcb9ef3b",
  "agentVersion": 1,
  "environment": "production",
  "conversationState": "READY",
  "redoCacheSize": 0,
  "conversationSteps": [
    {
      "conversationStep": [
        {
          "key": "input:initial",
          "value": "weather"
        },
        {
          "key": "actions",
          "value": [
            "ask_for_city"
          ]
        },
        {
          "key": "output:text:ask_for_city",
          "value": "Which City would you like to know the weather of?"
        }
      ],
      "timestamp": 1526247548410
    }
  ]
}
```

*Response Code*

`200`

### 10 - Say "Vienna"

*Request URL*

`POST` `http://localhost:7070/agents/<env>/<agent_id>/<conversation_id>?returnDetailed=false&returnCurrentStepOnly=true`

*Request Body*

```javascript
{
  "agentId": "5af8b075ba31c023bcb9ef3b",
  "agentVersion": 1,
  "environment": "production",
  "conversationState": "READY",
  "redoCacheSize": 0,
  "conversationSteps": [
    {
      "conversationStep": [
        {
          "key": "input:initial",
          "value": "Vienna"
        },
        {
          "key": "actions",
          "value": [
            "current_weather_in_city"
          ]
        },
        {
          "key": "output:text:current_weather_in_city",
          "value": "The current weather situation of Vienna is clear sky at 17.68 °C"
        }
      ],
      "timestamp": 1526247618080
    }
  ]
}
```

*Response Code*

`200`

## Full Example

Download the [Weather Agent Postman Collection](https://github.com/labsai/EDDI/tree/main/docs/.gitbook/assets/EDDI%20-%20Weather%20bot.postman_collection.json) to run the full example.


# LLM Integration

**Version: 6.2.0**

## Overview

The **LLM Lifecycle Task** (formerly "Langchain") is EDDI's unified integration point for Large Language Models (LLMs).

By default, it provides **simple chat** with any LLM provider. Optionally, you can enable **agent mode** to give your LLM access to built-in tools (calculator, web search, weather, etc.).

EDDI supports **12 LLM providers** out of the box: OpenAI, Anthropic, Google Gemini, Mistral AI, Azure OpenAI, Amazon Bedrock, Oracle GenAI, Ollama, Hugging Face, and Jlama — plus any OpenAI-compatible endpoint (DeepSeek, Cohere, etc.) via the `baseUrl` parameter.

The task automatically detects which mode to use based on your configuration—no manual switching required.

***

## EDDI's Value Proposition for LLMs

EDDI doesn't just forward messages to LLMs—it **orchestrates** them:

1. **Conditional LLM Invocation**: Use Behavior Rules to decide whether to call an LLM based on user input, context, or conversation state
2. **Pre-processing**: Parse, normalize, and enrich user input before sending to the LLM
3. **Context Management**: Control exactly what conversation history and context data is sent to the LLM
4. **Multi-LLM Support**: Switch between different LLMs (OpenAI, Claude, Gemini, Ollama, Hugging Face, Jlama) based on rules or user preferences
5. **Post-processing**: Transform, validate, or augment LLM responses before sending to users
6. **Hybrid Workflows**: Combine LLM calls with traditional APIs (e.g., LLM generates query → API fetches data → LLM formats result)
7. **State Persistence**: All LLM interactions are logged in conversation memory for analytics and debugging
8. **Tool Calling**: Enable LLMs to use built-in tools or custom HTTP call tools to access external capabilities

***

## Role in the Lifecycle

The Langchain task is a lifecycle task that executes when triggered by Behavior Rules:

```
User Input → Parser → Behavior Rules → [LangChain Task] → Output Generation
                           ↓
                    Action: "send_to_llm"
```

***

## Supported LLM Providers

The Langchain task integrates with multiple LLM providers via the langchain4j library:

* **OpenAI** (ChatGPT, GPT-4, GPT-4o) — also supports **DeepSeek** and **Cohere** via `baseUrl`
* **Anthropic** (Claude)
* **Google Gemini** (`gemini` - AI Studio API, `gemini-vertex` - Vertex AI)
* **Mistral AI** (Mistral Large, Codestral, Pixtral)
* **Azure OpenAI** (GPT-4o via Azure-hosted endpoints)
* **Amazon Bedrock** (Claude, Llama, Titan via AWS credential chain)
* **Oracle GenAI** (Cohere Command R+ via OCI authentication)
* **Ollama** (Local models)
* **Hugging Face** (Various models)
* **Jlama** (Local Java-based inference)

**Note**: Use the "Agent Father" agent to streamline setup and configuration of the Langchain task with guided assistance.

***

## Configuration Modes

### Default: Simple Chat

By default, the Langchain task provides straightforward LLM chat functionality. Just configure your LLM provider and start chatting.

### Optional: Agent Mode with Tools

To give your LLM access to tools (calculator, web search, weather, etc.), set `enableBuiltInTools: true` in your configuration.

The task automatically switches to agent mode when tools are enabled.

**Note**: Custom HTTP call tools (via the `tools` parameter) are also supported. You can provide a list of EDDI HTTP call URIs to give the agent access to your own APIs.

***

## Simple Chat Configuration

This is the standard way to use the Langchain task - just connect to an LLM and start chatting.

### Basic Example

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "simpleChat",
      "type": "openai",
      "description": "Simple chat interaction",
      "parameters": {
        "apiKey": "your-api-key",
        "modelName": "gpt-4o",
        "systemMessage": "You are a helpful assistant",
        "prompt": "",
        "logSizeLimit": "-1",
        "includeFirstAgentMessage": "true",
        "convertToObject": "false",
        "addToOutput": "true"
      }
    }
  ]
}
```

### Configuration Parameters

| Parameter                  | Type    | Description                                                                                                                                                                                          | Default           |
| -------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| **Core Parameters**        |         |                                                                                                                                                                                                      |                   |
| `apiKey`                   | string  | API key for the LLM provider                                                                                                                                                                         | Required          |
| `modelName`                | string  | Model identifier (e.g., "gpt-4o", "Claude")                                                                                                                                                          | Provider-specific |
| `systemMessage`            | string  | System message for LLM context                                                                                                                                                                       | ""                |
| `prompt`                   | string  | Override user input (if not set, uses actual input)                                                                                                                                                  | ""                |
| **Context Control**        |         |                                                                                                                                                                                                      |                   |
| `logSizeLimit`             | int     | Conversation history limit                                                                                                                                                                           | -1 (unlimited)    |
| `includeFirstAgentMessage` | boolean | Include first agent message in context                                                                                                                                                               | true              |
| **Output Control**         |         |                                                                                                                                                                                                      |                   |
| `convertToObject`          | boolean | Parse response as JSON. Enables three-layer enforcement: system prompt reinforcement, native API JSON mode (see the [provider matrix](#native-json-mode--provider-matrix)), and pre-parse validation | false             |
| `responseSchema`           | string  | JSON schema for structured output. When set with `convertToObject=true`, the exact schema is injected into the system prompt so the LLM knows the expected format                                    | ""                |
| `addToOutput`              | boolean | Add response to conversation output                                                                                                                                                                  | false             |
| **Logging**                |         |                                                                                                                                                                                                      |                   |
| `logRequests`              | boolean | Log API requests (sync and streaming)                                                                                                                                                                | false             |
| `logResponses`             | boolean | Log API responses (sync and streaming)                                                                                                                                                               | false             |
| **API Configuration**      |         |                                                                                                                                                                                                      |                   |
| `temperature`              | string  | Model temperature (0-1)                                                                                                                                                                              | Provider-specific |
| `maxTokens`                | string  | Maximum output tokens per response — see [Output Token Limits](#output-token-limits)                                                                                                                 | Provider-specific |
| `timeout`                  | string  | Request timeout (milliseconds) — see [Timeouts](#timeouts-and-streaming)                                                                                                                             | Provider-specific |

> **These three settings are part of a model's identity.** Two tasks that differ only in `timeout`, `logRequests` or `logResponses` get two separate cached model instances, so a task always runs with the settings it declares regardless of which task was constructed first.
>
> **Logging is EDDI's, not the provider's.** `logRequests`/`logResponses` are honoured by EDDI's own model decorators, which truncate the logged request to 200 and the logged response to 500 characters. They are deliberately **not** forwarded to the provider builders: langchain4j's client-level logging writes whole request and response bodies at INFO with no truncation, so switching it on would put full prompts, full conversation history and full model output into the application log. (`logRequestsAndResponses`, which only the Azure OpenAI and Gemini builders accept, is a provider-level escape hatch and *is* still forwarded — use it only where that exposure is acceptable.)
>
> **An unusable `timeout` is ignored, not fatal.** The value is normalised once before it reaches a provider builder: it is trimmed, and dropped entirely when it is blank, non-numeric (`"30s"`) or non-positive (`"0"`, historically "no timeout"), with a WARN naming the model type. Provider builders parse the value with an unguarded `Long.parseLong`, so without this a stored config carrying one of those values would fail on every turn. `" 5000 "` and `"5000"` are the same timeout and share one cached model.

### Output Token Limits

The `maxTokens` parameter controls the **maximum number of output tokens** the LLM can generate per response. This is a ceiling, not a target — the model generates only what it needs and stops. Setting it higher does not increase cost unless the model actually produces more tokens; the `timeout` parameter is the real cost safety net.

> \[!WARNING] **Anthropic + Extended Thinking**: Models with extended thinking (e.g. `claude-sonnet-5`, `claude-sonnet-4`) count **thinking tokens** toward `maxTokens`. If the limit is too low, thinking can consume the entire budget, leaving **zero tokens for the actual response** — resulting in empty/null output. This is a silent failure: the API returns successfully, token usage shows consumption, but `text()` is `null`.

#### Provider Limits and EDDI Defaults

| Provider         | Config Key        | Max Output Capability                               | EDDI Default (if omitted) | Notes                                           |
| ---------------- | ----------------- | --------------------------------------------------- | ------------------------- | ----------------------------------------------- |
| **Anthropic**    | `maxTokens`       | 8,192 (standard) / 128,000 (with extended thinking) | **16,384**                | Required by API. Set higher for thinking models |
| **Gemini**       | `maxOutputTokens` | 65,536                                              | Provider SDK default      | Use `maxOutputTokens` (not `maxTokens`)         |
| **OpenAI**       | `maxTokens`       | 4,096–16,384 (model-dependent)                      | Provider SDK default      |                                                 |
| **Azure OpenAI** | `maxTokens`       | Same as OpenAI                                      | Provider SDK default      |                                                 |
| **Bedrock**      | `maxTokens`       | Model-dependent                                     | Provider SDK default      |                                                 |
| **Mistral**      | `maxTokens`       | 32,768                                              | Provider SDK default      |                                                 |
| **Oracle GenAI** | `maxTokens`       | Model-dependent                                     | Provider SDK default      |                                                 |

#### Recommended Settings

For most conversational use cases:

```json
"maxTokens": "16384"
```

For complex analysis, multi-step reasoning, or models with extended thinking:

```json
"maxTokens": "32768"
```

For maximum output (e.g. long-form document generation):

```json
"maxTokens": "65536"
```

### Timeouts and Streaming

Two settings bound an LLM call, and they are deliberately distinct:

| Setting                                | Where                               | Unit | What it bounds                                                                                                                                                                                                                                                                                                    |
| -------------------------------------- | ----------------------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `timeout` (`parameters`)               | model parameter                     | ms   | The provider call. On a non-streaming task it bounds the whole request. On a **streaming** task it is the provider HTTP client's request/read timeout — for the JDK client, the time until the provider's first response, so it catches a provider that never answers without truncating one that answers slowly. |
| `streamingTimeoutSeconds` (task field) | task-level, sibling of `parameters` | s    | The **overall** wall-clock backstop for the whole stream, for providers whose native timeout does not fire (or does not exist). Streaming only.                                                                                                                                                                   |

How the backstop is resolved:

1. An explicit positive `streamingTimeoutSeconds` always wins.
2. Otherwise the backstop is **120s**, raised (never lowered) to cover a longer explicitly configured `timeout`. So `timeout: "300000"` with no `streamingTimeoutSeconds` yields a 300s backstop rather than being cut short at 120s; any `timeout` at or below 120s leaves the 120s default untouched.
3. Otherwise 120s.

Both stored shapes therefore keep working: a config that sets only `streamingTimeoutSeconds` behaves exactly as before, and a config that sets only `timeout` is now honoured on the streaming path instead of being discarded.

```json
{
  "actions": ["send_message"],
  "id": "longRunning",
  "type": "openai",
  "streamingTimeoutSeconds": 300,
  "parameters": {
    "apiKey": "your-openai-api-key",
    "modelName": "gpt-4o",
    "timeout": "300000"
  }
}
```

> `timeout` is read from the task's stored parameters when deriving the backstop. A Qute-templated value (e.g. `"{vars.llm-timeout}"`) cannot be resolved at that point and simply leaves the 120s default in place — it never produces a shorter bound.

### Provider-Specific Examples

#### OpenAI

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "openaiChat",
      "type": "openai",
      "description": "OpenAI GPT-4o chat",
      "parameters": {
        "apiKey": "your-openai-api-key",
        "modelName": "gpt-4o",
        "temperature": "0.7",
        "timeout": "15000",
        "logRequests": "true",
        "logResponses": "true",
        "systemMessage": "You are a helpful assistant",
        "addToOutput": "true"
      }
    }
  ]
}
```

#### Anthropic Claude

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "claudeChat",
      "type": "anthropic",
      "description": "Anthropic Claude chat",
      "parameters": {
        "apiKey": "your-anthropic-api-key",
        "modelName": "claude-sonnet-4-20250514",
        "temperature": "0.7",
        "maxTokens": "16384",
        "timeout": "60000",
        "systemMessage": "You are a helpful assistant",
        "includeFirstAgentMessage": "false",
        "addToOutput": "true"
      }
    }
  ]
}
```

> **Important**: Anthropic doesn't allow the first message to be from the agent, so `includeFirstAgentMessage` should be set to `false`.
>
> **maxTokens**: Anthropic requires `max_tokens` in every request. If omitted, EDDI defaults to **16384**. For models with **extended thinking** (e.g. `claude-sonnet-5`), thinking tokens count toward this budget — set it higher (e.g. `"32768"` or `"65536"`) for complex analysis tasks.

#### Google Gemini (Vertex AI)

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "geminiChat",
      "type": "gemini",
      "description": "Google Gemini chat",
      "parameters": {
        "publisher": "vertex-ai",
        "projectId": "your-project-id",
        "modelId": "gemini-pro",
        "temperature": "0.7",
        "timeout": "15000",
        "systemMessage": "You are a helpful assistant",
        "addToOutput": "true"
      }
    }
  ]
}
```

#### Ollama (Local Models)

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "ollamaChat",
      "type": "ollama",
      "description": "Ollama local model chat",
      "parameters": {
        "model": "llama3",
        "timeout": "15000",
        "systemMessage": "You are a helpful assistant",
        "addToOutput": "true"
      }
    }
  ]
}
```

#### Hugging Face

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "huggingfaceChat",
      "type": "huggingface",
      "description": "Hugging Face model chat",
      "parameters": {
        "accessToken": "your-huggingface-access-token",
        "modelId": "llama3",
        "temperature": "0.7",
        "timeout": "15000",
        "systemMessage": "You are a helpful assistant",
        "addToOutput": "true"
      }
    }
  ]
}
```

#### Jlama (Local Java Inference)

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "jlamaChat",
      "type": "jlama",
      "description": "Jlama local model chat",
      "parameters": {
        "modelName": "tjake/Llama-3.2-1B-Instruct-JQ4",
        "temperature": "0.7",
        "timeout": "30000",
        "systemMessage": "You are a helpful assistant",
        "addToOutput": "true"
      }
    }
  ]
}
```

**Note**: Jlama runs models locally in Java without requiring external services like Ollama.

#### Mistral AI

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "mistralChat",
      "type": "mistral",
      "description": "Mistral AI chat",
      "parameters": {
        "apiKey": "your-mistral-api-key",
        "modelName": "mistral-large-latest",
        "temperature": "0.7",
        "timeout": "15000",
        "systemMessage": "You are a helpful assistant",
        "addToOutput": "true"
      }
    }
  ]
}
```

#### Azure OpenAI

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "azureChat",
      "type": "azure-openai",
      "description": "Azure OpenAI chat",
      "parameters": {
        "apiKey": "your-azure-api-key",
        "deploymentName": "gpt-4o",
        "endpoint": "https://your-instance.openai.azure.com",
        "temperature": "0.7",
        "timeout": "15000",
        "systemMessage": "You are a helpful assistant",
        "addToOutput": "true"
      }
    }
  ]
}
```

**Note**: Azure OpenAI uses `deploymentName` (not `modelName`) and requires an `endpoint` URL for your Azure instance.

#### Amazon Bedrock

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "bedrockChat",
      "type": "bedrock",
      "description": "Amazon Bedrock chat",
      "parameters": {
        "modelId": "anthropic.claude-v2",
        "region": "us-east-1",
        "temperature": "0.7",
        "maxTokens": "16384",
        "timeout": "30000",
        "systemMessage": "You are a helpful assistant",
        "addToOutput": "true"
      }
    }
  ]
}
```

**Note**: Bedrock uses `modelId` (not `modelName`) and does not require an `apiKey`. Authentication is via the AWS SDK default credential chain (environment variables, IAM roles, `~/.aws/credentials`).

#### Oracle GenAI

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "oracleChat",
      "type": "oracle-genai",
      "description": "Oracle GenAI chat",
      "parameters": {
        "modelName": "cohere.command-r-plus",
        "compartmentId": "ocid1.compartment.oc1..your-compartment-id",
        "configProfile": "DEFAULT",
        "temperature": "0.7",
        "maxTokens": "16384",
        "systemMessage": "You are a helpful assistant",
        "addToOutput": "true"
      }
    }
  ]
}
```

**Note**: Oracle GenAI does not require an `apiKey`. Authentication is via OCI SDK (`~/.oci/config`). The `configProfile` parameter selects which OCI profile to use (defaults to `"DEFAULT"`).

#### DeepSeek / Cohere (via OpenAI-Compatible Endpoints)

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "deepseekChat",
      "type": "openai",
      "description": "DeepSeek via OpenAI-compatible endpoint",
      "parameters": {
        "apiKey": "your-deepseek-api-key",
        "modelName": "deepseek-chat",
        "baseUrl": "https://api.deepseek.com",
        "temperature": "0.7",
        "systemMessage": "You are a helpful assistant",
        "addToOutput": "true"
      }
    }
  ]
}
```

**Note**: Any OpenAI-compatible provider (DeepSeek, Cohere, etc.) can be used by setting the `baseUrl` parameter on the `openai` type. No additional dependencies are required.

***

## Agent Mode (Enhanced Features)

### AI Agent with Built-in Tools

```json
{
  "tasks": [
    {
      "actions": ["help"],
      "id": "aiAgent",
      "type": "openai",
      "description": "AI agent with calculator and web search",
      "parameters": {
        "apiKey": "your-api-key",
        "modelName": "gpt-4o",
        "systemMessage": "You are a helpful assistant with access to tools."
      },
      "enableBuiltInTools": true,
      "builtInToolsWhitelist": ["calculator", "datetime", "websearch"],
      "conversationHistoryLimit": 10
    }
  ]
}
```

### Agent Mode Parameters

| Parameter                  | Type      | Description                                                                                                                                                                                                                                                             | Default                                        |
| -------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| **Tool Configuration**     |           |                                                                                                                                                                                                                                                                         |                                                |
| `enableBuiltInTools`       | boolean   | Enable built-in tools                                                                                                                                                                                                                                                   | false                                          |
| `builtInToolsWhitelist`    | string\[] | Specific tools to enable                                                                                                                                                                                                                                                | (all if not specified)                         |
| `tools`                    | string\[] | Custom HTTP call tool URIs to enable                                                                                                                                                                                                                                    | (none)                                         |
| **Context Control**        |           |                                                                                                                                                                                                                                                                         |                                                |
| `conversationHistoryLimit` | int       | Max conversation turns in context                                                                                                                                                                                                                                       | 10                                             |
| `maxToolContextTokens`     | int       | Aggregate token ceiling on the **in-turn** tool-call context (tool requests + tool results across all loop iterations). The oldest complete tool exchange is evicted when exceeded. `-1`/`0` disables. See [In-Turn Tool Context Budget](#in-turn-tool-context-budget). | 60000                                          |
| **Cost & Performance**     |           |                                                                                                                                                                                                                                                                         |                                                |
| `maxBudgetPerConversation` | number    | Ceiling on accumulated **tool** cost per conversation, in USD. Records cost; only refuses calls when `enforceBudget` is on                                                                                                                                              | (unlimited)                                    |
| `enforceBudget`            | boolean   | Refuse tool calls once `maxBudgetPerConversation` is passed. **Opt-in** — a ceiling without it is report-only, and is named in a startup WARN                                                                                                                           | false (`eddi.tools.budget.enforce-by-default`) |
| `toolPricing`              | map       | Per-call tool prices in USD. Keyed on the built-in slug (`{"websearch": 0.005}`) or on a single dispatch name (`{"searchNews": 0.01}`), which takes precedence — so one operation can be priced apart from its siblings                                                 | (built-in defaults)                            |
| `enableToolCaching`        | boolean   | Cache tool results to reduce API calls                                                                                                                                                                                                                                  | true                                           |
| `toolCacheScopes`          | map       | Per-tool cache partition: `user`/`conversation`/`global`                                                                                                                                                                                                                | (all `user`)                                   |
| `defaultToolCacheScope`    | string    | Cache partition for tools without an override                                                                                                                                                                                                                           | `user`                                         |
| `enableRateLimiting`       | boolean   | Limit tool/LLM usage rate                                                                                                                                                                                                                                               | true                                           |

### Behavioral Safety (Counterweight & Identity Masking)

EDDI provides two per-task safety mechanisms that are injected into the system prompt before sending it to the LLM. Both must be explicitly enabled with `"enabled": true` — they are off by default.

#### Behavioral Counterweight

Counterweights append behavioral safety instructions to the system prompt. Three preset levels are available:

| Level      | Effect                                                                                                       |
| ---------- | ------------------------------------------------------------------------------------------------------------ |
| `normal`   | No-op — no safety instructions added (default)                                                               |
| `cautious` | Adds guidelines for careful responses, hedging on uncertain topics, and suggesting professional consultation |
| `strict`   | Adds stronger instructions: refuse harmful content, flag uncertainty, always suggest human oversight         |

**Auto-downgrade**: When an agent runs via the `scheduled` channel (e.g., `ScheduleFireExecutor`), `strict` is automatically downgraded to `cautious` to prevent overly rigid responses in automated pipelines.

**Configuration**:

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "type": "openai",
      "parameters": { "apiKey": "...", "modelName": "gpt-4o" },
      "counterweight": {
        "enabled": true,
        "level": "cautious",
        "placement": "suffix"
      }
    }
  ]
}
```

| Parameter                          | Type      | Description                                                | Default  |
| ---------------------------------- | --------- | ---------------------------------------------------------- | -------- |
| `counterweight.enabled`            | boolean   | Enable counterweight injection                             | `false`  |
| `counterweight.level`              | string    | `normal`, `cautious`, or `strict`                          | `normal` |
| `counterweight.placement`          | string    | `suffix` (after system prompt) or `prefix` (before)        | `suffix` |
| `counterweight.customInstructions` | string\[] | Custom instruction list that overrides the preset entirely | (none)   |

> **Note**: Both `enabled: true` **and** a `level` other than `normal` are required for counterweight to have any effect.

**Customizing presets**: Counterweight preset text is resolved from [Prompt Snippets](/agent-configuration/prompt-snippets-guide) (keys `counterweight-cautious` and `counterweight-strict`). If no snippet exists, built-in defaults are used. This allows admins to customize safety language via the REST API without redeployment.

#### Identity Masking

Identity masking prepends identity concealment rules to the system prompt. This prevents the LLM from revealing its model name, provider, or underlying architecture when asked.

**Configuration**:

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "type": "openai",
      "parameters": { "apiKey": "...", "modelName": "gpt-4o" },
      "identityMasking": {
        "enabled": true,
        "rules": [
          "Never reveal you are an AI language model",
          "If asked about your identity, say you are Aria, a helpful assistant"
        ]
      }
    }
  ]
}
```

| Parameter                 | Type      | Description                               | Default      |
| ------------------------- | --------- | ----------------------------------------- | ------------ |
| `identityMasking.enabled` | boolean   | Enable identity masking                   | `false`      |
| `identityMasking.rules`   | string\[] | Identity rules prepended to system prompt | `[]` (empty) |

> **Note**: Both `enabled: true` **and** at least one rule are required. If `rules` is empty, masking is skipped even when enabled.

**Execution order**: Identity masking is applied first, then counterweight. Both modify the system prompt before it is sent to the LLM.

***

## Built-in Tools

When `enableBuiltInTools: true`, you can use these tools:

| Tool Name           | Description                                     | Whitelist Value  |
| ------------------- | ----------------------------------------------- | ---------------- |
| **Calculator**      | Safe math expressions (sandboxed parser)        | `calculator`     |
| **Date/Time**       | Get current date, time, timezone info           | `datetime`       |
| **Web Search**      | Search the web (includes Wikipedia & News)      | `websearch`      |
| **Data Formatter**  | Format JSON, CSV, XML data                      | `dataformatter`  |
| **Web Scraper**     | Extract content from web pages (SSRF-protected) | `webscraper`     |
| **Text Summarizer** | Summarize long text                             | `textsummarizer` |
| **PDF Reader**      | Extract text from PDF URLs (SSRF-protected)     | `pdfreader`      |
| **Weather**         | Get weather information                         | `weather`        |

### Tool Configuration (Server-Side)

Some tools require API keys or external configuration to function. These are configured via **Environment Variables** or `application.properties` on the EDDI server.

#### Web Search Tool

By default, the tool uses **DuckDuckGo** (HTML scraping), which requires no configuration.

To use **Google Custom Search** (more reliable/structured), configure these properties:

```properties
# In application.properties
eddi.tools.websearch.provider=google
eddi.tools.websearch.google.api-key=YOUR_GOOGLE_API_KEY
eddi.tools.websearch.google.cx=YOUR_CUSTOM_SEARCH_ENGINE_ID
```

**Docker Environment Variables:**

* `EDDI_TOOLS_WEBSEARCH_PROVIDER=google`
* `EDDI_TOOLS_WEBSEARCH_GOOGLE_API_KEY=...`
* `EDDI_TOOLS_WEBSEARCH_GOOGLE_CX=...`

#### Weather Tool

The weather tool uses **OpenWeatherMap**. You must provide an API key:

```properties
# In application.properties
eddi.tools.weather.openweathermap.api-key=YOUR_OWM_API_KEY
```

**Docker Environment Variables:**

* `EDDI_TOOLS_WEATHER_OPENWEATHERMAP_API_KEY=...`

### Example: Selective Tool Enablement

```json
{
  "enableBuiltInTools": true,
  "builtInToolsWhitelist": ["calculator", "datetime", "websearch"]
}
```

This enables **only** calculator, datetime, and websearch tools.

### Example: Enable All Tools

```json
{
  "enableBuiltInTools": true
}
```

Omitting `builtInToolsWhitelist` enables all available built-in tools.

***

## Custom HTTP Tools

In addition to built-in tools, you can give your agent access to any configured EDDI HTTP call. This allows the agent to interact with your own APIs or third-party services.

### Configuration

To enable custom tools, add the `tools` property to your task configuration with a list of HTTP call URIs.

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "type": "openai",
      "parameters": {
        "apiKey": "...",
        "modelName": "gpt-4o"
      },
      "enableBuiltInTools": true,
      "tools": [
        "eddi://ai.labs.httpcalls/get_stock_price?version=1",
        "eddi://ai.labs.httpcalls/create_jira_ticket?version=1"
      ]
    }
  ]
}
```

### How it Works

1. **Configuration**: You provide the URIs of the HTTP calls you want the agent to use.
2. **Discovery**: The agent is automatically informed about these tools and how to use them.
3. **Execution**: When the agent decides to use a tool, it calls the `executeHttpCall` function with the tool's URI and necessary arguments.
4. **Security**: The agent can **only** execute the HTTP calls explicitly listed in the `tools` array. It cannot make arbitrary HTTP requests to the internet.

***

## Extended Configuration Options

The Langchain task supports advanced pre-request and post-response processing for fine-tuned control over task behavior.

### Complete Configuration Example

```json
{
  "tasks": [
    {
      "id": "advancedTask",
      "type": "openai",
      "description": "Task with pre/post processing",
      "actions": ["process_input"],
      "preRequest": {
        "propertyInstructions": [
          {
            "name": "userContext",
            "valueString": "premium_user",
            "scope": "conversation"
          }
        ]
      },
      "parameters": {
        "apiKey": "your-api-key",
        "modelName": "gpt-4o",
        "systemMessage": "You are a helpful assistant",
        "addToOutput": "false"
      },
      "postResponse": {
        "propertyInstructions": [
          {
            "name": "lastResponseTime",
            "valueString": "{{currentTimestamp}}",
            "scope": "conversation"
          }
        ],
        "outputBuildInstructions": [
          {
            "pathToTargetArray": "response.suggestions",
            "iterationObjectName": "item",
            "outputType": "text",
            "outputValue": "{{item.text}}"
          }
        ],
        "qrBuildInstructions": [
          {
            "pathToTargetArray": "response.quickReplies",
            "iterationObjectName": "reply",
            "quickReplyValue": "{{reply.text}}",
            "quickReplyExpressions": "{{reply.action}}"
          }
        ]
      }
    }
  ]
}
```

### Configuration Parameters Explained

#### Pre-Request Configuration

* **preRequest.propertyInstructions**: Defines properties to be set before making the request to the LLM API
  * **name**: The property name
  * **valueString**: The value to be assigned (supports templating)
  * **scope**: The scope of the property (`step`, `conversation`, `longTerm`)

#### Post-Response Configuration

* **postResponse.propertyInstructions**: Defines properties to be set based on the LLM response
  * **name**: The property name
  * **valueString**: The value to be assigned (supports templating)
  * **scope**: The scope of the property
* **postResponse.outputBuildInstructions**: Configures how the response should be transformed into output (alternative to `addToOutput`)
  * **pathToTargetArray**: The path to the array in the response
  * **iterationObjectName**: The name of the object for iterating
  * **outputType**: The type of output to generate
  * **outputValue**: The value to be used for output (supports templating)
* **postResponse.qrBuildInstructions**: Configures quick replies based on the response
  * **pathToTargetArray**: The path to the quick replies array
  * **iterationObjectName**: The name of the object for iterating
  * **quickReplyValue**: The value for the quick reply (supports templating)
  * **quickReplyExpressions**: The expressions for the quick reply

#### Response Metadata

* **responseObjectName**: Name for storing the full response object in memory
* **responseMetadataObjectName**: Name for storing response metadata (token usage, finish reason) in memory

## Conversation Window Management

EDDI provides two modes for controlling how much conversation history is sent to the LLM:

### Step-Count Window (Default)

The default mode uses `conversationHistoryLimit` (or `logSizeLimit` parameter) to include the last N conversation steps. This is simple and backward compatible.

### Token-Aware Window with Anchored Opening

For production workloads where token costs matter, EDDI supports **token-budget windowing** that also **anchors the first N steps** to preserve the opening context.

```
[System prompt]
[Turn 1: user's opening message]        ← anchored (always included)
[Turn 1: agent's opening response]      ← anchored (always included)
[... turns 3-40 omitted ...]            ← gap marker
[Turn 41: user message]                 ← recent window (fills remaining budget)
[Turn 42: agent response]               ← recent window
[Turn 43: user message]                 ← current
```

#### Configuration

| Parameter          | Type | Description                                                                                   | Default |
| ------------------ | ---- | --------------------------------------------------------------------------------------------- | ------- |
| `maxContextTokens` | int  | Maximum token budget for conversation history (excluding system prompt). -1 = use step count. | -1      |
| `anchorFirstSteps` | int  | Number of opening conversation steps to always include regardless of window position.         | 2       |

#### Example

```json
{
  "tasks": [
    {
      "actions": ["*"],
      "id": "costAwareAgent",
      "type": "openai",
      "parameters": {
        "apiKey": "your-api-key",
        "modelName": "gpt-4o",
        "systemMessage": "You are a project planner."
      },
      "enableBuiltInTools": true,
      "maxContextTokens": 4000,
      "anchorFirstSteps": 2
    }
  ]
}
```

This agent:

* Uses at most **4000 tokens** of conversation history (excluding the system prompt)
* **Always includes** the first 2 conversation steps (the user's initial requirements)
* Fills the remaining budget with the most recent messages
* Inserts a gap marker between anchored and recent messages when turns are omitted

#### Token Counting

* **OpenAI / Azure OpenAI**: Uses tiktoken-based tokenizer (accurate, model-specific)
* **All other providers**: Uses an approximate tokenizer (characters ÷ 4)

When `maxContextTokens` is -1 (default), the existing `conversationHistoryLimit` step-count behavior applies. **Full backward compatibility is guaranteed.**

### In-Turn Tool Context Budget

`maxContextTokens` and `conversationHistoryLimit` bound the **conversation history** — the turns already on the record. They do **not** bound the messages a single tool-using turn accumulates *while it runs*. Inside one turn the agent loop appends the model's tool-call request and every tool result, iteration after iteration, up to `maxToolIterations`. Verbose tools (web scrapes, full PDF dumps, raw API bodies) can push that in-turn context past the model's context window and hard-fail the whole turn with a provider `400` — mid-loop, after the tool side effects have already happened. Per-tool `toolResponseLimits` help only when they are configured; they have no default, so an ordinary agent runs unbounded.

`maxToolContextTokens` puts an **aggregate** ceiling on that in-turn tool traffic:

* It counts only tool traffic — every `AiMessage` that carries tool-call requests plus its `ToolExecutionResultMessage`s, summed across all iterations of this turn (and across a HITL pause, which replays the same transcript). System, user and assistant-prose messages are never counted or touched here — that is what `maxContextTokens` governs.
* When the ceiling is exceeded, the **oldest complete tool exchange** — a requesting `AiMessage` **together with all of its results** — is dropped before the next model call, repeatedly, until the traffic fits. Requests and their results are always evicted together: dropping one without the other leaves a dangling `tool_call_id` that itself provokes the `400` the budget exists to prevent.
* The **most recent** exchange is never evicted. If it alone exceeds the ceiling the request is sent unchanged (the model asked for those results and must see them) and the overrun is logged — reach for `toolResponseLimits` or a lower `maxToolIterations` in that case.
* The same token estimator used for conversation windowing is reused, so a budget expressed in tokens means the same thing in both halves of the request (tiktoken for OpenAI/Azure, characters ÷ 4 elsewhere).

**Default: `60000`.** High enough that no ordinary tool-using turn is ever touched — the guard is byte-for-byte inert below the ceiling, so agents that work today are unaffected — and low enough to keep a runaway loop inside a 128k context window once the system prompt, the conversation history and the model's own completion are added. Set `-1` (or `0`) to disable the guard and restore the pre-6.1 unbounded behaviour.

**Observability.** Eviction is never silent: it emits a `tool_context_evicted` entry in the execution trace (with token counts before/after, exchanges and messages dropped, and whether the result is within budget), increments the `eddi.llm.tool_context.evictions` counter (tagged `outcome=within_budget|still_over_budget`), and logs a `WARN` (`llm.tool_context.evicted`) carrying the conversation id and the remediation hint. Because eviction removes tool results the model can no longer see, treat a steady stream of these as a signal to lower `maxToolIterations`, set `toolResponseLimits`, or raise `maxToolContextTokens`.

```json
{
  "type": "LANGCHAIN",
  "parameters": { "modelName": "gpt-4o" },
  "enableBuiltInTools": true,
  "builtInToolsWhitelist": ["websearch", "webscraper"],
  "maxToolIterations": 10,
  "maxToolContextTokens": 60000
}
```

***

## API Endpoints

The Langchain task configurations can be managed via REST API endpoints.

### Endpoints Overview

1. **Read JSON Schema**
   * **Endpoint:** `GET /langchainstore/langchains/jsonSchema`
   * **Description:** Retrieves the JSON schema for validating Langchain configurations
2. **List Langchain Descriptors**
   * **Endpoint:** `GET /langchainstore/langchains/descriptors`
   * **Description:** Returns a list of all Langchain configurations with optional filters
3. **Read Langchain Configuration**
   * **Endpoint:** `GET /langchainstore/langchains/{id}`
   * **Description:** Fetches a specific Langchain configuration by its ID
4. **Update Langchain Configuration**
   * **Endpoint:** `PUT /langchainstore/langchains/{id}`
   * **Description:** Updates an existing Langchain configuration
5. **Create Langchain Configuration**
   * **Endpoint:** `POST /langchainstore/langchains`
   * **Description:** Creates a new Langchain configuration
6. **Duplicate Langchain Configuration**
   * **Endpoint:** `POST /langchainstore/langchains/{id}`
   * **Description:** Duplicates an existing Langchain configuration
7. **Delete Langchain Configuration**
   * **Endpoint:** `DELETE /langchainstore/langchains/{id}`
   * **Description:** Deletes a specific Langchain configuration

***

## Tool Execution Pipeline

All tool invocations—both built-in tools and custom HTTP call tools—are routed through a unified **Tool Execution Service** that applies enterprise-grade controls:

```
Tool Call ──▶ Rate Limiter ──▶ Cache Check ──▶ Execute Tool ──▶ Cost Tracker ──▶ Result
```

### Controls

| Feature           | Description                                                                                     | Config Key                                                                       |
| ----------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| **Rate Limiting** | Token-bucket per tool, configurable limits                                                      | `enableRateLimiting`, `defaultRateLimit`, `toolRateLimits`                       |
| **Smart Caching** | Deduplicates identical tool calls, partitioned per identity                                     | `enableToolCaching`, `toolCacheScopes`, `defaultToolCacheScope`                  |
| **Cost Tracking** | Per-conversation tool-cost accounting, with an opt-in ceiling and automatic stale-data eviction | `enableCostTracking`, `toolPricing`, `maxBudgetPerConversation`, `enforceBudget` |

#### Tool names: dispatch name vs. configuration slug

A built-in tool has **two** names, and knowing which one a setting expects is the difference between a rule that binds and one that is silently ignored:

* the **slug** — the token you write in `builtInToolsWhitelist` (`websearch`, `calculator`, `datetime`, …). This is a property of the *tool*.
* the **dispatch name** — the `@Tool` method the model actually calls (`searchWeb`, `searchNews`, `searchWikipedia` all belong to `websearch`). This is a property of the individual *operation*.

| Setting                         | Accepted keys                                     |
| ------------------------------- | ------------------------------------------------- |
| `builtInToolsWhitelist`         | slug only                                         |
| `toolRateLimits`                | slug **or** dispatch name — dispatch name wins    |
| `toolPricing`                   | slug **or** dispatch name — dispatch name wins    |
| `toolCacheScopes`               | slug **or** dispatch name — dispatch name wins    |
| `toolApprovals`                 | dispatch name, optionally `source:name`-qualified |
| cache TTL, default price        | slug (resolved automatically)                     |
| `eddi.tool.*` metric `tool` tag | dispatch name                                     |

> **Rate-limit buckets are per dispatch name.** `{"websearch": 30}` sets the *limit* for the whole tool but gives `searchWeb`, `searchNews` and `searchWikipedia` 30 calls/minute **each**, not 30 between them. Pin a single operation by using its dispatch name: `{"searchNews": 5}`.

#### Budgets

`maxBudgetPerConversation` bounds **tool** cost only — the accumulated per-call prices of the tools a conversation invokes. LLM token spend is a separate, run-scoped concern governed by the model cascade's `maxCostPerRun`; the two are not added together.

Enforcement is **opt-in**: a configured ceiling records cost but refuses nothing until you add `enforceBudget: true`. Built-in tools priced at $0.00 until the canonical-slug fix in this release, so enforcing automatically would make those ceilings bind for the first time and start aborting tool calls mid-conversation on upgrade.

That choice has a real cost, which is why the engine warns rather than staying quiet: http, MCP, A2A and dynamic tools dispatch under their configured name, so a tool called `websearch` **was** priced and refused before `enforceBudget` existed. If you relied on such a ceiling, add the flag — every task carrying a ceiling without it is named once in a startup WARN. Cost is tracked and reported (`GET /llm/toolhistory/costs`, `eddi.tool.costs`) either way. The deployment-wide default comes from `eddi.tools.budget.enforce-by-default` (default `false`).

The check runs *before* each call and uses `<=`, so the call that crosses the ceiling still completes and the next one is refused with `Error: Budget exceeded for conversation <id>`.

Default per-call prices: `webscraper` $0.002, `websearch` $0.001, `pdfreader` $0.001, `weather` $0.0005; `calculator`, `datetime`, `dataformatter` and `textsummarizer` are free. Anything not in that table — http, mcp, a2a, dynamic and the remaining built-ins — costs $0.00 until you price it with `toolPricing`. Negative `toolPricing` values are clamped to 0.0.

#### Tool cache scoping

Cached tool results are partitioned by identity. The cache key is `scopeTag|toolName:arguments`, and the scope tag is resolved per tool call as `toolCacheScopes[<dispatch name>]` → `toolCacheScopes[<slug>]` → `defaultToolCacheScope` → `user`:

| Scope          | Tag                                   | A cached result is reused…                     |
| -------------- | ------------------------------------- | ---------------------------------------------- |
| `user`         | `u:<32 hex chars of SHA-256(userId)>` | only for the same authenticated user (default) |
| `conversation` | `c:<conversationId>`                  | only inside the conversation that produced it  |
| `global`       | `g`                                   | by everyone — opt-in only                      |

Choose `global` **only** for tools whose result depends purely on their arguments and never on who is asking (pure computation, public reference data). It is the one setting that permits cross-user reuse.

When `user` scope applies but there is no user id, the entry falls back to the narrower conversation partition. When neither identity is available the cache is bypassed entirely for that call — nothing is read and nothing is stored, and the `eddi.tool.cache.bypassed` counter is incremented.

An unrecognized token never fails the agent load, and it never widens a tool's audience either. A `toolCacheScopes` entry whose value does not parse (`"usr"`, `""`, `null`) resolves to `user` — **not** to `defaultToolCacheScope`, which could be `global` — and is logged at WARN naming the tool and the bad token. An unrecognized `defaultToolCacheScope` likewise resolves to `user`.

Per-tool TTLs are enforced per entry: a cached result is removed once its own TTL has elapsed since it was written, independently of the other entries in the cache. The TTL is resolved from the dispatch name first and the slug second, so `searchNews` gets the 10-minute `news` TTL while its `searchWeb` sibling inherits `websearch`'s 30 minutes. `GET /llm/tools/cache/ttl/{toolName}` reports the TTL that will be applied. Size eviction (10 000 entries) is the secondary bound.

### Configuration Example

```json
{
  "tasks": [
    {
      "actions": ["help"],
      "type": "openai",
      "enableBuiltInTools": true,
      "enableRateLimiting": true,
      "defaultRateLimit": 100,
      "toolRateLimits": { "websearch": 30, "weather": 50 },
      "enableToolCaching": true,
      "enableCostTracking": true,
      "toolPricing": { "websearch": 0.005 },
      "maxBudgetPerConversation": 5.0,
      "enforceBudget": true,
      "parameters": { "apiKey": "...", "modelName": "gpt-4o" }
    }
  ]
}
```

### Security Hardening

Tools that accept URLs from LLM-generated arguments are protected against **Server-Side Request Forgery (SSRF)**:

* Only `http` and `https` schemes are allowed
* Private/internal IP ranges are blocked (loopback, site-local, link-local)
* Cloud metadata endpoints are blocked (`169.254.169.254`, `metadata.google.internal`)
* Internal hostnames (`.local`, `.internal`, `localhost`) are rejected

The **Calculator** tool uses a sandboxed recursive-descent math parser (`SafeMathParser`) instead of a script engine, eliminating any possibility of code injection.

See the [Security documentation](/security-and-compliance/security) for full details.

***

## Monitoring & Observability

EDDI provides built-in metrics for monitoring agent performance:

* Tool execution success/failure rates
* Response latency (P50, P95, P99)
* Cache hit rates
* Cost tracking
* Rate limit violations

See the [Metrics Documentation](/deployment-and-infrastructure/metrics) for details on configuring Prometheus/Grafana monitoring.

***

## Complete Example: Multi-Capability Agent

```json
{
  "tasks": [
    {
      "actions": ["*"],
      "id": "universalAssistant",
      "type": "openai",
      "description": "Universal AI assistant with multiple capabilities",
      "parameters": {
        "apiKey": "your-openai-api-key",
        "modelName": "gpt-4o",
        "systemMessage": "You are a helpful AI assistant with access to calculator, web search, and weather tools.",
        "temperature": "0.7",
        "timeout": "30000"
      },
      "enableBuiltInTools": true,
      "builtInToolsWhitelist": [
        "calculator",
        "datetime",
        "websearch",
        "weather"
      ],
      "conversationHistoryLimit": 10
    }
  ]
}
```

This agent can:

* ✅ Perform calculations
* ✅ Get date/time info
* ✅ Search the web
* ✅ Check weather
* ✅ Maintain 10 turns of conversation history

***

## Integration with Behavior Rules

To trigger the Langchain task, configure Behavior Rules to emit the appropriate action:

```json
{
  "name": "Send to LLM",
  "rules": [
    {
      "name": "User asks question",
      "conditions": [
        {
          "type": "occurrence",
          "occurrence": "currentstep",
          "value": "input:initial"
        }
      ],
      "actions": ["send_message"]
    }
  ]
}
```

Then reference this action in your Langchain task:

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "myChat",
      "type": "openai",
      "parameters": {
        "apiKey": "your-api-key",
        "addToOutput": "true"
      }
    }
  ]
}
```

***

## Structured Output (JSON Mode)

When you need the LLM to return a specific JSON structure (e.g., for property extraction, API response formatting, or quick reply generation), use the `convertToObject` parameter with an optional `responseSchema`.

### Three-Layer Enforcement

EDDI uses three complementary mechanisms to ensure reliable JSON output:

| Layer                | Mechanism                                                                     | Coverage             |
| -------------------- | ----------------------------------------------------------------------------- | -------------------- |
| **1. System Prompt** | Appends `## RESPONSE FORMAT (MANDATORY)` section with schema to every request | All providers        |
| **2. Native API**    | Sets `ResponseFormatType.JSON` on the outgoing `ChatRequest`                  | See the matrix below |
| **3. Validation**    | Pre-parse `startsWith("{")` check before deserialization                      | All providers        |

If a provider doesn't support native JSON mode (e.g. Anthropic), EDDI gracefully falls back to prompt-only enforcement.

#### Native JSON mode — provider matrix

Layer 2 is applied **per request**, never baked into the model instance, and it is applied in **all three execution modes**: no-tools (legacy), agent mode (tool-calling) and streaming — including every step of a multi-model cascade, which is evaluated against that step's own provider.

| Provider                                         | No tools (legacy / streaming)                             | Agent mode (tools present)                                                            |
| ------------------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `openai`                                         | ✅                                                         | ✅                                                                                     |
| `azure-openai`                                   | ✅                                                         | ✅                                                                                     |
| `mistral`                                        | ✅                                                         | ✅                                                                                     |
| `gemini`, `gemini-vertex`                        | ✅                                                         | ❌ — the Gemini API rejects `responseMimeType: application/json` together with `tools` |
| `anthropic`, `bedrock`                           | ❌ — both reject a JSON format without a schema            | ❌                                                                                     |
| `ollama`, `jlama`, `huggingface`, `oracle-genai` | ❌ (not verified — opt in with `jsonResponseFormat: "on"`) | ❌                                                                                     |

#### Overriding the matrix per task

Set `jsonResponseFormat` on the LLM **task** (not in `parameters`):

| Value            | Behaviour                                                                                                                                                                                                                                                                           |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auto` (default) | Use the matrix above, including the tools-aware distinction                                                                                                                                                                                                                         |
| `on`             | Always send the JSON format when `convertToObject=true`, tools included. The escape hatch for a provider or OpenAI-compatible gateway the matrix does not know yet — it also bypasses the Gemini guard, so only use it where you have verified the provider accepts the combination |
| `off`            | Never send it; enforcement stays prompt-only                                                                                                                                                                                                                                        |

```json
{
  "id": "classifier",
  "type": "mistral",
  "jsonResponseFormat": "auto",
  "parameters": { "convertToObject": "true" }
}
```

> **Do not set a `responseFormat` model parameter.** It is only read by the OpenAI builder and it bakes JSON mode into a **cached** model that is then reused for tool-calling and streaming requests — the cause of the historical Gemini `400 Function calling with a response mime type: 'application/json' is unsupported`. `convertToObject` alone is enough.

### Basic JSON Mode

```json
{
  "parameters": {
    "convertToObject": "true",
    "addToOutput": "false",
    "systemMessage": "You are a customer support classifier."
  }
}
```

### With Response Schema

For maximum reliability, specify the exact JSON structure you expect:

```json
{
  "parameters": {
    "convertToObject": "true",
    "addToOutput": "false",
    "responseSchema": "{\"htmlResponseText\": \"string — the formatted response\", \"quickReplies\": [\"string — suggested follow-up options\"], \"sentiment\": \"positive|negative|neutral\"}",
    "systemMessage": "You are a customer support agent. Analyze the user's message and respond."
  }
}
```

The schema is injected into the system prompt as a JSON code block so the LLM sees the exact expected format.

### Using with Output Configuration

When `convertToObject=true`, the LLM's JSON response is stored in conversation memory as a parsed object. You can then reference its fields in the Output Configuration:

```json
{
  "outputBuildInstructions": [{
    "outputType": "text",
    "outputValue": "{properties.aiOutputObject.htmlResponseText}"
  }],
  "qrBuildInstructions": [{
    "pathToTargetArray": "properties.aiOutputObject.quickReplies",
    "iterationObjectName": "quickReply",
    "quickReplyValue": "{quickReply}",
    "quickReplyExpressions": "trigger(quick_reply)"
  }]
}
```

### Debugging

When `convertToObject=true`, the raw LLM response is **always** persisted in conversation memory (key: `langchain:data`) even if JSON parsing fails. This ensures you can inspect what the LLM actually returned via the conversation log.

### Tips

* **Streaming**: Not recommended with JSON mode — the UI would show raw JSON building up. It does work (the streamed request carries the format for supported providers), but pair it with `addToOutput: "false"` and a `postResponse`
* **Provider compatibility**: see the provider matrix above. Unsupported providers rely on prompt-based enforcement
* **Schema specificity**: The more specific your `responseSchema`, the more reliable the output. Use type hints (`"string"`, `"number"`, `"boolean"`) and descriptions

***

## Common Issues and Troubleshooting

### API Key Issues

* **Problem**: "Invalid API key" errors
* **Solution**: Ensure API keys are valid and have not expired. Renew them before expiry.

### Model Misconfiguration

* **Problem**: "Model not found" errors
* **Solution**: Verify model names match those supported by the provider (e.g., "gpt-4o" for OpenAI, not "gpt4")

### Timeout Issues

* **Problem**: Requests timing out
* **Solution**: Increase the `timeout` parameter value (in milliseconds). Default is often 15000 (15 seconds).
* **Problem**: A *streaming* turn is cut off after \~120s even though `timeout` is larger
* **Solution**: This was the behaviour before the `timeout`/`streamingTimeoutSeconds` unification; the backstop now follows a longer `timeout` automatically. Set `streamingTimeoutSeconds` explicitly if you need a bound that differs from the derived one — see [Timeouts and Streaming](#timeouts-and-streaming).

### Anthropic First Message Error

* **Problem**: Anthropic API rejects conversations starting with agent message
* **Solution**: Set `includeFirstAgentMessage: "false"` for Anthropic tasks

### Tool Not Working

* **Problem**: Agent not using expected tools
* **Solution**:
  * Verify `enableBuiltInTools: true` is set
  * Check `builtInToolsWhitelist` includes the desired tool
  * Ensure the model supports tool calling (e.g., gpt-4o, not gpt-3.5-turbo)

### Response Not Added to Output

* **Problem**: LLM response not visible to user
* **Solution**: Set `addToOutput: "true"` in parameters, or configure `postResponse.outputBuildInstructions`

***

## Tool Execution Context

Understanding how tools execute is critical for designing new built-in tools and avoiding common pitfalls.

### Execution Path

All LLM tools execute **inside a conversation pipeline**. The full execution path is:

```
LlmTask.execute(memory)
  └─→ AgentOrchestrator.buildToolList(memory, config)
      └─→ Constructs tool instances with conversation context
  └─→ LLM invokes tool
  └─→ ToolExecutionService.executeToolWrapped()
      └─→ Rate Limiter → Cache Check → Execute → Cost Tracker → Result
```

### Implicit Context

`IConversationMemory` is **always available** when tools execute. Tools that need conversation state (e.g., `userId`, `agentId`, `groupIds`) receive it via constructor injection from `AgentOrchestrator`, which has the memory object at tool-list build time.

This means:

* **No ThreadLocal** or request-scoped beans needed
* **No `userId` parameter** on LLM tools — the conversation always knows who the user is
* Only external interfaces (MCP, REST) that operate **outside** a conversation need explicit user identification

### LLM Tools vs MCP Tools

| Aspect              | LLM Tools (built-in)                        | MCP Tools                           |
| ------------------- | ------------------------------------------- | ----------------------------------- |
| Execution context   | Inside conversation pipeline                | Outside conversation                |
| User identification | Implicit from `IConversationMemory`         | Explicit `userId` parameter         |
| Registration        | `builtInToolsWhitelist` in langchain config | `McpMemoryTools.java`               |
| Audience            | The LLM agent itself                        | External AI agents or admin tooling |

***

## See Also

* [Behavior Rules](/agent-configuration/behavior-rules) - Triggering LLM tasks conditionally
* [HTTP Calls](/agent-configuration/httpcalls) - Creating custom HTTP call tools for agents
* [Security](/security-and-compliance/security) - SSRF protection, sandboxed evaluation, tool hardening
* [Output Configuration](/agent-configuration/output-configuration) - Formatting agent responses
* [Conversation Memory](/architecture-and-concepts/conversation-memory) - Understanding conversation state
* [Metrics](/deployment-and-infrastructure/metrics) - Monitoring LLM performance

***

## Summary

The LLM Lifecycle Task provides a flexible, unified interface for integrating LLMs into EDDI agents:

1. ✅ **Simple by Default** - Start with basic chat, add tools when needed
2. ✅ **12 Provider Support** - OpenAI, Anthropic, Google, Mistral, Azure, Bedrock, Oracle, Ollama, Hugging Face, Jlama + OpenAI-compatible (DeepSeek, Cohere)
3. ✅ **Built-in Tools** - 8 tools available when you enable agent mode
4. ✅ **Tool Execution Pipeline** - Rate limiting, caching, cost tracking for every tool call
5. ✅ **Security Hardened** - SSRF protection, sandboxed math evaluation, input validation
6. ✅ **Fine-Grained Control** - Pre/post processing, context management, templating
7. ✅ **Orchestration Layer** - Conditional invocation, hybrid workflows, state persistence
8. ✅ **Easy Configuration** - Use Agent Father for guided setup

Whether you need simple chat or advanced agent capabilities, the Langchain task provides the foundation for intelligent conversational experiences in EDDI.


# RAG (Retrieval-Augmented Generation)

> **Phase 8c** — Config-driven knowledge base retrieval integrated into the LLM pipeline.

## Overview

EDDI's RAG system is a first-class workflow extension that adds contextual knowledge retrieval to LLM conversations. Knowledge bases are versioned configurations — just like behavior rules or httpCalls — managed via REST API and wired into workflows.

At execution time, the `LlmTask` discovers RAG configurations from the agent's workflow, performs vector similarity search against the user's query, and injects the retrieved context into the LLM system message — all automatically and transparently.

## Architecture

```
User Query
    │
    ▼
┌─────────────────── LlmTask.executeTask() ───────────────────┐
│                                                              │
│  1. Extract user input from conversation memory              │
│  2. RagContextProvider.retrieveContext()                     │
│     ├── WorkflowTraversal.discoverConfigs() → find RAG steps │
│     ├── Match KBs (explicit refs or auto-discover all)       │
│     ├── EmbeddingModelFactory → cached embedding model       │
│     ├── EmbeddingStoreFactory → cached vector store          │
│     ├── EmbeddingStoreContentRetriever → similarity search   │
│     └── Store audit trace in conversation memory             │
│  3. Inject context: systemMessage += "## Relevant Context"   │
│  4. Build chat messages and call LLM                         │
│                                                              │
└──────────────────────────────────────────────────────────────┘
```

## Configuration

### RagConfiguration (Knowledge Base)

A `RagConfiguration` is a versioned resource at `/ragstore/rags/`. It defines:

```json
{
  "name": "product-docs",
  "embeddingProvider": "openai",
  "embeddingParameters": {
    "model": "text-embedding-3-small",
    "apiKey": "${vault:tenant/agent/openai-key}"
  },
  "storeType": "in-memory",
  "storeParameters": {},
  "chunkStrategy": "recursive",
  "chunkSize": 512,
  "chunkOverlap": 64,
  "maxResults": 5,
  "minScore": 0.6
}
```

| Field                 | Default     | Description                                             |
| --------------------- | ----------- | ------------------------------------------------------- |
| `name`                | —           | Display name / identifier for this knowledge base       |
| `embeddingProvider`   | `openai`    | Provider (see Embedding Providers table below)          |
| `embeddingParameters` | —           | Provider-specific params (model, apiKey, baseUrl, etc.) |
| `storeType`           | `in-memory` | Vector store (see Vector Stores table below)            |
| `storeParameters`     | —           | Store-specific connection params                        |
| `chunkStrategy`       | `recursive` | Document chunking strategy                              |
| `chunkSize`           | `512`       | Chunk size in characters                                |
| `chunkOverlap`        | `64`        | Chunk overlap in characters                             |
| `maxResults`          | `5`         | Default top-K results                                   |
| `minScore`            | `0.6`       | Default minimum similarity score (0.0–1.0)              |

### LLM Task RAG Configuration

RAG is wired into LLM tasks via three fields on `LlmConfiguration.Task`:

#### Option 1: Explicit Knowledge Base References

```json
{
  "tasks": [{
    "actions": ["*"],
    "type": "openai",
    "knowledgeBases": [
      { "name": "product-docs", "maxResults": 5, "minScore": 0.7 },
      { "name": "faq", "maxResults": 3 }
    ],
    "parameters": {
      "systemMessage": "You are a helpful assistant."
    }
  }]
}
```

Each reference names a KB from the workflow and optionally overrides retrieval parameters.

#### Option 2: Auto-Discovery

```json
{
  "tasks": [{
    "enableWorkflowRag": true,
    "ragDefaults": { "maxResults": 5, "minScore": 0.7 }
  }]
}
```

When `enableWorkflowRag` is `true`, the system discovers all RAG steps from the workflow automatically.

#### Option 3: httpCall RAG (Phase 8c-0)

```json
{
  "tasks": [{
    "httpCallRag": "search-api"
  }]
}
```

Zero-infrastructure RAG: execute a named httpCall and inject its response as `## Search Results:` context. The user's input is available as `{userInput}` in httpCall templates. No vector store needed. Both httpCall RAG and vector RAG can be active simultaneously.

#### Context Injection

Retrieved vector-RAG context (Options 1 and 2) is **always** appended to the LLM **system message** under a `## Relevant Context:` heading. `RagContextProvider` returns one formatted block covering every matched knowledge base, and `LlmTask` appends it. There is no per-knowledge-base or per-task switch for the injection point or for the formatting.

> **Note for existing configurations:** older `langchain.json` documents may still carry `injectionStrategy` (on `knowledgeBases[]` or `ragDefaults`) or `contextTemplate` (on `knowledgeBases[]`). Neither key was ever read by the engine — context has always gone to the system message — and both were removed from `LlmConfiguration`. Stored configurations remain valid: the leftover keys are ignored on load and dropped the next time the configuration is saved. No migration is required.

## REST API

### Configuration Management

| Method   | Path                            | Description                |
| -------- | ------------------------------- | -------------------------- |
| `GET`    | `/ragstore/rags/jsonSchema`     | JSON Schema for validation |
| `GET`    | `/ragstore/rags/descriptors`    | List KB descriptors        |
| `GET`    | `/ragstore/rags/{id}?version=N` | Read a KB configuration    |
| `POST`   | `/ragstore/rags`                | Create a new KB            |
| `PUT`    | `/ragstore/rags/{id}?version=N` | Update a KB                |
| `POST`   | `/ragstore/rags/{id}?version=N` | Duplicate a KB             |
| `DELETE` | `/ragstore/rags/{id}?version=N` | Delete a KB                |

### Document Ingestion

| Method | Path                                                             | Description                                         |
| ------ | ---------------------------------------------------------------- | --------------------------------------------------- |
| `POST` | `/ragstore/rags/{id}/ingest?version=N&kbId=...&documentName=...` | Ingest a text document (returns 202 + ingestion ID) |
| `GET`  | `/ragstore/rags/{id}/ingestion/{ingestionId}/status`             | Poll ingestion status                               |

**Example: Ingest a document**

```bash
curl -X POST http://localhost:7070/ragstore/rags/abc123/ingest?version=1\&documentName=readme.txt \
  -H "Content-Type: text/plain" \
  -d "This is the document content to be chunked, embedded, and stored."
```

Response: `202 Accepted`

```json
{
  "ingestionId": "550e8400-e29b-41d4-a716-446655440000",
  "kbId": "product-docs",
  "status": "pending"
}
```

**Poll status:**

```bash
curl http://localhost:7070/ragstore/rags/abc123/ingestion/550e8400-e29b-41d4-a716-446655440000/status
```

Response:

```json
{
  "ingestionId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed"
}
```

Status values: `pending` → `processing` → `completed` | `failed: <error message>`

## Observability

RAG operations write audit traces to conversation memory:

| Memory Key                    | Content                                                                               |
| ----------------------------- | ------------------------------------------------------------------------------------- |
| `rag:trace:{taskId}`          | Per-KB retrieval metadata (provider, storeType, maxResults, minScore, retrievedCount) |
| `rag:context:{taskId}`        | Formatted context string injected into the LLM                                        |
| `rag:httpcall:trace:{taskId}` | httpCall RAG execution metadata (httpCall name, context length)                       |

These are visible in the conversation memory snapshot and the audit ledger.

## Embedding Providers

| Provider       | Default Model                  | Required Parameters                    | Notes                                                       |
| -------------- | ------------------------------ | -------------------------------------- | ----------------------------------------------------------- |
| `openai`       | `text-embedding-3-small`       | `apiKey`                               | Use `${vault:...}` for keys                                 |
| `azure-openai` | `text-embedding-3-small`       | `endpoint`, `apiKey`, `deploymentName` | Azure-hosted OpenAI models                                  |
| `ollama`       | `nomic-embed-text`             | —                                      | `baseUrl` (default: `localhost:11434`)                      |
| `mistral`      | `mistral-embed`                | `apiKey`                               | Mistral AI embedding model                                  |
| `bedrock`      | `amazon.titan-embed-text-v2:0` | —                                      | Uses AWS credentials chain; `region` (default: `us-east-1`) |
| `cohere`       | `embed-english-v3.0`           | `apiKey`                               | Excellent multilingual support                              |
| `vertex`       | `text-embedding-005`           | `project`                              | `location` (default: `us-central1`); uses GCP credentials   |

## Vector Stores

| Store Type      | Required Parameters | Notes                                                                                                  |
| --------------- | ------------------- | ------------------------------------------------------------------------------------------------------ |
| `in-memory`     | —                   | Ephemeral, for dev/test only                                                                           |
| `pgvector`      | `password`          | PostgreSQL + pgvector; `host`, `port`, `database`, `user`, `table`, `dimension`                        |
| `mongodb-atlas` | `connectionString`  | MongoDB Atlas Vector Search; `databaseName`, `collectionName`, `indexName`                             |
| `elasticsearch` | —                   | `serverUrl` (default: `localhost:9200`); optional `apiKey` or `userName`+`password`; `indexName`       |
| `qdrant`        | —                   | `host` (default: `localhost`), `port` (default: `6334`); optional `apiKey`, `useTls`; `collectionName` |

## Status

* ✅ **Phase 8c**: RAG Foundation — config-driven knowledge base retrieval
* ✅ **Phase 8c-0**: httpCall-based RAG (zero infrastructure)
* ✅ **Phase 8c-β**: Persistent vector stores (pgvector)
* ✅ **Phase 8c-γ**: RAG provider expansion (8 embedding models + 6 vector stores)
* ✅ **Phase 8c-M**: Manager UI — RAG editor with full provider parity + document ingestion
* ✅ **REST ingestion endpoint**: `POST /ragstore/rags/{id}/ingest`

## Future Enhancements

* Advanced retrieval: re-ranking, hybrid search, metadata filtering
* ONNX in-process embeddings (air-gapped / edge deployments)


# Model Cascade

> Cost-optimized LLM execution via sequential model escalation with confidence-based routing.

## Overview

Multi-model cascading lets an LLM task try a fast, cheap model first and only escalate to a more expensive model if the confidence in the response is below a threshold. This reduces cost for queries that simpler models handle well, while preserving quality for hard queries.

**Flow:** `User query → Model A (fast/cheap) → Confidence check → if low → Model B (powerful) → Confidence check → ...`

The cascade is a self-contained branch inside `LlmTask` — no engine pipeline changes, and configs without `modelCascade` (or with `enabled: false`) behave exactly as before.

## Configuration

Cascading is configured per-task in a `langchain.json` resource:

```json
{
  "tasks": [
    {
      "id": "cascade-task",
      "type": "openai",
      "actions": ["*"],
      "parameters": {
        "systemMessage": "You are a helpful assistant.",
        "apiKey": "${vault:openai-key}",
        "logSizeLimit": "10"
      },
      "modelCascade": {
        "enabled": true,
        "strategy": "cascade",
        "evaluationStrategy": "structured_output",
        "enableInAgentMode": true,
        "maxTotalDurationMs": 45000,
        "maxCostPerRun": 0.05,
        "steps": [
          {
            "type": "openai",
            "parameters": { "model": "gpt-4o-mini" },
            "confidenceThreshold": 0.7,
            "timeoutMs": 10000,
            "inputPricePer1M": 0.15,
            "outputPricePer1M": 0.60
          },
          {
            "type": "openai",
            "parameters": { "model": "gpt-4o" },
            "confidenceThreshold": null,
            "timeoutMs": 30000,
            "inputPricePer1M": 2.50,
            "outputPricePer1M": 10.00
          }
        ]
      }
    }
  ]
}
```

### Cascade fields

| Field                                  | Type    | Default               | Description                                                                                                                                                                                                                                                                            |
| -------------------------------------- | ------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`                              | boolean | `false`               | Master toggle for cascading                                                                                                                                                                                                                                                            |
| `strategy`                             | string  | `"cascade"`           | Execution strategy. Only `cascade` (sequential) is implemented; `parallel` and any unknown value warn at deploy time and run sequentially.                                                                                                                                             |
| `evaluationStrategy`                   | string  | `"structured_output"` | How confidence is evaluated (see below)                                                                                                                                                                                                                                                |
| `enableInAgentMode`                    | boolean | `true`                | Whether cascade activates when tools/agents are configured                                                                                                                                                                                                                             |
| `judgeModel`                           | object  | —                     | Model for the `judge_model` strategy: `{ "type": "...", "parameters": {...} }`. Expected when `evaluationStrategy` is `judge_model`; if omitted or unbuildable, deployment logs a warning and confidence evaluation falls back to `heuristic` at runtime.                              |
| `heuristic`                            | object  | —                     | Overrides for the `heuristic` strategy (see below). Optional.                                                                                                                                                                                                                          |
| `maxTotalDurationMs`                   | long    | —                     | Wall-clock ceiling across the whole cascade. When reached, escalation stops and the best response so far is returned. Also caps each **buffered** step's timeout by the remaining budget — a step streamed live is exempt (see [Streaming the Final Step](#streaming-the-final-step)). |
| `maxCostPerRun`                        | double  | —                     | Dollar ceiling for a single run, computed from token usage × per-step pricing. When reached, escalation stops and the best response so far is returned.                                                                                                                                |
| `inputPricePer1M` / `outputPricePer1M` | double  | —                     | Cascade-level default token pricing (steps may override). Used for cost reporting and the cost ceiling.                                                                                                                                                                                |
| `returnBestAcrossSteps`                | boolean | `false`               | When true, if an earlier (escalated) step scored strictly higher than the finally-accepted step, the earlier step's response is returned.                                                                                                                                              |
| `steps`                                | array   | —                     | Ordered list of cascade steps (cheap → expensive)                                                                                                                                                                                                                                      |

### Step fields

| Field                                  | Type   | Default         | Description                                                                                                                                                                                                                                                                          |
| -------------------------------------- | ------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type`                                 | string | task `type`     | Provider type (e.g., `openai`, `anthropic`, `ollama`). Resolved through global variables, like the task type.                                                                                                                                                                        |
| `parameters`                           | object | `{}`            | Provider-specific params. Merged over the base task parameters (step wins). Values are resolved for `${vault:...}` secrets, global variables, and Qute templates — parity with task params.                                                                                          |
| `confidenceThreshold`                  | Double | `null`          | Minimum confidence to accept this step. Below it, escalate. **A non-last step should set a threshold** (a null threshold there is always-accepted, making later steps unreachable — flagged with a deploy-time warning). The last step's threshold is ignored (always accepted).     |
| `timeoutMs`                            | long   | `30000`         | Per-step timeout in milliseconds, for **buffered** (non-streamed) steps — also bounded by the remaining `maxTotalDurationMs` budget. A step streamed live ignores this and instead runs under an internal \~120 s bound (see [Streaming the Final Step](#streaming-the-final-step)). |
| `inputPricePer1M` / `outputPricePer1M` | double | cascade default | Per-step token pricing (overrides the cascade-level default).                                                                                                                                                                                                                        |

> **Merge note:** Step parameters are merged over base task parameters (step wins). Steps only specify overrides (e.g., a different `model`); shared params like `systemMessage` are inherited.
>
> **⚠️ Cross-provider credentials:** Because parameters are inherited, a step (or `judgeModel`) that targets a **different provider** than the task must supply **its own credentials** — otherwise it silently inherits the task's `apiKey`, which is wrong for a different provider and fails at runtime as a 401 (which the cascade then treats as an escalation). A different-provider step/judge that omits its own `apiKey` is flagged with a deploy-time warning. Give each cross-provider step its own full parameter set (`apiKey`, `baseUrl`, etc.). Same-provider steps may safely inherit the task's credentials.

## Confidence Evaluation Strategies

### `structured_output` (default)

Appends a JSON-format instruction to the system prompt asking the model to respond with a single JSON object:

```json
{ "response": "The actual answer...", "confidence": 0.85 }
```

The evaluator tries a **real JSON parse first** (Jackson), and only treats the response as a confidence wrapper when the whole response is a single JSON object — so a stray `"confidence": ...` inside legitimate answer content (e.g. a code sample) is **not** mistaken for the score. A regex fallback handles a malformed-but-object-shaped wrapper. If the response is not a JSON-object wrapper, it falls back to `heuristic`.

> **Agent mode / convertToObject:** the wrapper cannot be used with tools or with `convertToObject: true` (it would collide with the raw-schema JSON). In those cases the cascade automatically uses `judge_model` (if a judge is configured) or `heuristic`. The `convertToObject` + `structured_output` combination is flagged with a deploy-time warning; the agent-mode downgrade is logged at debug level at runtime (agent mode is only known when the task runs). See below.

### `heuristic`

Analyzes the response text for uncertainty signals. Phrases and thresholds are **configurable** via `heuristic` (English defaults). When no configured phrase matches, a language-agnostic default score is used.

| Signal                                                  | Confidence (default)   |
| ------------------------------------------------------- | ---------------------- |
| Empty/null response                                     | `0.0`                  |
| Very short (< `shortLengthThreshold`, default 20 chars) | `shortScore` (`0.3`)   |
| Refusal phrase (e.g. "I cannot fulfill")                | `refusalScore` (`0.2`) |
| Hedging phrase (e.g. "I'm not sure")                    | `hedgingScore` (`0.4`) |
| No red flags                                            | `defaultScore` (`0.8`) |

`heuristic` config fields (all optional): `lowConfidencePhrases`, `refusalPhrases`, `shortLengthThreshold`, `shortScore`, `refusalScore`, `hedgingScore`, `defaultScore`. Localize the phrase lists for non-English deployments — without configured phrases, the evaluator cannot distinguish hedging from confidence and returns the default score. Configured score values are clamped to `[0.0, 1.0]`, so a mis-set value can't produce an out-of-range confidence.

### `judge_model`

A separate (typically cheap) model rates the response's confidence. Requires a `judgeModel` config block; the judge is built once via the model registry (with vault + global-variable resolution). If the judge cannot be built or the call fails, it falls back to `heuristic`.

```json
"judgeModel": { "type": "openai", "parameters": { "model": "gpt-4o-mini", "apiKey": "${vault:openai-key}" } }
```

### `none`

Always returns `1.0` — effectively disables confidence gating. The first step's response is always accepted. Useful for timeout/error recovery only, or A/B testing.

## Error Handling

| Error Type                          | Behavior                                                                                                                                                                            |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Rate limited (429) / 5xx**        | Retried **in-step** up to the task's `retry.maxAttempts` (with backoff) before escalating to the next step.                                                                         |
| **Timeout**                         | The step is cancelled and the cascade escalates; a warning is logged. A step streamed live is exempt from cancellation — see [Streaming the Final Step](#streaming-the-final-step). |
| **Other errors**                    | Logged; escalate to the next step.                                                                                                                                                  |
| **Duration / cost ceiling reached** | Stop escalating, return the best response so far.                                                                                                                                   |
| **All steps fail**                  | Return the best response seen so far, or throw `LifecycleException` if none produced a result.                                                                                      |

The cascade tracks the "best response" seen so far — if a later step fails but an earlier step produced a usable response, that response is returned rather than throwing.

## SSE Events

Two SSE event types provide real-time visibility, emitted through `ConversationEventSink` → `StreamingResponseHandler` → the `/agents/{conversationId}/stream` SSE endpoint:

| Event                | Fields                                                                  |
| -------------------- | ----------------------------------------------------------------------- |
| `cascade_step_start` | `stepIndex`, `modelType`, `modelName`, `totalSteps`                     |
| `cascade_escalation` | `fromStep`, `toStep`, `confidence`, `threshold`, `reason`, `durationMs` |

`reason` is one of `low_confidence`, `timeout`, `error`, `retryable_error`.

## Streaming the Final Step

When streaming (SSE), a **guaranteed-accept step** is streamed **live** token-by-token: the last step, a step with a null `confidenceThreshold`, or a `none`-strategy step whose threshold is `≤ 1.0` (its confidence is always `1.0`, so it will always accept). This requires legacy (no-tools) mode, a non-wrapper strategy (`heuristic`, `judge_model`, or `none`), and a streaming-capable provider. Any step that could still escalate is always buffered instead (its full text is needed to evaluate confidence before deciding). In agent mode, the cascade emits the final response as a single chunk rather than streaming it live.

> **Bounds & consistency:** a live-streamed step is **not** subject to the per-step `timeoutMs` / `maxTotalDurationMs` cap — cancelling it mid-stream cannot stop the provider from continuing to emit tokens to the client, so instead it runs under the streaming executor's own internal bound (\~120 s) and its result — even if partial at that bound — is the accepted answer. The client therefore never receives tokens for a response that is then replaced. `returnBestAcrossSteps` also never supersedes a step that was streamed live, for the same reason.

## Observability

### Trace

The full per-step trace is stored in conversation memory under `langchain:cascade:trace:<taskId>`. Each entry contains: `step`, `model`, `modelType`, `confidence`, `durationMs`, `tokenUsage` (`inputTokens`/`outputTokens`/`totalTokens`), `costUsd`, and `status` (`accepted`, `escalated`, `timeout`, `error`, `retryable_error`). When `returnBestAcrossSteps` overrides the outcome, the step that would have been accepted is relabeled `superseded_by_best` and the earlier winning step is relabeled `accepted_as_best`, so the trace always agrees with the returned `stepUsed`.

### Response metadata

If `responseMetadataObjectName` is set, the cascade populates it with real token usage plus `cascadeCostUsd`, `cascadeModel` (`provider/model`), `cascadeStep`, and `cascadeConfidence`.

### Metrics (Micrometer, `/q/metrics`)

`eddi.llm.cascade.executions` (tag `agentMode`), `eddi.llm.cascade.escalations` (tag `reason`), `eddi.llm.cascade.accepted.step` (tag `step`), `eddi.llm.cascade.step.latency` (timer, tag `provider`), `eddi.llm.cascade.confidence` (distribution), `eddi.llm.cascade.step.errors` (tags `provider`, `type`), `eddi.llm.cascade.tokens` / `eddi.llm.cascade.cost` (tag `provider`), `eddi.llm.cascade.ceiling.exceeded` (tag `kind` = `duration`|`cost`).

## Audit Trail

When the audit collector is active, the cascade writes:

| Memory Key                  | Content                                        |
| --------------------------- | ---------------------------------------------- |
| `audit:model_name`          | The **actual** winning model, `provider/model` |
| `audit:cascade_model`       | `provider/model (step N)`                      |
| `audit:cascade_confidence`  | Confidence of the accepted response            |
| `audit:cascade_cost`        | Aggregate run cost in dollars                  |
| `audit:cascade_token_usage` | Token usage of the accepted step               |

## Agent Mode

When `enableInAgentMode` is `true` (default), the cascade also works when tools (built-in, MCP, HTTP calls, A2A) are configured — each step can independently invoke the tool-calling loop. Because the `structured_output` wrapper cannot be injected around the tool loop, agent-mode confidence uses `judge_model` (if configured) or `heuristic`.

**Cancellation:** when a step times out, the orchestrator checks for interruption between tool-loop iterations and before each tool, so it stops launching further side-effectful tools. A tool already in flight when the timeout fires may still complete — keep cascade-in-agent-mode tools idempotent where possible.

When `enableInAgentMode` is `false`, cascading is skipped in agent mode and the standard single-model path is used.

## Configure-time Validation

Cascade configs are validated at deploy (`LlmTask.configure`), in two tiers so an upgrade never stops a previously-loading agent from deploying:

* **Hard error (deployment fails)** — only the **new** numeric fields, since no stored config predating this release can contain them: non-positive `maxTotalDurationMs`, negative `maxCostPerRun`, negative per-step / cascade `inputPricePer1M` / `outputPricePer1M`.
* **Warning (logged, deployment proceeds)** — conditions older releases tolerated at load and that still fail/degrade at runtime exactly as before: empty steps, unknown `strategy`, unknown `evaluationStrategy`, `judge_model` without a `judgeModel`, `confidenceThreshold` outside `[0.0, 1.0]`, a non-last step with a null threshold (dead-step trap), non-positive `timeoutMs`, a cross-provider step/judge missing its own `apiKey`, and `convertToObject: true` with `structured_output` (auto-downgraded at runtime).

## Backward Compatibility

* Configs without `modelCascade` work exactly as before.
* `enabled: false` (default) keeps standard execution.
* All new config fields are optional with today's behavior as defaults.
* The cascade lives entirely within `LlmTask`; `StreamingResponseHandler`'s cascade methods are `default`, so other implementers are unaffected.

## Example: Cost Optimization

A 3-tier cascade for a customer support agent, with pricing so savings are measurable:

```json
{
  "modelCascade": {
    "enabled": true,
    "evaluationStrategy": "heuristic",
    "maxCostPerRun": 0.02,
    "steps": [
      {
        "type": "ollama",
        "parameters": { "model": "llama3.2:3b", "baseUrl": "http://localhost:11434" },
        "confidenceThreshold": 0.7,
        "timeoutMs": 5000
      },
      {
        "type": "openai",
        "parameters": { "model": "gpt-4o-mini" },
        "confidenceThreshold": 0.8,
        "timeoutMs": 15000,
        "inputPricePer1M": 0.15,
        "outputPricePer1M": 0.60
      },
      {
        "type": "anthropic",
        "parameters": { "model": "claude-sonnet-4-20250514" },
        "confidenceThreshold": null,
        "timeoutMs": 30000,
        "inputPricePer1M": 3.00,
        "outputPricePer1M": 15.00
      }
    ]
  }
}
```

This routes simple FAQs to a local Ollama model (free), medium queries to GPT-4o-mini, and only complex queries to Claude Sonnet — with per-turn cost recorded in the trace, audit ledger, and metrics so the savings are provable.


# Prompt Snippets

> Prompt Snippets are reusable system prompt building blocks stored as versioned configuration documents. They replace the deleted `CounterweightService`, `IdentityMaskingService`, and `DeploymentContextService` with a flexible, user-extensible, config-driven approach.

## Quick Start

### 1. Create a Snippet via REST API

```bash
POST /snippetstore/snippets
Content-Type: application/json

{
  "name": "cautious_mode",
  "category": "governance",
  "description": "Instructs the agent to verify facts before responding",
  "content": "IMPORTANT: You must always verify facts before responding. If you are unsure about something, say so explicitly rather than guessing. Never fabricate information.",
  "tags": ["safety", "production"],
  "templateEnabled": true
}
```

### 2. Use in a System Prompt

Reference the snippet in your LLM task's system prompt template:

```
You are a helpful customer service agent for {properties.company_name}.

{snippets.cautious_mode}

Always respond in {properties.preferred_language}.
```

That's it. The snippet content is automatically injected at template resolution time.

***

## How It Works

### Auto-Loading

All snippets are loaded from MongoDB at LLM task execution time and injected into the template data map under the `snippets` namespace. This happens **before** the Qute template engine processes the system prompt, so `{snippets.xxx}` resolves like any other template variable.

```
Template Data Map:
├── context       → input context variables
├── properties    → conversation properties  
├── memory        → conversation step data
├── snippets      → ← ALL snippets auto-injected here
│   ├── cautious_mode       → "IMPORTANT: You must..."
│   ├── persona_formal      → "Use formal language..."
│   └── compliance_gdpr     → "You must comply with..."
├── userInfo      → authenticated user
└── conversationLog → formatted history
```

### Caching

Snippets are cached in a Caffeine cache with a **5-minute TTL**. This means:

* Snippets load once from MongoDB, then serve from cache
* After creating/updating/deleting a snippet, changes appear within 5 minutes
* For immediate effect, restart the server or call `invalidateCache()` programmatically
* Cache hit/miss metrics are exposed at `/q/metrics` as `eddi.snippets.cache.hits` and `eddi.snippets.cache.misses`

### Name Validation

Snippet names **must** match the pattern `[a-z0-9_]+`:

| Valid ✅         | Invalid ❌                  |
| --------------- | -------------------------- |
| `cautious_mode` | `CautiousMode` (uppercase) |
| `safety_rules`  | `with-dash` (hyphen)       |
| `tone_formal`   | `with.dot` (dot)           |
| `rule_42`       | `with space` (space)       |

This ensures safe Qute dot-notation access (`{snippets.name}`).

***

## Template Control

### `templateEnabled` (default: `true`)

Controls whether the Qute template engine resolves template markers inside the snippet content.

**When `true` (default):** Template variables in the snippet are resolved against the full template data map. This allows snippets to be dynamic:

```json
{
  "name": "personalized_greeting",
  "content": "Address the user as {properties.preferred_name} and respond in {properties.preferred_language}.",
  "templateEnabled": true
}
```

**When `false`:** the content is wrapped in a Qute unparsed block (`{|...|}`) automatically, so `{...}` markers inside it reach the model literally instead of being resolved. This is useful for code examples or documentation snippets:

```json
{
  "name": "code_example_instructions",
  "content": "When showing code examples, use the format: {variable_name} for placeholders.",
  "templateEnabled": false
}
```

### Inline Override

Even when `templateEnabled` is `true`, you can protect specific sections with a Qute unparsed block directly in the content:

```json
{
  "name": "mixed_content",
  "content": "Hello {properties.name}! {|Use {placeholder} in templates.|}",
  "templateEnabled": true
}
```

***

## REST API Reference

All endpoints require `eddi-admin` or `eddi-editor` role.

| Method   | Path                                    | Description                          |
| -------- | --------------------------------------- | ------------------------------------ |
| `GET`    | `/snippetstore/snippets/descriptors`    | List snippet descriptors (paginated) |
| `GET`    | `/snippetstore/snippets/{id}?version=1` | Read a snippet                       |
| `POST`   | `/snippetstore/snippets`                | Create a snippet                     |
| `PUT`    | `/snippetstore/snippets/{id}?version=1` | Update a snippet                     |
| `DELETE` | `/snippetstore/snippets/{id}?version=1` | Delete a snippet                     |

### Query Parameters for Descriptors

| Param    | Default | Description                      |
| -------- | ------- | -------------------------------- |
| `filter` | `""`    | Filter by name (substring match) |
| `index`  | `0`     | Pagination offset                |
| `limit`  | `20`    | Max results                      |

***

## Model Reference

```json
{
  "name": "string (required, [a-z0-9_]+)",
  "category": "string (optional: governance, persona, compliance, custom)",
  "description": "string (optional, for UI gallery)",
  "content": "string (required, the prompt text)",
  "tags": ["string array (optional, for filtering)"],
  "templateEnabled": "boolean (default: true)"
}
```

***

## Example Snippets

### Governance — Cautious Mode

```json
{
  "name": "cautious_mode",
  "category": "governance",
  "description": "Instructs the agent to verify facts and avoid fabrication",
  "content": "CRITICAL SAFETY INSTRUCTION: You must always verify facts before responding. If uncertain, explicitly state your uncertainty. Never fabricate citations, statistics, or technical details. If a question is outside your knowledge, redirect the user to appropriate resources.",
  "tags": ["safety", "production", "enterprise"],
  "templateEnabled": true
}
```

Usage: `{snippets.cautious_mode}`

### Persona — Formal Tone

```json
{
  "name": "tone_formal",
  "category": "persona",
  "description": "Enforces formal business communication style",
  "content": "COMMUNICATION STYLE: Use formal, professional language at all times. Address users respectfully. Avoid slang, contractions, and casual expressions. Structure responses with clear headings when appropriate.",
  "tags": ["persona", "enterprise"],
  "templateEnabled": true
}
```

### Compliance — GDPR Notice

```json
{
  "name": "gdpr_notice",
  "category": "compliance",
  "description": "GDPR-compliant data handling instructions",
  "content": "DATA PRIVACY: You are operating under GDPR regulations. Never store or repeat personal data beyond the current conversation unless the user explicitly consents. If asked about data handling, refer to our privacy policy at {properties.privacy_policy_url}.",
  "tags": ["compliance", "gdpr", "eu"],
  "templateEnabled": true
}
```

### Dynamic — Context-Aware Routing

```json
{
  "name": "routing_context",
  "category": "custom",
  "description": "Injects department-specific instructions from properties",
  "content": "You are handling inquiries for the {properties.department} department. Follow these department-specific guidelines:\n{properties.department_guidelines}",
  "tags": ["routing", "dynamic"],
  "templateEnabled": true
}
```

***

## Composing a Full System Prompt

Snippets give you full control over prompt composition order:

```
{snippets.tone_formal}

You are a customer service agent for Acme Corp.

{snippets.cautious_mode}

{snippets.gdpr_notice}

Your specialization is {properties.specialization}.

Important context:
{snippets.routing_context}
```

The designer controls exactly where each snippet appears, enabling precise prompt engineering.

***

## Migration from Legacy Services

| Legacy Service             | Snippet Replacement                                                  |
| -------------------------- | -------------------------------------------------------------------- |
| `CounterweightService`     | Create a `cautious_mode` snippet with your safety instructions       |
| `IdentityMaskingService`   | Create a `persona_instructions` snippet with masking rules           |
| `DeploymentContextService` | Create environment-specific snippets (`prod_rules`, `staging_rules`) |

The key advantage: snippets are **user-configurable** without code changes, versionable, and composable.


# Output Configuration

## Overview

**Output Configurations** define what your agent says to users. They are templates that are triggered by **actions** from Behavior Rules, making them the final step in EDDI's Lifecycle Pipeline.

### Role in the Lifecycle

```
User Input → Parser → Behavior Rules → Actions → Output Configuration → Response
```

When a Behavior Rule matches, it triggers **actions**. The Output Configuration contains pre-defined responses mapped to those actions, which are then sent to the user.

### Key Features

* **Action-Based**: Each output is mapped to a specific action name
* **Multiple Alternatives**: Provide multiple response variations for natural conversations
* **Quick Replies**: Suggest user responses with quick reply buttons
* **Occurrence Tracking**: Show different outputs based on how many times an action has been triggered
* **Templating Support**: Combine with output templating for dynamic responses

### How It Works

1. **Behavior Rule triggers action**: `actions: ["welcome"]`
2. **Output Configuration matches action**: Finds output with `action: "welcome"`
3. **Selects output variant**: Randomly chooses from `valueAlternatives` (if multiple exist)
4. **Returns to user**: Sends the selected output as agent response

## Configuration Structure

`Output Configurations` contain prepared sentences that the Agent replies to the user (depending on the `actions` coming from the `Behavior Rules`).

Simple Output Configuration looks like this:

```javascript
{
  "outputSet": [
    {
      "action": "welcome",
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "Welcome! I am E.D.D.I."
            }
          ]
        }
      ]
    }
  ]
}
```

The configuration contains an `array` of `outputSet`, which can contain one or more output objects.

The minimum amount of values that you need to provide in order be functional are **`action`** and **`outputs`.**

Now let's look at a more complex output configuration file:

```javascript
{
  "outputSet": [
    {
      "action": "welcome",
      "timesOccurred": 0,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "Welcome!"
            }
          ]
        },
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "I am E.D.D.I. How are you doing today?"
            }
          ]
        }
      ],
      "quickReplies": [
        {
          "value": "I am fine",
          "expressions": "feeling(fine)"
        },
        {
          "value": "not so good",
          "expressions": "feeling(not_good)"
        }
      ]
    },
    {
      "action": "greet",
      "timesOccurred": 0,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "Hi there! Nice to meet up! :-)"
            },
            {
              "type": "text",
              "text": "Hello you! It is a pleasure meeting you.. :-)"
            }
          ]
        }
      ]
    },
    {
      "action": "greet",
      "timesOccurred": 1,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "Did we already say hi ?! Well, twice is better than not at all! ;-)"
            },
            {
              "type": "text",
              "text": "I like it if people are polite and greet twice, rather than not at all ;-)"
            }
          ]
        }
      ]
    },
    {
      "action": "say_goodbye",
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "See you soon!"
            }
          ]
        }
      ]
    }
  ]
}
```

### Explanation of model

| Key           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| action        | This will be the "`actions`" coming from the `Behavior Rules`. If a rule succeeds, the defined action will be stored in the **conversation memory.**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| outputs       | This array of output objects are the outputs that will be replied back to the user in case the `action` matched the `action key`. You can define multiple `output objects`, which represent separate chat bubbles on the client side. If more than one `valueAlternatives` is defined, one will be picked randomly. If this `output` will be triggered again in a future `conversationStep`, then another `output` of this array will be favored in order to avoid repetition. (If all available `outputs` have been selected, it is randomized again like in the beginning). The `type` is mainly there to be able to distinguish the type of output on the client side (e.g. `image`, `video`, `audio`, etc). |
| quickReplies  | This is an `array` of `QuickReply objects`. Each `object` must contain a value, which is the text that should be displayed to the user (e.g. as button) in the conversation flow. The `expressions` is `optional`, you can define one or more comma separated expressions that define the meaning of this `QuickReply`. Those `expressions` will be temporarily taken into account in the `semantic parser` in the next `conversationStep`. So if a user chooses one of the quick replies, the parser would recognize them (even if not defined in any of the `dictionaries` explicitly) and resolve them with the `expressions` defined within this quick reply.                                               |
| timesOccurred | How often this `action` should have occurred within that `conversation` in order to be selected as `output` to the user (thus, if `value` of `1`, it would be chosen if the action occurs for the second time)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |


# Output Templating

## Overview

**Output Templating** is one of EDDI's most powerful features—it allows you to create **dynamic, data-driven responses** that pull information from conversation memory, API responses, or context data. Instead of static text, your agent can generate personalized, contextual replies.

### Why Output Templating Matters

Without templating:

```
"The weather is available"
```

With templating:

```
"The weather in {context.city} is {memory.current.httpCalls.weatherData.condition} with {memory.current.httpCalls.weatherData.temperature}°F"
```

Result:

```
"The weather in Paris is sunny with 75°F"
```

### Powered by Qute

The **output templating** is evaluated by the **Quarkus Qute templating engine**, which provides native image compatibility and a clean, expressive syntax.

> **Note:** EDDI v6 migrated from Thymeleaf to Qute. Existing templates are automatically migrated via `V6QuteMigration` (startup) and the import pipeline.

### Common Use Cases

* **Personalization**: Greet users by name: `"Hello {context.userName}!"`
* **API Response Formatting**: Display data from HTTP calls: `"Your order #{httpCalls.orderData.orderId} is on the way"`
* **Conditional Outputs**: `"{#if user.isPremium}Exclusive offer for you!{/if}"`
* **Iteration**: Loop through arrays: `"Available options: {#for opt in options}{opt}{#if opt_hasNext}, {/if}{/for}"`

### What You Can Access

In templates, you have access to:

* **`memory.current.*`** - Current step data (input, httpCalls, properties)
* **`memory.previous.*`** - Previous step data
* **`context.*`** - Context passed from your application
* **`properties.*`** - Conversation properties (stored data)
* **`httpCalls.*`** - Responses from external APIs

## Configuration

One of the coolest features of **EDDI** is it will allow you dynamically template your output based on data that you would receive from `httpCalls` or `context information`, making **EDDI's** replies rich and dynamic.

## Enabling the feature:

While creating the agent you must include `eddi://ai.labs.output` to one of the `workflows` that will be part of the agent.

> **Important:** The templating feature will not work if it is included before `eddi://ai.labs.output` extension, **it must be included after**.

## Example

Here is how the output templating should be specified **inside of a workflow.**

```javascript
{
  "workflowExtensions": [
    {
      "type": "eddi://ai.labs.output",
      "config": {
        "uri": "eddi://ai.labs.output/outputstore/outputsets/{{outputset_id}}?version=1"
      }
    },
    {
      "type": "eddi://ai.labs.templating"
    }
  ]
}
```

Make sure the templating is defined after the output, not before.

## Template Syntax Reference

### Variable Output

```
{variableName}
{object.nested.property}
```

### Conditionals

```
{#if condition}
  Content shown when true
{#else}
  Content shown when false
{/if}
```

### Iteration

```
{#for item in items}
  {item.name}
  {#if item_hasNext}, {/if}
{/for}
```

**Iteration metadata** available inside `{#for}`:

| Variable           | Description                   |
| ------------------ | ----------------------------- |
| `item_index`       | 0-based index                 |
| `item_indexParity` | `odd` or `even`               |
| `item_hasNext`     | `true` if not the last item   |
| `item_count`       | Total items in the collection |
| `item_isFirst`     | `true` if first item          |
| `item_isLast`      | `true` if last item           |

### String Methods

All standard String methods are available as natural method calls on string variables:

| Expression                    | Description          |
| ----------------------------- | -------------------- |
| `{str.toLowerCase()}`         | Convert to lowercase |
| `{str.toUpperCase()}`         | Convert to uppercase |
| `{str.replace('old', 'new')}` | Replace substring    |
| `{str.substring(5)}`          | Substring from index |
| `{str.substring(0, 5)}`       | Substring range      |
| `{str.indexOf('x')}`          | Find character index |
| `{str.contains('sub')}`       | Check if contains    |
| `{str.startsWith('pre')}`     | Check prefix         |
| `{str.endsWith('suf')}`       | Check suffix         |
| `{str.trim()}`                | Trim whitespace      |
| `{str.length()}`              | String length        |
| `{str.isEmpty()}`             | Empty check          |

**Chaining** is supported: `{name.replace(' ', '-').toLowerCase()}`

## Custom Expression Utilities

EDDI provides custom namespace extensions for use in templates (output, httpcalls, property setters).

### `uuidUtils` — ID & URI Utilities

| Expression                                | Description                                           |
| ----------------------------------------- | ----------------------------------------------------- |
| `{uuidUtils:generateUUID()}`              | Generates a random UUID string                        |
| `{uuidUtils:extractId(locationUri)}`      | Extracts the resource ID from an EDDI location URI    |
| `{uuidUtils:extractVersion(locationUri)}` | Extracts the version number from an EDDI location URI |

**`extractId` and `extractVersion`** work with both MongoDB ObjectIds (24-char hex) and PostgreSQL UUIDs (36-char with dashes):

```
// Input: "http://localhost:7070/behaviorstore/behaviorsets/6740832a2b0f614abcaee7ab?version=1"
{uuidUtils:extractId(properties.location)}     → "6740832a2b0f614abcaee7ab"
{uuidUtils:extractVersion(properties.location)} → "1"
```

> **Important:** Avoid using `.substring()` with hardcoded offsets to extract IDs from URIs—use `{uuidUtils:extractId(...)}` instead. Hardcoded offsets break when switching between MongoDB (24-char ObjectIds) and PostgreSQL (36-char UUIDs).

### Other Custom Extensions

| Namespace | Expression               | Purpose                     |
| --------- | ------------------------ | --------------------------- |
| `json`    | `{json:serialize(obj)}`  | JSON manipulation utilities |
| `encoder` | `{encoder:base64(data)}` | Text encoding utilities     |

## Migration from Thymeleaf (v5 → v6)

If you are upgrading from EDDI v5, template syntax is automatically migrated:

| v5 (Thymeleaf)                    | v6 (Qute)                   |
| --------------------------------- | --------------------------- |
| `[[${variable}]]`                 | `{variable}`                |
| `[(${variable})]`                 | `{variable}`                |
| `[# th:each="x : ${list}"]...[/]` | `{#for x in list}...{/for}` |
| `[# th:if="${condition}"]...[/]`  | `{#if condition}...{/if}`   |
| `#strings.toLowerCase(var)`       | `{var.toLowerCase()}`       |
| `#strings.substring(var, 37)`     | `{var.substring(37)}`       |
| `#uuidUtils.method()`             | `{uuidUtils:method()}`      |
| `#json.method()`                  | `{json:method()}`           |
| `a + '/' + b`                     | `{a}/{b}`                   |

## ***Additional Information:***

[Quarkus Qute documentation.](https://quarkus.io/guides/qute-reference)


# Semantic Parser

## Overview

The **Pattern Matcher** (historically called "Semantic Parser") is EDDI's input classification system that transforms raw user input into **structured expressions** for agent routing and orchestration decisions.

**Role in Multi-Agent Orchestration:**

* **Route to Agents**: Match input patterns to determine which AI agent should handle the request
* **Categorize Requests**: Classify user intent for orchestration rules (e.g., "support" → support agent, "sales" → sales agent)
* **Whitelist Patterns**: Define allowed vocabulary and patterns for security and compliance
* **Extract Parameters**: Pull structured data from input for agent context

**What it actually does:**

* Matches words and phrases from dictionaries
* Applies fuzzy matching corrections (typos, phonetics, merged terms)
* Converts matched patterns to expression strings
* Enables pattern-based orchestration logic

**What it's NOT:**

* Not natural language understanding (NLU)
* Not machine learning-based
* Not semantic meaning extraction
* Not context-aware interpretation

### Role in Orchestration Pipeline

```
User Input: "I need technical support"
    ↓
Pattern Matcher (using dictionaries)
    ↓
Expressions: "intent(support),category(technical)"
    ↓
Orchestration Rules evaluate expressions
    ↓
Route to: Technical Support Agent (specific LLM or API)
```

The pattern matcher is the **first step** in the Orchestration Pipeline after receiving user input.

### Why Use Pattern Matching for Agent Orchestration?

**Without pattern matching** (hardcoded routing):

```javascript
if (
  input.contains("support") ||
  input.contains("help") ||
  input.contains("issue")
) {
  if (input.contains("billing") || input.contains("payment")) {
    routeToAgent("billing-support");
  } else if (input.contains("technical") || input.contains("bug")) {
    routeToAgent("technical-support");
  }
}
```

Brittle, hard to maintain, requires code changes for new routing rules!

**With pattern matching** (dictionary-based orchestration):

```json
// Dictionary: Support Category Classification
{
  "lang": "en",
  "words": [
    {
      "word": "billing",
      "expressions": "category(billing),intent(support)",
      "frequency": 0
    },
    {
      "word": "payment",
      "expressions": "category(billing),intent(support)",
      "frequency": 0
    },
    {
      "word": "bug",
      "expressions": "category(technical),intent(support)",
      "frequency": 0
    },
    {
      "word": "technical",
      "expressions": "category(technical)",
      "frequency": 0
    }
  ]
}
```

```json
// Orchestration Rule: Route based on category
{
  "behaviorRules": [
    {
      "name": "Route to Billing Agent",
      "conditions": [
        {
          "type": "inputmatcher",
          "configs": { "expressions": "category(billing)" }
        }
      ],
      "actions": ["agent(billing-specialist)"]
    },
    {
      "name": "Route to Technical Agent",
      "conditions": [
        {
          "type": "inputmatcher",
          "configs": { "expressions": "category(technical)" }
        }
      ],
      "actions": ["agent(technical-expert)"]
    }
  ]
}
```

**Agent Orchestration Benefits:**

* **Declarative Routing**: Define routing in configuration, not code
* **Multi-Agent Coordination**: Same input can trigger multiple agents
* **Dynamic Agent Selection**: Change routing rules at runtime
* **Pattern Reusability**: Share vocabularies across agent configurations
* **Fuzzy Matching**: Handle user typos and variations automatically

### Key Components

1. **Dictionaries**: Define words/phrases and their classification
   * `"billing"` → `category(billing),intent(support)`
   * `"technical issue"` → `category(technical),intent(support)`
   * Used for agent routing and request classification
2. **Built-in Dictionaries**: Pre-configured for common patterns

   * **Integer**: `"42"` → `integer(42)`
   * **Decimal**: `"3.14"` → `decimal(3.14)`
   * **Email**: `"user@example.com"` → `email(user@example.com)`
   * **Time**: `"13:43"` → `time(<epoch-millis>)` — 24-hour clock only, no am/pm and no relative dates
   * **Punctuation**: `"!"` → `punctuation(exclamation_mark)`
   * **Ordinal Number**: `"1st"` → `ordinal_number(1)`

   See [Dictionary Types Reference](#dictionary-types-reference) for the exact emitted expression names.
3. **Corrections**: Handle typos and variations
   * **Levenshtein**: `"helo"` → `"hello"` (distance 1-2 characters)
   * **Phonetic**: `"nite"` → `"night"`
   * **Merged Terms**: Handles words without spaces

### Example Flow: Agent Routing

**User Input**: "I need help with a billing issue"

**Pattern Matcher Processing**:

1. Tokenizes: `["I", "need", "help", "with", "a", "billing", "issue"]`
2. Looks up in dictionaries:
   * `"help"` → `intent(support)`
   * `"billing"` → `category(billing)`
   * `"issue"` → `type(problem)`
3. Applies corrections (if needed)
4. Produces expressions: `intent(support),category(billing),type(problem)`

**Orchestration Rule** routes to appropriate agent:

```json
{
  "conditions": [
    {
      "type": "inputmatcher",
      "configs": {
        "expressions": "category(billing)",
        "occurrence": "currentStep"
      }
    }
  ],
  "actions": ["route_to_billing_agent"]
}
```

**Result**: Request is routed to specialized billing support agent (could be a specific LLM configuration, a human agent queue, or a billing API).

## Creating a Regular Dictionary

Regular dictionaries define custom words and phrases for agent routing. We'll create a dictionary and then configure a parser to use it.

### Step 1: Create a Regular Dictionary for Agent Routing

Make a `POST` request to `/dictionarystore/dictionaries` with this JSON:

```json
{
  "lang": "en",
  "words": [
    {
      "word": "support",
      "expressions": "intent(support)",
      "frequency": 0
    },
    {
      "word": "help",
      "expressions": "intent(support)",
      "frequency": 0
    },
    {
      "word": "billing",
      "expressions": "category(billing)",
      "frequency": 0
    },
    {
      "word": "technical",
      "expressions": "category(technical)",
      "frequency": 0
    },
    {
      "word": "sales",
      "expressions": "category(sales)",
      "frequency": 0
    }
  ],
  "phrases": [
    {
      "phrase": "technical support",
      "expressions": "intent(support),category(technical)"
    },
    {
      "phrase": "billing question",
      "expressions": "intent(inquiry),category(billing)"
    },
    {
      "phrase": "sales inquiry",
      "expressions": "intent(inquiry),category(sales)"
    }
  ]
}
```

**Request:**

```bash
curl -X POST http://localhost:7070/dictionarystore/dictionaries \
  -H "Content-Type: application/json" \
  -d '{
    "lang": "en",
    "words": [
      {"word": "billing", "expressions": "category(billing),intent(support)", "frequency": 0}
    ],
    "phrases": [
      {"phrase": "technical support", "expressions": "intent(support),category(technical)"}
    ]
  }'
```

**Response:** HTTP `201 Created`

The response's `Location` header contains the URI of the created dictionary:

```
Location: http://localhost:7070/dictionarystore/dictionaries/DICT_ID?version=1
```

This gives you the reference URI:

```
eddi://ai.labs.dictionary/dictionarystore/dictionaries/DICT_ID?version=1
```

**Key Points:**

* `lang`: ISO language code (e.g., `"en"`, `"de"`, `"fr"`) — **this is a filter, not just an annotation** (see the upgrade note below)
* `word`: The actual word to match
* `expressions`: Classification/routing information (can have multiple, comma-separated)
* `frequency`: Usage frequency (0 = common, higher = less common)
* `phrases`: Multi-word expressions treated as single units

> **Upgrade note — `lang` now gates the dictionary.** In earlier releases `lang` was recorded but never evaluated: every dictionary was consulted for every turn. From v6.x on, a dictionary whose `lang` is set is only consulted when it matches the conversation's language — on the direct lookup **and** on the corrections path (Levenshtein, phonetic, merged terms), so a mismatched dictionary can no longer sneak back in through a typo correction.
>
> The conversation language comes from the `lang` conversation property and defaults to `"en"` when that property is not set. So a deployment with, say, a `"de"` dictionary and no `lang` property recognises nothing after the upgrade. Two ways to keep the pre-upgrade behaviour:
>
> * leave `lang` unset (or empty) on the dictionary — an unset language means "applies to every language"; or
> * set the `lang` conversation property (e.g. via a property setter or the request context) to the dictionary's language.

### Step 2: Create a Parser Configuration

Now create a parser that uses your dictionary along with built-in dictionaries.

Make a `POST` request to `/parserstore/parsers` with this JSON:

> **Important:** Replace `<DICT_ID>` with your dictionary ID from Step 1!

## Example Parser Configuration

```json
{
  "extensions": {
    "dictionaries": [
      {
        "type": "eddi://ai.labs.parser.dictionaries.integer"
      },
      {
        "type": "eddi://ai.labs.parser.dictionaries.decimal"
      },
      {
        "type": "eddi://ai.labs.parser.dictionaries.punctuation"
      },
      {
        "type": "eddi://ai.labs.parser.dictionaries.email"
      },
      {
        "type": "eddi://ai.labs.parser.dictionaries.time"
      },
      {
        "type": "eddi://ai.labs.parser.dictionaries.ordinalNumber"
      },
      {
        "type": "eddi://ai.labs.parser.dictionaries.regular",
        "config": {
          "uri": "eddi://ai.labs.dictionary/dictionarystore/dictionaries/<DICT_ID>?version=1"
        }
      }
    ],
    "corrections": [
      {
        "type": "eddi://ai.labs.parser.corrections.levenshtein",
        "config": {
          "distance": "2"
        }
      },
      {
        "type": "eddi://ai.labs.parser.corrections.mergedTerms"
      }
    ]
  },
  "config": null
}
```

**Request:**

```bash
curl -X POST http://localhost:7070/parserstore/parsers \
  -H "Content-Type: application/json" \
  -d '{
    "extensions": {
      "dictionaries": [
        {"type": "eddi://ai.labs.parser.dictionaries.integer"},
        {"type": "eddi://ai.labs.parser.dictionaries.regular",
         "config": {"uri": "eddi://ai.labs.dictionary/dictionarystore/dictionaries/DICT_ID?version=1"}}
      ],
      "corrections": [
        {"type": "eddi://ai.labs.parser.corrections.levenshtein", "config": {"distance": "2"}}
      ]
    }
  }'
```

**Response:** HTTP `201 Created`

The response's `Location` header contains the parser URI:

```
Location: http://localhost:7070/parserstore/parsers/PARSER_ID?version=1
```

This gives you the parser reference:

```
eddi://ai.labs.parser/parserstore/parsers/PARSER_ID?version=1
```

### Dictionary Types Reference

> **These are the exact expression names the parser emits.** Behavior rules match on the emitted name, so a rule written against a different name never fires — copy the names from this table verbatim.

| Type           | EDDI URI                                           | Description                                                                                                                     | Example                                                     |
| -------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| Integer        | `eddi://ai.labs.parser.dictionaries.integer`       | Matches positive integers                                                                                                       | `"42"` → `integer(42)`                                      |
| Decimal        | `eddi://ai.labs.parser.dictionaries.decimal`       | Matches decimal numbers (both `.` and `,` separators)                                                                           | `"3.14"` → `decimal(3.14)`                                  |
| Punctuation    | `eddi://ai.labs.parser.dictionaries.punctuation`   | Matches common punctuation: `!` (exclamation\_mark), `?` (question\_mark), `.` (dot), `,` (comma), `:` (colon), `;` (semicolon) | `"!"` → `punctuation(exclamation_mark)`                     |
| Email          | `eddi://ai.labs.parser.dictionaries.email`         | Matches email addresses                                                                                                         | `"user@example.com"` → `email(user@example.com)`            |
| Time           | `eddi://ai.labs.parser.dictionaries.time`          | Matches 24-hour clock formats only: `13:43`, `13:43:23`, `01h20`, `22h`. **No am/pm parsing** — `"3pm"` is not a time.          | `"13:43"` → `time(<epoch-millis>)`                          |
| Ordinal Number | `eddi://ai.labs.parser.dictionaries.ordinalNumber` | Ordinal numbers, either in English suffix notation (1st, 2nd, 3rd, …) or in dot notation (`3.`, at most two digits)             | `"1st"` → `ordinal_number(1)`, `"3."` → `ordinal_number(3)` |
| Regular        | `eddi://ai.labs.parser.dictionaries.regular`       | Custom dictionary for agent routing                                                                                             | `"billing"` → `category(billing)`                           |

> **Dot notation affects sentence-final numbers.** Because `"5."` is an ordinal number, an English sentence ending in a number — `"I want 5."` — now yields `ordinal_number(5)` for the last token where it previously yielded `unknown`. Conversely a bare `"."` is no longer treated as an ordinal and is normalised as punctuation. Only enable the ordinal-number dictionary when you actually want that reading.

> **Time values are epoch milliseconds, not a formatted clock string.** The matched token is converted to a `java.sql.Time` and the expression carries `Time#getTime()` — e.g. `"13:43"` becomes something like `time(45780000)` (the exact number depends on the JVM's time zone). Match on the presence of `time(*)` rather than on a literal value.

### Correction Types Reference

| Type         | EDDI URI                                        | Description                                      | Example                            |
| ------------ | ----------------------------------------------- | ------------------------------------------------ | ---------------------------------- |
| Levenshtein  | `eddi://ai.labs.parser.corrections.levenshtein` | Matches words with typos (configurable distance) | `"helo"` → `"hello"` (distance=1)  |
| Phonetic     | `eddi://ai.labs.parser.corrections.phonetic`    | Matches phonetically similar words               | `"nite"` → `"night"`               |
| Merged Terms | `eddi://ai.labs.parser.corrections.mergedTerms` | Handles words without spaces                     | `"techsupport"` → `"tech support"` |

**Levenshtein config keys**

| Key             | Default | Description                                                                                                                                                                            |
| --------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `distance`      | `2`     | Maximum edit distance a dictionary word may have from the input token.                                                                                                                 |
| `maxCandidates` | `5`     | Upper bound on how many correction candidates one token may produce. Every candidate becomes another branch in the parser's match matrix, so raising this multiplies the search space. |

Candidates are sorted by edit distance first, so the closest matches survive the cap. A missing, non-numeric or non-positive value falls back to the default.

> **Corrections respect the dictionary language too.** A dictionary whose `lang` does not match the conversation language is skipped by the corrections exactly as it is skipped by the direct lookup — otherwise a foreign-language word would come back as a "correction" at distance 0 for every unknown token.

> **There is no stemming correction.** EDDI ships exactly the three corrections above; referencing `eddi://ai.labs.parser.corrections.stemming` (or any other unregistered extension URI) makes workflow initialization fail with `UnrecognizedExtensionException` and the agent will not start.

## Testing the Pattern Matcher

Once you've created both dictionary and parser, you can test it standalone.

Make a `POST` request to `/parser/{PARSER_ID}?version={VERSION}` with plain text in the body:

**Request:**

```bash
curl -X POST "http://localhost:7070/parser/PARSER_ID?version=1" \
  -H "Content-Type: text/plain" \
  -d "I need billing support"
```

**Response:**

```json
[
  {
    "expressions": "intent(support),category(billing)"
  }
]
```

The parser returns an array of solutions, where each solution contains expressions representing the classification of the input.

## Using Pattern Matcher in Agent Orchestration

To use the pattern matcher in your agent orchestration, add it as the first step of your workflow configuration:

```json
{
  "workflowSteps": [
    {
      "type": "eddi://ai.labs.parser",
      "extensions": {
        "dictionaries": [
          {
            "type": "eddi://ai.labs.parser.dictionaries.regular",
            "config": {
              "uri": "eddi://ai.labs.dictionary/dictionarystore/dictionaries/DICT_ID?version=1"
            }
          }
        ],
        "corrections": [
          {
            "type": "eddi://ai.labs.parser.corrections.levenshtein",
            "config": {
              "distance": "2"
            }
          }
        ]
      },
      "config": {
        "includeUnknown": true,
        "includeUnused": true
      }
    }
  ]
}
```

**Configuration Options** (all live under the step's `config` object):

| Option              | Default | Description                                                                  |
| ------------------- | ------- | ---------------------------------------------------------------------------- |
| `includeUnknown`    | `true`  | Include `unknown(...)` expressions for unrecognized words                    |
| `includeUnused`     | `true`  | Include `unused(...)` expressions for words that matched no dictionary entry |
| `appendExpressions` | `true`  | Append the freshly parsed expressions to the ones already in the step        |
| `maxInputTokens`    | `200`   | Hard cap on tokens taken from one input; anything beyond is dropped          |
| `maxSuggestions`    | `1000`  | Hard cap on dictionary suggestions evaluated per input                       |
| `maxSolutions`      | `100`   | Hard cap on solutions collected per input                                    |

The three `max*` limits guard against pathological inputs. Values below `1` are ignored and fall back to the default.

## Complete Example: Multi-Agent Customer Service Orchestration

Let's build an agent routing system for customer service:

### 1. Create Dictionary for Agent Routing

```json
{
  "lang": "en",
  "words": [
    {
      "word": "billing",
      "expressions": "category(billing),intent(support)",
      "frequency": 0
    },
    {
      "word": "payment",
      "expressions": "category(billing),intent(support)",
      "frequency": 0
    },
    {
      "word": "invoice",
      "expressions": "category(billing),intent(inquiry)",
      "frequency": 0
    },
    {
      "word": "technical",
      "expressions": "category(technical)",
      "frequency": 0
    },
    {
      "word": "bug",
      "expressions": "category(technical),intent(support)",
      "frequency": 0
    },
    {
      "word": "feature",
      "expressions": "category(technical),intent(inquiry)",
      "frequency": 0
    },
    {
      "word": "sales",
      "expressions": "category(sales),intent(inquiry)",
      "frequency": 0
    },
    {
      "word": "pricing",
      "expressions": "category(sales),intent(inquiry)",
      "frequency": 0
    },
    {
      "word": "demo",
      "expressions": "category(sales),intent(inquiry)",
      "frequency": 0
    }
  ],
  "phrases": [
    {
      "phrase": "billing issue",
      "expressions": "category(billing),intent(support),urgency(high)"
    },
    {
      "phrase": "technical problem",
      "expressions": "category(technical),intent(support),urgency(high)"
    },
    {
      "phrase": "interested in",
      "expressions": "category(sales),intent(inquiry)"
    }
  ]
}
```

### 2. User Says: "I have a billing issue"

### 3. Pattern Matcher Output:

```json
[{ "expressions": "category(billing),intent(support),urgency(high)" }]
```

### 4. Orchestration Rule Routes to Agent:

```json
{
  "name": "Route Billing Issues to Specialist",
  "conditions": [
    {
      "type": "inputmatcher",
      "configs": { "expressions": "category(billing)" }
    }
  ],
  "actions": ["agent(billing-specialist)", "set_priority(high)"]
}
```

### 5. Result:

* Request routed to Billing Specialist Agent (e.g., GPT-4 with billing context)
* Priority set to high for escalation tracking
* Conversation context includes category and intent for agent

## Best Practices for Agent Orchestration

1. **Use Category-Based Routing**: `category(billing)` is better than `entity(invoice)`
2. **Combine Intent + Category**: `intent(support),category(technical)` enables flexible routing
3. **Define Urgency Levels**: `urgency(high)` helps prioritize agent allocation
4. **Test Thoroughly**: Use the `/parser` endpoint to verify routing classifications
5. **Start Broad, Then Specialize**: Begin with major categories, add subcategories as needed
6. **Document Expression Schema**: Keep a reference of all category/intent/urgency values used
7. **Version Dictionaries**: Use version control for routing changes

## Troubleshooting

**Problem**: Requests not routing to expected agent\
**Solution**: Test pattern matcher output - verify expressions match orchestration rules

**Problem**: Too many unknown expressions\
**Solution**: Add more words to dictionary or enable fuzzy corrections

**Problem**: Multiple agents triggered for same input\
**Solution**: Make conditions more specific or add rule priority

**Problem**: Corrections too aggressive (wrong routing)\
**Solution**: Reduce Levenshtein distance or disable specific corrections

> **Example:** To reduce the Levenshtein distance threshold for fuzzy matching, lower `distance` on the correction entry in the parser configuration (there is no YAML configuration file — parsers are JSON documents stored via `/parserstore/parsers`):
>
> ```json
> {
>   "type": "eddi://ai.labs.parser.corrections.levenshtein",
>   "config": {
>     "distance": "1"
>   }
> }
> ```

> **Note:** The pattern matcher is optimized for conversational inputs, not full-text documents. Design dictionaries for typical user queries.

## Related Documentation

* [Behavior Rules](/agent-configuration/behavior-rules) - Using expressions for agent routing
* [Architecture Overview](/architecture-and-concepts/architecture) - Understanding the orchestration pipeline
* [LangChain Integration](/agent-configuration/langchain) - Configuring AI agents
* [HTTP Calls](/agent-configuration/httpcalls) - Integrating business system agents


# Passing Context Information

## Overview

**Context** is external data that you pass from your application into EDDI conversations. It's how you inject real-world information—like user profiles, session data, or business state—into your agent's logic without hard-coding it.

### Why Context Matters

Context enables your agents to:

* **Personalize responses**: Use user names, preferences, account details
* **Make business decisions**: Check user roles, subscription status, account balances
* **Maintain session state**: Pass authentication tokens, session IDs
* **Adapt behavior**: Change agent responses based on time of day, location, language
* **Integrate with your systems**: Bring data from your CRM, database, or services

### Context vs Conversation Memory

| Aspect        | Context                     | Conversation Memory            |
| ------------- | --------------------------- | ------------------------------ |
| **Source**    | Your application (external) | EDDI (internal)                |
| **Direction** | Input to EDDI               | Managed by EDDI                |
| **Lifetime**  | Per request                 | Persistent across conversation |
| **Purpose**   | Inject external data        | Store conversation history     |
| **Usage**     | `${context.userName}`       | `${memory.current.input}`      |

### Context Types

EDDI supports three context types:

1. **`string`**: Simple text values

   ```json
   "userRole": {"type": "string", "value": "premium"}
   ```
2. **`object`**: Structured JSON data

   ```json
   "userInfo": {"type": "object", "value": {"name": "John", "age": 30}}
   ```
3. **`expressions`**: Parsed semantic expressions

   ```json
   "intent": {"type": "expressions", "value": "purchase(product)"}
   ```

### How Context is Used

Once passed to EDDI, context can be:

* **Matched in Behavior Rules**: Conditions check context values
* **Used in Output Templates**: `{context.userName}`
* **Included in HTTP Call Bodies**: Pass to external APIs
* **Stored as Properties**: Save to conversation memory

### Example Flow

```
Your App → POST /agents/conv456
{
  "input": "What's my account balance?",
  "context": {
    "userId": {"type": "string", "value": "user-789"},
    "accountType": {"type": "string", "value": "premium"}
  }
}

→ EDDI Behavior Rule checks context:
   IF context.accountType = "premium" THEN httpcall(get-balance)

→ HTTP Call uses context:
   GET /api/accounts/${context.userId}/balance

→ Output Template uses context:
   "Hello! Your premium account balance is ${httpCalls.balance.amount}"

→ Response to Your App:
   "Hello! Your premium account balance is $1,250.00"
```

## Sending Context to Conversations

In this section we will explain how **EDDI** handles the context of a conversation and which data can be passed within the scope of a conversation.

In order to talk to **EDDI** with context, send a **`POST`** request to `/agents/`**`{conversationId}`** (same way as interacting in a normal conversation in EDDI), but this time provide context parameters:

### Send message in a conversation with an Agent REST API Endpoint

| Element                          | Tags                                                                                                                                                                                                                                                                                    |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| HTTP Method                      | `POST`                                                                                                                                                                                                                                                                                  |
| API endpoint                     | `/agents/{conversationId}`                                                                                                                                                                                                                                                              |
| {conversationId}                 | (`Path` **parameter**): `String Id` of the **conversation** that you wish to **send** the message to.                                                                                                                                                                                   |
| returnDetailed (Optional)        | (`Query` **parameter**):`Boolean` - Default : `false` Will return all sub results of the entire `conversation steps`, otherwise only public ones such as `input, action, output & quickReplies`.                                                                                        |
| returnCurrentStepOnly (Optional) | (`Query` **parameter**):`Boolean` - Default : `true` Will return only the latest `conversationStep` that has just been processed, otherwise returns all `conversationSteps` since the beginning of this `conversation`.                                                                 |
| Request Body                     | a `JSON` object sent in the request body consists of the usual input text (message to the agent) only this time we are going to provide `context` information through a `key value` data structure ; the Context value must have one of the following : `string,object or expressions.` |
|                                  |                                                                                                                                                                                                                                                                                         |

## Example

Here is an example of a `JSON` object of the input data:

```javascript
{
  "input": "",
  "context": {
    "onboardingOfUser": {
      "type": "string",
      "value": "true"
    },
    "userInfo": {
      "type": "object",
      "value": {
        "username": "Barbara"
      }
    }
  }
}
```

> You can also test context parameters in the **EDDI Manager** chat panel at `http://localhost:7070`.


# Multimodal Attachments

> EDDI's attachment pipeline enables multimodal conversations — users can send images, files, and documents alongside text input. Attachments flow through the lifecycle pipeline and are automatically forwarded to vision-capable LLMs.

## Quick Start

### Send an Image via URL

```bash
POST /agents/{conversationId}/say?message=What%20is%20in%20this%20image?
Content-Type: application/json

{
  "attachment_0": {
    "type": "object",
    "value": {
      "mimeType": "image/png",
      "url": "https://example.com/photo.png",
      "fileName": "photo.png"
    }
  }
}
```

### Send an Image via Base64

```bash
POST /agents/{conversationId}/say?message=Describe%20this%20icon
Content-Type: application/json

{
  "attachment_0": {
    "type": "object",
    "value": {
      "mimeType": "image/png",
      "data": "iVBORw0KGgoAAAANSUhEUgAAAAE...",
      "fileName": "icon.png"
    }
  }
}
```

The image is automatically forwarded to the LLM as multimodal content. The LLM "sees" the image alongside the text message.

***

## How It Works

```
Client sends context with attachment_* keys
           │
           ▼
┌──────────────────────────────────┐
│  Conversation.prepareLifecycleData()  │
│                                       │
│  AttachmentContextExtractor parses    │
│  attachment_0, attachment_1, ...      │
│  into List<Attachment> objects        │
│                                       │
│  Stored in memory: "attachments"      │
└──────────────┬───────────────────┘
               │
    ┌──────────┼──────────┐
    ▼          ▼          ▼
┌────────┐ ┌────────┐ ┌────────────┐
│BehaviorRules│ │LlmTask│ │Other Tasks │
│             │ │       │ │            │
│ContentType- │ │Multi- │ │Read from   │
│Matcher      │ │modal  │ │memory key  │
│condition    │ │Message│ │"attachments"│
│             │ │Enhancer││            │
└────────┘ └────────┘ └────────────┘
```

### Pipeline Stages

1. **Context Extraction** — `AttachmentContextExtractor` parses `attachment_*` context keys into `Attachment` objects
2. **Memory Storage** — Attachments are stored as `List<Attachment>` in the `attachments` memory key
3. **Rule Matching** — `ContentTypeMatcher` condition matches on MIME types for routing
4. **LLM Forwarding** — `MultimodalMessageEnhancer` converts attachments to langchain4j `ImageContent` and enhances the user message

***

## Input Paths

### Path A: URL Reference (Recommended)

Best for images already hosted somewhere. The LLM provider fetches the image directly from the URL.

```json
{
  "attachment_0": {
    "type": "object",
    "value": {
      "mimeType": "image/jpeg",
      "url": "https://cdn.example.com/photos/sunset.jpg",
      "fileName": "sunset.jpg"
    }
  }
}
```

### Path B: Base64 Inline

Best for small images (< 5MB). Data is sent inline as a base64-encoded string.

```json
{
  "attachment_0": {
    "type": "object",
    "value": {
      "mimeType": "image/png",
      "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk...",
      "fileName": "icon.png"
    }
  }
}
```

> **Note:** `base64Data` is transient — it's never persisted to MongoDB. For large files, use the upload endpoint (Path C below) or URL references.

### Path C: File Upload

For large files, upload them to the storage backend and receive a storage reference:

```bash
POST /conversations/{conversationId}/attachments
Content-Type: multipart/form-data

# Form field: file (the binary file)
```

**Response (201):**

```json
{
  "storageRef": "gridfs://68abc123def456",
  "fileName": "report.pdf",
  "mimeType": "application/pdf",
  "sizeBytes": 524288
}
```

The returned `storageRef` can then be used in subsequent conversation turns by setting it as the `url` in an attachment context key. The storage backend (GridFS or PostgreSQL) is selected automatically based on the configured datastore.

| Response Code | Meaning                          |
| ------------- | -------------------------------- |
| `201`         | File stored successfully         |
| `400`         | No file provided                 |
| `503`         | No attachment storage configured |

***

## Context Key Format

Attachment context keys must match the pattern `attachment_*`:

| Key                     | Valid?           |
| ----------------------- | ---------------- |
| `attachment_0`          | ✅                |
| `attachment_screenshot` | ✅                |
| `attachment_`           | ✅                |
| `image_0`               | ❌ (wrong prefix) |
| `attachment`            | ❌ (no suffix)    |

### Required Fields

| Field      | Required        | Description                                      |
| ---------- | --------------- | ------------------------------------------------ |
| `mimeType` | Yes             | MIME type (e.g., `image/png`, `application/pdf`) |
| `url`      | One of url/data | External URL reference                           |
| `data`     | One of url/data | Base64-encoded content                           |
| `fileName` | No              | Original filename (for metadata/logging)         |

If both `url` and `data` are present, `url` takes precedence.

***

## Multiple Attachments

Send multiple attachments by incrementing the key index:

```json
{
  "attachment_0": {
    "type": "object",
    "value": { "mimeType": "image/png", "url": "https://example.com/page1.png" }
  },
  "attachment_1": {
    "type": "object",
    "value": { "mimeType": "image/png", "url": "https://example.com/page2.png" }
  }
}
```

All attachments are forwarded to the LLM in a single multimodal user message.

***

## LLM Multimodal Support

The `MultimodalMessageEnhancer` automatically converts attachments to the appropriate langchain4j content type:

| MIME Type         | langchain4j Content                      | Provider Support                                |
| ----------------- | ---------------------------------------- | ----------------------------------------------- |
| `image/*`         | `ImageContent`                           | OpenAI GPT-4o, Gemini, Claude 3, Ollama (LLaVA) |
| `application/pdf` | Metadata text (future: `PdfFileContent`) | Gemini                                          |
| `audio/*`         | Metadata text (future: `AudioContent`)   | Gemini                                          |
| Other             | Metadata text description                | All (text-only)                                 |

For unsupported MIME types, a text description is injected so the LLM knows an attachment was present:

```
[Attachment: report.csv (text/csv, 15240 bytes)]
```

***

## Routing with Behavior Rules

Use `contentTypeMatcher` to create different workflows based on attachment type:

### Route Images to Vision Agent

```json
{
  "name": "Image received",
  "actions": ["analyze_image"],
  "conditions": [
    {
      "type": "contentTypeMatcher",
      "configs": {
        "mimeType": "image/*",
        "minCount": "1"
      }
    }
  ]
}
```

### Route PDFs to Document Processor

```json
{
  "name": "Document received",
  "actions": ["process_document"],
  "conditions": [
    {
      "type": "contentTypeMatcher",
      "configs": {
        "mimeType": "application/pdf",
        "minCount": "1"
      }
    }
  ]
}
```

### Require Specific Attachment Count

```json
{
  "name": "Comparison ready",
  "actions": ["compare_images"],
  "conditions": [
    {
      "type": "contentTypeMatcher",
      "configs": {
        "mimeType": "image/*",
        "minCount": "2"
      }
    }
  ]
}
```

***

## Template Access

Attachments are available in templates via the memory namespace:

```
Current step attachments: {memory.current.attachments}
```

This can be useful for logging, debugging, or constructing custom prompts that reference attachment metadata.

***

## Architecture Notes

* **No inline storage**: Attachment payloads are never stored inline in conversation memory documents. Only metadata references are persisted.
* **Transient base64**: The `base64Data` field is `transient` — it exists only during the pipeline turn. For persistence, use the upload endpoint with `IAttachmentStorage`.
* **DB-agnostic**: The `IAttachmentStorage` SPI supports MongoDB (GridFS) and PostgreSQL (bytea) implementations.
* **GDPR cleanup**: `IAttachmentStorage.deleteByConversation()` removes all attachments when a conversation is deleted.


# Conversations

## Overview

**Conversations** are the primary interaction mechanism in EDDI. Each conversation represents a stateful dialog session between a user and an agent, maintaining complete history, context, and state throughout the interaction.

### Key Concepts

* **Stateful Sessions**: Each conversation maintains its own state (conversation memory) that persists across multiple interactions
* **Conversation ID**: Unique identifier that references a specific conversation session
* **Lifecycle States**: Conversations transition through states: `READY`, `IN_PROGRESS`, `ENDED`, `ERROR`
* **History Management**: Full conversation history is maintained, with support for undo/redo operations
* **Context Passing**: External context can be injected into conversations at any step

### How Conversations Work in EDDI

When you create a conversation:

1. EDDI creates a new `IConversationMemory` object
2. Assigns a unique conversation ID
3. Links it to a specific agent and user
4. Initializes the first conversation step
5. Returns the conversation ID for subsequent interactions

Each message sent to a conversation:

1. Loads the conversation memory from MongoDB/cache
2. Executes the agent's lifecycle pipeline
3. Updates the conversation memory with results
4. Saves the updated memory
5. Returns the agent's response

> **Time Travel Feature**: EDDI has a powerful feature for conversations—the ability to go back in time using the `/undo` and `/redo` API endpoints!

## Working with Conversations

In this section we will explain how to **send/receive messages** from an Agent. The first step is creating a `conversation`. Once you have the `conversation` `Id`, you can **send** messages via **`POST`** requests and **receive** responses via **`GET`** requests, while having the capacity to send context information through the body of the **POST** request.

## Creating/initiating a conversation :

### Create a Conversation with an Agent REST API Endpoint

| Element          | Tags                                                                                          |
| ---------------- | --------------------------------------------------------------------------------------------- |
| **HTTP Method**  | `POST`                                                                                        |
| **API endpoint** | `/agents/{agentId}/start`                                                                     |
| {**agentId**}    | (`Path` **parameter**):`String Id` of the agent that you wish to **start conversation with**. |
| environment      | (`Query` **parameter**, optional):`String` Deployment environment. **Default**: `production`. |

### Response Model

```javascript
{
  "agentId": "string",
  "agentVersion": Integer,
  "userId": "string",
  "environment": "string",
  "conversationState": "string",
  "redoCacheSize": 0,
  "conversationOutputs": [
                "input"    :    "string",
                "expressions"    :    <arrayOfString>,
                "intents" :        <arrayOfString>,
                "actions"    :    <arrayOfString>,
                "httpCalls"    :    {JsonObject},
                "properties" :    <arrayOfString>,
                "output" : "string"
    ],
  "conversationProperties": {
        "<nameOfProperty>" : {
      "name" : "string",
      "value" : "string" | {JsonObject},
      "scope" : "string"
    }},
  "conversationSteps": [
    {
      "conversationStep": [
        {
          "key": "string",
          "value": {}
        }
      ],
      "timestamp": "dateTime"
    }
  ]
}
```

### Description of the Conversation response model

| Element                        | Tags                                                                                                                                                                                                                                                                              |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **agentId**                    | (`String`) The id of the agent that sent the reply.                                                                                                                                                                                                                               |
| agentVersion                   | (`integer`) The version of the agent that sent the reply.                                                                                                                                                                                                                         |
| userId                         | (`String`) The id of the user who interacted with the agent.                                                                                                                                                                                                                      |
| environment                    | (`String`) the name of the environment where the agent is deployed                                                                                                                                                                                                                |
| conversationState              | <p>(<code>String</code>) The state of the current conversation, could be:</p><p><code>READY</code>,</p><p><code>IN\_PROGRESS</code>,</p><p><code>ENDED</code>,</p><p><code>EXECUTION\_INTERRUPTED</code>,</p><p><code>ERROR</code></p>                                            |
| redoCount                      | (`integer`) if undo has been performed, this number indicates how many times redo can be done (=times undo has been triggered)                                                                                                                                                    |
| conversationOutputs            | (`Array`: <`conversationOutput`>) Array of `conversationOutput`                                                                                                                                                                                                                   |
| conversationOutput.input       | (`String`) The user's input.                                                                                                                                                                                                                                                      |
| conversationOutput.expressions | (`Array`: <`String`>) an array of the `expressions` involved in the creation of this reply (output).                                                                                                                                                                              |
| conversationOutput.intents     | (`Array`: <`String`>) an array of the `intents` involved in the creation of this reply (output).                                                                                                                                                                                  |
| conversationOutput.actions     | (`Array`: <`String`>) an array of the `actions` involved in the creation of this reply (output).                                                                                                                                                                                  |
| conversationOutput.httpCalls   | (`Array`: <`JsonObject`>) an array of the `httpCalls` objects involved in the creation of this reply (output).                                                                                                                                                                    |
| conversationOutput.properties  | (`Array`: <`JsonObject`>) the list of available properties in the current conversation.                                                                                                                                                                                           |
| conversationOutput.output      | (`String`) The final agent's output                                                                                                                                                                                                                                               |
| conversationProperties         | (`Array`: <>) Array of `conversationProperty`, <`nameOfProperty`> is a dynamic value that represents the name of the property                                                                                                                                                     |
| \<nameOfProperty>.name         | (`String`) name of the property.                                                                                                                                                                                                                                                  |
| \<nameOfProperty>.value        | (`String`\|`JsonObject`) value of the property.                                                                                                                                                                                                                                   |
| \<nameOfProperty>.scope        | <p>(<code>String</code>) scope can be</p><p><code>step</code> (=valid one interaction \[user input to user output]),</p><p><code>conversation</code> (=valid for the entire conversation),</p><p><code>longTerm</code> (=valid across conversations \[based on given userId])</p> |
| conversationSteps              | (`Array`: <`conversationStep`>) Array of `conversationStep`.                                                                                                                                                                                                                      |
| conversationStep.key           | (`String`) the element key in the conversationStep e.g key : input:initial, actions                                                                                                                                                                                               |
| conversationStep.value         | (`String`) the element value of the conversationStep e.g in case of `actionq` as `key` it could be an array string `[ "current_weather_in_city" ]`.                                                                                                                               |
| timestamp.timestamp            | (`dateTime`) the timestamp in (ISO 8601) format                                                                                                                                                                                                                                   |

> **Note** `conversationProperties` can also be used in output templating e.g: `{properties.city}.`

### Sample Response

```javascript
{
  "agentId": "5bf5418c46e0fb000b7636d0",
  "agentVersion": 10,
  "userId": "anonymous-zj1p1GDtM5",
  "environment": "production",
  "conversationState": "READY",
  "redoCacheSize": 0,
  "conversationOutputs": [
    {
      "input": "madrid",
      "expressions": "unknown(madrid)",
      "intents": [
        "unknown"
      ],
      "actions": [
        "current_weather_in_city"
      ],
      "httpCalls": {
        "currentWeather": {
          "coord": {
            "lon": -3.7,
            "lat": 40.42
          },
          "weather": [
            {
              "id": 800,
              "main": "Clear",
              "description": "clear sky",
              "icon": "01n"
            }
          ],
          "base": "stations",
          "main": {
            "temp": 10.86,
            "pressure": 1019,
            "humidity": 66,
            "temp_min": 8.33,
            "temp_max": 13.33
          },
          "visibility": 10000,
          "wind": {
            "speed": 5.7,
            "deg": 240
          },
          "clouds": {
            "all": 0
          },
          "dt": 1551735805,
          "sys": {
            "type": 1,
            "id": 6443,
            "message": 0.0049,
            "country": "ES",
            "sunrise": 1551681788,
            "sunset": 1551723011
          },
          "id": 3117735,
          "name": "Madrid",
          "cod": 200
        }
      },
      "properties": {
        "currentWeather": {
          "coord": {
            "lon": -3.7,
            "lat": 40.42
          },
          "weather": [
            {
              "id": 800,
              "main": "Clear",
              "description": "clear sky",
              "icon": "01n"
            }
          ],
          "base": "stations",
          "main": {
            "temp": 10.86,
            "pressure": 1019,
            "humidity": 66,
            "temp_min": 8.33,
            "temp_max": 13.33
          },
          "visibility": 10000,
          "wind": {
            "speed": 5.7,
            "deg": 240
          },
          "clouds": {
            "all": 0
          },
          "dt": 1551735805,
          "sys": {
            "type": 1,
            "id": 6443,
            "message": 0.0049,
            "country": "ES",
            "sunrise": 1551681788,
            "sunset": 1551723011
          },
          "id": 3117735,
          "name": "Madrid",
          "cod": 200
        },
        "city": "madrid"
      },
      "output": [
        "The current weather situation of madrid is clear sky at 10.86 °C"
      ]
    }
  ],
  "conversationProperties": {
    "city": {
      "name": "city",
      "value": "madrid",
      "scope": "conversation"
    }
  },
  "conversationSteps": [
    {
      "conversationStep": [
        {
          "key": "input:initial",
          "value": "madrid"
        },
        {
          "key": "actions",
          "value": [
            "current_weather_in_city"
          ]
        },
        {
          "key": "output:text:current_weather_in_city",
          "value": "The current weather situation of madrid is clear sky at 10.86 °C"
        }
      ],
      "timestamp": 1551736024776
    }
  ]
}
```

The `conversationId` will be provided through the **`location`** **HTTP Header** of the response, you will use that later to submit messages to the Agent to maintain a conversation.

### Example *:*

*Request URL:*

`http://localhost:7070/agents/5ad2ab182de29719b44a792a/start`

*Response Body*

`no content`

*Response Code*

`201`

## Send/receive messages

### Send a message

### Send message in a conversation with an Agent REST API Endpoint

| Element               | Tags                                                                                                                                                                                                                                                                   |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| HTTP Method           | `POST`                                                                                                                                                                                                                                                                 |
| API endpoint          | `/agents/{conversationId}`                                                                                                                                                                                                                                             |
| {conversationId}      | (`Path` **parameter**): `String Id` of the **conversation** that you wish to **send** the message to.                                                                                                                                                                  |
| returnDetailed        | (`Query` **parameter**):`Boolean` - **Default** : `false`                                                                                                                                                                                                              |
| returnCurrentStepOnly | (`Query` **parameter**):`Boolean` - **Default** : `true`                                                                                                                                                                                                               |
| Request Body          | <p>JSON Object , example : <code>{ "input": "the message", "context": {} }</code></p><p>The <code>context</code> here is where you pass context variables that can be evaluated by EDDI, we will be explaining this in more details in Passing Context Information</p> |

### Example :

*Request URL*

`http://localhost:7070/agents/5add1fe8a081a228a0588d1c?returnDetailed=false&returnCurrentStepOnly=true`

*Request Body*

```javascript
{
  "input": "Hi!",
  "context": {}
}
```

Response Code

`200`

Response Body

```javascript
{
  "agentId": "5aaf90e29f7dd421ac3c7dd4",
  "agentVersion": 1,
  "environment": "production",
  "conversationState": "READY",
  "redoCacheSize": 0,
  "conversationSteps": [
    {
      "conversationStep": [
        {
          "key": "input:initial",
          "value": "Hi!"
        }
      ],
      "timestamp": 1524441253098
    }
  ]
}
```

````

### Receive a message

### Receive message in a conversation with an Agent REST API Endpoint

| Element          | Tags                                                                                                         |
| ---------------- | ------------------------------------------------------------------------------------------------------------ |
| HTTP Method      | `GET`                                                                                                        |
| API endpoint     | `/agents/{conversationId}`                                                           |
| {conversationId} | (`Path` **parameter**): `String Id` of the **conversation** that you wish to **receive** a the message from. |
| returnDetailed   | (`Query` **parameter**):`Boolean` - **Default** : `false`                                                    |
|                  |                                                                                                              |

### Example

_Request URL:_

`http://localhost:7070/agents/5add1fe8a081a228a0588d1c?returnDetailed=false`

_Response Body_

```javascript
{
  "agentId": "5aaf90e29f7dd421ac3c7dd4",
  "agentVersion": 1,
  "environment": "production",
  "conversationState": "READY",
  "redoCacheSize": 0,
  "conversationSteps": [
    {
      "conversationStep": [
        {
          "key": "actions",
          "value": [
            "global_menu"
          ]
        },
        {
          "key": "output:text:global_menu",
          "value": "What do you want to do?"
        },
        {
          "key": "quickReplies:global_menu",
          "value": [
            {
              "value": "Show me your skillz",
              "expressions": "confirmation(show_skills)",
              "default": false
            },
            {
              "value": "Tell me a joke",
              "expressions": "trigger(tell_a_joke)",
              "default": false
            }
          ]
        }
      ],
      "timestamp": 1524441064450
    },
    {
      "conversationStep": [
        {
          "key": "input:initial",
          "value": "Hi!"
        }
      ],
      "timestamp": 1524441253098
    }
  ]
}
````

Response Code

`200`

## Time Travel: Undo and Redo

One of EDDI's most powerful features is the ability to **go back in time** within a conversation. The undo/redo functionality allows you to step backward and forward through conversation history, perfect for:

* **User Correction**: User made a mistake and wants to retry
* **Testing**: Developers testing different conversation paths
* **Debugging**: Analyzing agent behavior at specific steps
* **User Experience**: Allowing users to explore different options

### How It Works

EDDI maintains a **redo cache** of undone conversation steps. When you undo a step, it's moved to this cache. You can then either:

* Continue the conversation (clears redo cache)
* Redo the step (restores it from cache)

```
Step 1 → Step 2 → Step 3 (current)
         undo ↓
Step 1 → Step 2 (current) | [Step 3 in redo cache]
         redo ↓
Step 1 → Step 2 → Step 3 (current)
```

### Undo API

#### Check if Undo is Available

| Element      | Value                                          |
| ------------ | ---------------------------------------------- |
| HTTP Method  | `GET`                                          |
| API Endpoint | `/agents/{conversationId}/undo`                |
| Response     | `true` if undo is available, `false` otherwise |

**Example:**

```bash
curl -X GET "http://localhost:7070/agents/CONV_ID/undo"
```

**Response:**

```json
true
```

#### Perform Undo

| Element      | Value                           |
| ------------ | ------------------------------- |
| HTTP Method  | `POST`                          |
| API Endpoint | `/agents/{conversationId}/undo` |
| Response     | HTTP 200 (no content)           |

**Example:**

```bash
curl -X POST "http://localhost:7070/agents/CONV_ID/undo"
```

**Response:** HTTP 200 (No Content)

**Effect**: The last conversation step is removed from the conversation history and stored in the redo cache.

### Redo API

#### Check if Redo is Available

| Element      | Value                                          |
| ------------ | ---------------------------------------------- |
| HTTP Method  | `GET`                                          |
| API Endpoint | `/agents/{conversationId}/redo`                |
| Response     | `true` if redo is available, `false` otherwise |

**Example:**

```bash
curl -X GET "http://localhost:7070/agents/CONV_ID/redo"
```

**Response:**

```json
true
```

#### Perform Redo

| Element      | Value                           |
| ------------ | ------------------------------- |
| HTTP Method  | `POST`                          |
| API Endpoint | `/agents/{conversationId}/redo` |
| Response     | HTTP 200 (no content)           |

**Example:**

```bash
curl -X POST "http://localhost:7070/agents/CONV_ID/redo"
```

**Response:** HTTP 200 (No Content)

**Effect**: The last undone step is restored from the redo cache and added back to the conversation history.

### Complete Example Flow

```bash
# 1. Start conversation
curl -X POST "http://localhost:7070/agents/AGENT_ID/start" -d '{}'
# Returns: {"conversationId": "CONV_ID"}

# 2. Send message
curl -X POST "http://localhost:7070/agents/CONV_ID" \
  -H "Content-Type: application/json" \
  -d '{"input": "Hello"}'
# Agent responds: "Hi! How can I help?"

# 3. Send another message
curl -X POST "http://localhost:7070/agents/CONV_ID" \
  -H "Content-Type: application/json" \
  -d '{"input": "Book a flight"}'
# Agent responds: "Where would you like to go?"

# 4. Oops, user meant hotel not flight! Undo last step
curl -X POST "http://localhost:7070/agents/CONV_ID/undo"
# Now back to: "Hi! How can I help?"

# 5. Try again with correct input
curl -X POST "http://localhost:7070/agents/CONV_ID" \
  -H "Content-Type: application/json" \
  -d '{"input": "Book a hotel"}'
# Agent responds: "Which city?"

# 6. Wait, maybe flight was right. Check if redo is available
curl -X GET "http://localhost:7070/agents/CONV_ID/redo"
# Returns: false (because we sent a new message, clearing redo cache)
```

### Redo Cache Behavior

**Important**: The redo cache is cleared when you send a new message after an undo. This prevents inconsistent conversation states.

```
Normal flow:
Step 1 → Step 2 → Step 3

After undo:
Step 1 → Step 2 | [Step 3 cached]
         ↓ can redo

After new message:
Step 1 → Step 2 → Step 4
         ↓ redo cache cleared (Step 3 lost)
```

### Checking Redo Cache Size

The conversation response includes `redoCacheSize` field:

```json
{
  "conversationId": "CONV_ID",
  "redoCacheSize": 0,
  "conversationState": "READY"
}
```

* `redoCacheSize: 0` - No undo has been performed, redo not available
* `redoCacheSize: 1` - One undo performed, one redo available
* `redoCacheSize: 2` - Two undos performed, two redos available

### Use Cases

**1. User Correction**

```
User: "Book me a table at 7pm"
Agent: "For how many people?"
User: "Wait, I meant 8pm"
→ Undo and retry
```

**2. Exploring Options**

```
User: "Show me flights to Paris"
Agent: [Shows flights]
User: "Actually, let me see hotels instead"
→ Undo and try different path
```

**3. Testing Agent Behavior**

```
Developer tests:
1. Input A → Response X
2. Undo
3. Input B → Response Y
4. Undo, redo → Back to Response X
```

### Limitations

* Undo/redo only affects conversation **history** and **memory**
* External API calls made during undone steps are **not reversed**
  * Example: If a payment API was called, undoing won't refund the payment
* Redo cache has a **size limit** (configurable)
* Redo cache is **session-specific** (cleared on conversation end)

### Best Practices

1. **Always check availability** before calling undo/redo to avoid errors
2. **Inform users** when undo clears redo cache (UX consideration)
3. **Be careful with side effects** - undo doesn't reverse external API calls
4. **Use for user convenience** - great for conversational UX
5. **Log undo/redo** - helps with analytics and debugging

## Related API Endpoints

* `POST /agents/{agentId}/start` - Start conversation
* `POST /agents/{conversationId}` - Send message
* `GET /agents/{conversationId}` - Get conversation state
* `POST /agents/{conversationId}/undo` - Undo last step
* `POST /agents/{conversationId}/redo` - Redo last step
* `GET /agents/{conversationId}/undo` - Check undo availability
* `GET /agents/{conversationId}/redo` - Check redo availability

## Sample Agent

Download the [Weather Agent v2 (Postman collection)](https://github.com/labsai/EDDI/blob/main/docs/.gitbook/assets/weather_bot_v2.zip) to try the full example.


# Group Conversations

> Multi-agent structured discussions with moderator synthesis.

## Overview

Group Conversations enable multiple agents to discuss a question. Each agent participates through its normal pipeline — agents are group-unaware by default. A `GroupConversationService` orchestrates the discussion through configurable phases.

## Discussion Styles

| Style            | Flow                                       | Best For                                          |
| ---------------- | ------------------------------------------ | ------------------------------------------------- |
| `ROUND_TABLE`    | Opinion × N → Synthesis                    | Brainstorming, open-ended exploration             |
| `PEER_REVIEW`    | Opinion → Critique → Revision → Synthesis  | Code review, document review                      |
| `DEVIL_ADVOCATE` | Opinion → Challenge → Defense → Synthesis  | Risk assessment, stress-testing                   |
| `DELPHI`         | Anonymous rounds → convergence → Synthesis | Forecasting, reducing groupthink                  |
| `DEBATE`         | Pro → Con → Rebuttals → Judge              | Trade-off analysis, comparisons                   |
| `TASK_FORCE`     | Plan → Execute → Verify → Synthesis        | Structured task decomposition, parallel execution |
| `CUSTOM`         | Define your own phases                     | Any workflow                                      |

## Quick Start (MCP)

```
# 1. Discover available styles
describe_discussion_styles

# 2. Create a group
create_group(
  name="Architecture Review",
  memberAgentIds="expert-1,expert-2,expert-3",
  memberDisplayNames="Backend Expert,Frontend Expert,DevOps Expert",
  moderatorAgentId="moderator-agent",
  style="PEER_REVIEW"
)

# 3. Run a discussion
discuss_with_group(groupId="<id>", question="Should we use microservices?")
```

## Quick Start (REST)

```bash
# Create group config
curl -X POST /groupstore/groups \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Architecture Panel",
    "members": [
      {"agentId": "expert-1", "displayName": "Backend Expert", "speakingOrder": 1},
      {"agentId": "expert-2", "displayName": "Frontend Expert", "speakingOrder": 2}
    ],
    "moderatorAgentId": "moderator-agent",
    "style": "ROUND_TABLE",
    "maxRounds": 2
  }'

# Start discussion
curl -X POST /groups/<groupId>/conversations \
  -H "Content-Type: application/json" \
  -d '{"input": "What is the best architecture for our new service?"}'
```

## Member Roles

Some styles require specific roles:

| Role             | Used By               | Purpose                  |
| ---------------- | --------------------- | ------------------------ |
| `DEVIL_ADVOCATE` | DEVIL\_ADVOCATE style | Argues against consensus |
| `PRO`            | DEBATE style          | Argues in favor          |
| `CON`            | DEBATE style          | Argues against           |

```
create_group(
  name="Debate Panel",
  memberAgentIds="agent-a,agent-b",
  memberRoles="PRO,CON",
  moderatorAgentId="judge-agent",
  style="DEBATE"
)
```

## Nested Groups (Group-of-Groups)

Members can be other groups. The sub-group runs its own discussion and its synthesized answer becomes the member's response.

```
# Create sub-groups
create_group(name="Team A", memberAgentIds="a1,a2", style="PEER_REVIEW")  → g1
create_group(name="Team B", memberAgentIds="a3,a4", style="DEBATE")       → g2

# Create meta-group with GROUP members
create_group(
  name="Tournament",
  memberAgentIds="g1,g2",
  memberTypes="GROUP,GROUP",
  moderatorAgentId="judge-agent",
  style="ROUND_TABLE"
)
```

Depth tracking prevents infinite recursion (`eddi.groups.max-depth`, default: 3).

## Custom Phases

For full control, define phases directly:

```json
{
  "name": "Custom Panel",
  "style": "CUSTOM",
  "phases": [
    {
      "name": "Independent Opinions",
      "type": "OPINION",
      "participants": "ALL",
      "turnOrder": "PARALLEL",
      "contextScope": "NONE"
    },
    {
      "name": "Peer Critique",
      "type": "CRITIQUE",
      "participants": "ALL",
      "targetEachPeer": true,
      "contextScope": "FULL"
    },
    {
      "name": "Final Synthesis",
      "type": "SYNTHESIS",
      "participants": "MODERATOR",
      "contextScope": "FULL"
    }
  ]
}
```

### Phase Types

| Type        | Purpose                                    |
| ----------- | ------------------------------------------ |
| `OPINION`   | Share perspective on the question          |
| `CRITIQUE`  | Review another member's response           |
| `REVISION`  | Revise own response based on feedback      |
| `CHALLENGE` | Argue against consensus (devil's advocate) |
| `DEFENSE`   | Defend position against challenges         |
| `ARGUE`     | Present argument for a side (debate)       |
| `REBUTTAL`  | Counter opposing arguments                 |
| `PLAN`      | Decompose the question into sub-tasks      |
| `EXECUTE`   | Work on assigned sub-task                  |
| `VERIFY`    | Review and validate another member's work  |
| `SYNTHESIS` | Moderator produces balanced conclusion     |

### Context Scopes

| Scope            | What the agent sees                              |
| ---------------- | ------------------------------------------------ |
| `NONE`           | Only the question (independent)                  |
| `FULL`           | All previous transcript entries                  |
| `LAST_PHASE`     | Only the previous phase's entries                |
| `ANONYMOUS`      | Previous entries with speaker names removed      |
| `OWN_FEEDBACK`   | Only feedback addressed to this agent            |
| `TASK_ONLY`      | Only this agent's assigned task from the plan    |
| `TASK_WITH_DEPS` | Assigned task plus outputs from dependency tasks |

### TASK\_FORCE Configuration

The TASK\_FORCE style uses a 4-phase pipeline: **Plan → Execute → Verify → Synthesize**.

1. **PLAN** — The moderator decomposes the goal into actionable tasks and assigns each to an agent
2. **EXECUTE** — Agents execute their assigned tasks in parallel (each sees only `TASK_ONLY` or `TASK_WITH_DEPS` context)
3. **VERIFY** — The moderator reviews each task result against the original goal
4. **SYNTHESIS** — The moderator combines all verified results into a coherent final deliverable

#### Pre-Configured Tasks

Pass a `tasks` array to skip the PLAN phase entirely — useful for deterministic, repeatable workflows:

```json
{
  "name": "Documentation Team",
  "style": "TASK_FORCE",
  "moderatorAgentId": "moderator-id",
  "members": [
    {"agentId": "researcher-id", "displayName": "Researcher"},
    {"agentId": "writer-id", "displayName": "Writer"}
  ],
  "tasks": [
    {
      "subject": "Research topic",
      "description": "Research the key trends and data points.",
      "assignToRole": "Researcher",
      "priority": 0
    },
    {
      "subject": "Write article",
      "description": "Using the research findings, write a 500-word article.",
      "assignToRole": "Writer",
      "dependsOn": ["Research topic"],
      "priority": 1
    }
  ]
}
```

When `tasks` is provided, the system posts `[System] "Pre-configured task plan: N tasks"` instead of invoking the moderator's LLM.

#### Task Dependencies

Use `dependsOn` to create sequential execution chains. Each entry references a task `subject`:

* Tasks with no dependencies execute in **parallel**
* Tasks with dependencies wait for their predecessors to complete
* Dependent tasks receive their predecessor's output via the `TASK_WITH_DEPS` context scope
* **Cycle detection** prevents circular dependency chains (fails fast at planning time)

#### Task Statuses

| Status        | Meaning                                |
| ------------- | -------------------------------------- |
| `PENDING`     | Waiting for dependencies or execution  |
| `ASSIGNED`    | Assigned to an agent, waiting to start |
| `IN_PROGRESS` | Currently being executed by an agent   |
| `COMPLETED`   | Agent produced output                  |
| `VERIFIED`    | Moderator verified the result          |
| `FAILED`      | Agent or verification failed           |

### Dynamic Agents

During TASK\_FORCE (or any group) discussions, agents with the appropriate LLM tools can **create, recruit, and delegate to new agents at runtime**:

| Tool                         | Purpose                                                    |
| ---------------------------- | ---------------------------------------------------------- |
| `CreateSubAgentTool`         | Create a new ephemeral agent with a specific system prompt |
| `ConverseWithAgentTool`      | Delegate a sub-task to an existing deployed agent          |
| `FindAgentsByCapabilityTool` | Discover agents by capability keywords                     |
| `TeardownAgentTool`          | Clean up dynamically created agents                        |

#### DynamicAgentConfig

Guardrails for dynamic agent creation are configured per-group via `AgentGroupConfiguration.dynamicAgents`:

```json
{
  "dynamicAgents": {
    "enabled": true,
    "allowCreation": true,
    "allowRecruitment": true,
    "allowDelegation": true,
    "maxCreatedAgentsPerDiscussion": 5,
    "maxRecruitedAgentsPerDiscussion": 10,
    "maxDelegationsPerTask": 3,
    "lifecyclePolicy": "ephemeral",
    "inheritParentModel": true,
    "allowedProviders": ["anthropic", "openai"],
    "allowedModels": {
      "anthropic": ["claude-sonnet-4-6"],
      "openai": ["gpt-4o"]
    }
  }
}
```

| Setting                           | Default      | Purpose                                                           |
| --------------------------------- | ------------ | ----------------------------------------------------------------- |
| `enabled`                         | `false`      | Master switch for dynamic agent capabilities                      |
| `allowCreation`                   | `false`      | Allow creating new agents (vs. only recruiting existing)          |
| `allowRecruitment`                | `false`      | Allow recruiting already-deployed agents into the discussion      |
| `allowDelegation`                 | `true`       | Allow delegating sub-tasks to other agents                        |
| `maxCreatedAgentsPerDiscussion`   | `5`          | Cap on new agents created per discussion                          |
| `maxRecruitedAgentsPerDiscussion` | `10`         | Cap on recruited agents per discussion                            |
| `maxDelegationsPerTask`           | `3`          | Cap on delegations per task                                       |
| `lifecyclePolicy`                 | `EPHEMERAL`  | `EPHEMERAL`, `KEEP_DEPLOYED`, `UNDEPLOY_ONLY`, or `AGENT_DECIDES` |
| `inheritParentModel`              | `true`       | Created agents inherit the parent agent's model                   |
| `allowedProviders`                | `null` (any) | Whitelist of LLM providers                                        |
| `allowedModels`                   | `null` (any) | Per-provider model whitelist                                      |

Dynamic agents are tracked in `GroupConversation.dynamicMembers`, `createdAgentIds`, and `retainedAgentIds`.

### Tenant Quota Enforcement

If tenant quotas are enabled, `QuotaExceededException` is propagated regardless of the group's `onAgentFailure` policy — quota violations always abort the discussion to prevent runaway resource consumption.

## Protocol Configuration

```json
{
  "protocol": {
    "agentTimeoutSeconds": 180,
    "onAgentFailure": "SKIP",
    "maxRetries": 2,
    "onMemberUnavailable": "SKIP"
  }
}
```

| Setting               | Options                  | Default |
| --------------------- | ------------------------ | ------- |
| `agentTimeoutSeconds` | Any positive integer     | 180     |
| `onAgentFailure`      | `SKIP`, `RETRY`, `ABORT` | `SKIP`  |
| `maxRetries`          | 0+                       | 2       |
| `onMemberUnavailable` | `SKIP`, `FAIL`           | `SKIP`  |

> **Timeout guidance**: 180s covers thinking models (e.g. `claude-sonnet-5`) and synthesis phases comfortably. For tool-calling agents with multiple tool loops, consider `300`–`600`. The timeout is per agent turn, not per phase.

## REST API

| Method   | Path                                   | Description            |
| -------- | -------------------------------------- | ---------------------- |
| `POST`   | `/groupstore/groups`                   | Create group config    |
| `GET`    | `/groupstore/groups`                   | List group configs     |
| `GET`    | `/groupstore/groups/{id}`              | Read group config      |
| `PUT`    | `/groupstore/groups/{id}`              | Update group config    |
| `DELETE` | `/groupstore/groups/{id}`              | Delete group config    |
| `GET`    | `/groupstore/groups/styles`            | List discussion styles |
| `POST`   | `/groups/{groupId}/conversations`      | Start discussion       |
| `GET`    | `/groups/{groupId}/conversations/{id}` | Read transcript        |
| `GET`    | `/groups/{groupId}/conversations`      | List conversations     |
| `DELETE` | `/groups/{groupId}/conversations/{id}` | Delete + cascade       |

## MCP Tools

| Tool                         | Description                                                                                  |
| ---------------------------- | -------------------------------------------------------------------------------------------- |
| `describe_discussion_styles` | Rich descriptions of all styles                                                              |
| `list_groups`                | List group configs                                                                           |
| `read_group`                 | Read group config                                                                            |
| `create_group`               | Create group (name, members, style, roles, types)                                            |
| `update_group`               | Update group config JSON                                                                     |
| `delete_group`               | Delete group config                                                                          |
| `discuss_with_group`         | Start discussion, return transcript                                                          |
| `read_group_conversation`    | Read conversation transcript                                                                 |
| `list_group_conversations`   | List past discussions for a group, with state and timestamps                                 |
| `start_group_discussion`     | Start a discussion asynchronously (returns immediately). Poll with `read_group_conversation` |
| `delete_group_conversation`  | Delete a group conversation and cascade-delete all member conversations                      |

## Slack Integration

Group discussions integrate natively with Slack. See [slack-integration.md](/protocols-and-integration/slack-integration) for full setup instructions.

### UX Pattern: Header + Thread

All discussion styles use the same rendering pattern in Slack:

1. **Start Banner** — posted in the user's thread with style name, agent count, and question
2. **Agent Headers** — each agent's first contribution is a channel-level message with a short preview
3. **Full Content** — the complete response is posted as a thread reply under the agent's header
4. **Peer Feedback** — feedback threads under the target agent's header message
5. **Revisions** — revised contributions thread under the agent's own header
6. **Synthesis** — moderator's synthesis gets its own channel-level header + thread

### Discussion Styles in Slack

| Style               | Phase Flow in Slack                                                                          |
| ------------------- | -------------------------------------------------------------------------------------------- |
| **ROUND\_TABLE**    | Each agent posts → Moderator synthesizes                                                     |
| **PEER\_REVIEW**    | Agents post → Critiques thread under targets → Revisions thread under own → Synthesis        |
| **DEVIL\_ADVOCATE** | Agent posts → Challenger threads challenges → Agent threads defense → Synthesis              |
| **DEBATE**          | PRO agent posts → CON agent posts → Rebuttals thread under opponents → Judge synthesizes     |
| **DELPHI**          | Round 1 agents post → Round 2 agents post (convergence) → Synthesis                          |
| **TASK\_FORCE**     | Moderator posts plan → Agents post task results → Verifiers thread under targets → Synthesis |

### Trigger Keywords

Configure trigger keywords in `ChannelIntegrationConfiguration` to route to specific groups:

```
@EDDI panel: Should we adopt microservices?     → GROUP target "panel"
@EDDI debate: REST vs GraphQL                   → GROUP target "debate"
@EDDI peer: Review this architecture             → GROUP target "peer"
```

### Follow-up Conversations

After a discussion, users can reply in any agent's thread to ask follow-up questions. The system injects the agent's discussion context (contribution + peer feedback received) into the prompt for a contextual response.

## Configuration

```properties
# application.properties
eddi.groups.max-depth=3    # Max recursion depth for nested groups
```


# Managed Agents

## Overview

**Managed Agents** is an EDDI feature that provides automatic conversation management, allowing you to trigger agents based on **intents** without manually creating and managing conversation IDs. EDDI handles the conversation lifecycle for you.

### The Problem It Solves

**Without Managed Agents** (manual approach):

1. Your app creates a conversation: `POST /agents/agent123/start`
2. EDDI returns conversation ID: `conv-456`
3. Your app stores this ID
4. Your app sends messages: `POST /agents/conv-456`
5. Your app manages conversation lifecycle

**With Managed Agents** (automatic approach):

1. You define an agent trigger with an intent keyword
2. Your app sends: `POST /agents/managed/weather_help/user123`
3. EDDI automatically:
   * Creates conversation (if none exists for this user/intent)
   * Routes to correct agent
   * Manages conversation state
   * Reuses existing conversation on subsequent calls

### Use Cases

* **Multi-Agent Applications**: Route users to different agents based on intent without tracking conversation IDs
* **Microservices Architecture**: Each service triggers agents by intent, EDDI handles coordination
* **Simplified Integration**: Client apps don't need conversation management logic
* **User-Centric Sessions**: One conversation per user per intent, automatically managed
* **A/B Testing**: Define multiple agents for same intent; EDDI picks one randomly

### Key Concepts

**Intent**: A keyword or phrase that maps to one or more agent deployments

* Example: `"weather_help"` → Weather Agent
* Example: `"order_status"` → Order Tracking Agent
* Example: `"support_technical"` → Technical Support Agent

**Agent Trigger**: Configuration that links an intent to specific agents

**User ID**: Identifies the user; EDDI maintains one conversation per user per intent

### How It Works

```
1. Define Agent Trigger:
   Intent: "weather_help" → Agent: weather-agent-v2 (production)

2. User Requests:
   POST /agents/managed/weather_help/user-123
   {"input": "What's the weather?"}

3. EDDI Logic:
   - Checks if user-123 has active conversation for "weather_help"
   - If NO: Creates new conversation with weather-agent-v2
   - If YES: Continues existing conversation
   - Processes message through agent's lifecycle
   - Returns response

4. Subsequent Requests:
   POST /agents/managed/weather_help/user-123
   {"input": "What about tomorrow?"}
   → Continues same conversation
```

### Benefits

* **Simplified Client Logic**: No conversation ID management needed
* **Intent-Based Routing**: Natural way to organize multi-agent systems
* **Automatic Session Management**: EDDI handles conversation lifecycle
* **Initial Context Support**: Pass context at conversation start
* **Random Agent Selection**: A/B testing or load distribution built-in

## Managed Agents Configuration

This feature allows you to take advantage of **EDDI**'s automatic management of agents. It is possible to avoid creating conversations and managing them yourself—let EDDI handle it.

This acts as a shortcut to directly start a conversation with an agent that covers a specific **intent**.

First, you need to set up a `AgentTrigger`.

## AgentTrigger

### The request model

```javascript
{
  "intent": "string",
  "agentDeployments": [
    {
      "environment": "environment",
      "agentId": "string",
      "initialContext": {
        "additionalProp1": {
          "type": "string",
          "value": ""
        },
        "additionalProp2": {
          "type": "string",
          "value": ""
        },
        "additionalProp3": {
          "type": "string",
          "value": ""
        }
      }
    }
  ]
}
```

### Description of the request model

| Element          | Description                                                                                                                                                                    |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| intent           | (`String`) keyword or phrase (camel case or with '-') that will be used in managed agents to trigger the agents defined in this model                                          |
| agentDeployments | (`Array:`<`AgentDeployment`>) array of `AgentDeployment`. If multiple `agentDeployments` are defined, one will be picked randomly.                                             |
| environment      | (`String`) the environment: `production` (default) or `test`. Legacy values `unrestricted` and `restricted` are accepted and mapped to `production`.                           |
| agentId          | (`String`) the id of the agent that you want to create the agentTrigger for it.                                                                                                |
| initialContext   | (Object) Context handed to the agent at conversation start. Keys map to `Context` objects with `type` and `value` fields. Only applied when the conversation is first created. |

### AgentTrigger API endpoints

| HTTP Method | API Endpoint                                | Request Body         | Response             |
| ----------- | ------------------------------------------- | -------------------- | -------------------- |
| DELETE      | `/agenttriggerstore/agenttriggers/{intent}` | N/A                  | N/A                  |
| GET         | `/agenttriggerstore/agenttriggers/{intent}` | N/A                  | Agent Triggers-model |
| PUT         | `/agenttriggerstore/agenttriggers/{intent}` | Agent Triggers-model | N/A                  |
| POST        | `/agenttriggerstore/agenttriggers`          | Agent Triggers-model | N/A                  |

## Triggering a ManagedAgent

To trigger a managed agent you will have to call the following API endpoints.

### API Methods

| HTTP Method | API Endpoint                                        | Request Body | Response           |
| ----------- | --------------------------------------------------- | ------------ | ------------------ |
| GET         | `/agents/managed/{intent}/{userId}`                 | N/A          | Conversation model |
| POST        | `/agents/managed/{intent}/{userId}`                 | Input model  | N/A                |
| POST        | `/agents/managed/{intent}/{userId}/endConversation` | N/A          | N/A                |
| GET         | `/agents/managed/{intent}/{userId}/undo`            | N/A          | Boolean            |
| POST        | `/agents/managed/{intent}/{userId}/undo`            | N/A          | N/A                |
| GET         | `/agents/managed/{intent}/{userId}/redo`            | N/A          | Boolean            |
| POST        | `/agents/managed/{intent}/{userId}/redo`            | N/A          | N/A                |

### Description API endpoint required path parameters

| Element   | Description                                                                |
| --------- | -------------------------------------------------------------------------- |
| {intent}​ | (`String`) the label/keyword used originally to point to this AgentTrigger |
| {userId}​ | (`String`) used to specify the user who triggered the conversation         |

### Example *:*

#### 1/Create an AgentTrigger

*Request URL:*

`POST` `http://localhost:7070/agenttriggerstore/agenttriggers`

*Request Body*

```javascript
{
  "intent": "weather_trigger",
  "agentDeployments": [
    {
      "environment": "production",
      "agentId": "5bf5418c46e0fb000b7636d0",
      "initialContext": {}
    }
  ]
}
```

*Response Body*

`no content`

*Response Code*

`200`

#### 2/Trigger the ManagedAgent

*Request URL:*

`POST` `http://localhost:7070/agents/managed/weather_trigger/myUserId`

*Request Body*

```javascript
{
  "input": "Hello managed agent!",
  "context": {}
}
```

*Response Body*

```javascript
{
  "agentId": "5bf5418c46e0fb000b7636d0",
  "agentVersion": 10,
  "userId": "myUserId",
  "environment": "production",
  "conversationState": "READY",
  "redoCacheSize": 0,
  "conversationOutputs": [
    {
      "input": "Hello managed agent!",
      "expressions": "unknown(Hello), unknown(managed), unknown(agent!)",
      "intents": [
        "unknown",
        "unknown",
        "unknown"
      ]
    }
  ],
  "conversationProperties": {},
  "conversationSteps": [
    {
      "conversationStep": [
        {
          "key": "input:initial",
          "value": "Hello managed agent!"
        }
      ],
      "timestamp": 1552869578596
    }
  ]
}
```

*Response Code*

`200`

### MCP Integration

The same managed conversation functionality is available via the MCP `chat_managed` tool:

```
chat_managed(intent: "weather_trigger", userId: "myUserId", message: "Hello managed agent!")
```

See [MCP Server](/protocols-and-integration/mcp-server) for full tool documentation.


# Scheduled Execution & Heartbeats

## Overview

EDDI supports **scheduled agent execution** — agents can be triggered automatically on a timer without any user input. This enables proactive agents, background maintenance, periodic data processing, and memory consolidation.

### Use Cases

| Use Case                | Description                                                                                 |
| ----------------------- | ------------------------------------------------------------------------------------------- |
| **Proactive Agents**    | Check for updates, send notifications, or perform monitoring at regular intervals           |
| **Dream Consolidation** | Background memory maintenance — prune stale entries, detect contradictions, summarize facts |
| **Data Pipelines**      | Periodically fetch data from external APIs and process it through the agent pipeline        |
| **Health Checks**       | Run diagnostic agents that verify system health and report anomalies                        |
| **Report Generation**   | Generate daily/weekly summary reports through conversational agents                         |

## Concepts

### Schedule

A **Schedule** defines when and how often an agent fires:

```json
{
  "agentId": "agent-123",
  "agentVersion": 0,
  "triggerType": "CRON",
  "cronExpression": "0 2 * * *",
  "conversationStrategy": "persistent",
  "message": "Run maintenance cycle",
  "userId": "system:scheduler",
  "timeZone": "Europe/Vienna",
  "enabled": true
}
```

### Trigger Types

| Type        | Description                        | Default Strategy | Example                    |
| ----------- | ---------------------------------- | ---------------- | -------------------------- |
| `CRON`      | Wall-clock aligned cron expression | `new`            | `0 2 * * *` (daily at 2am) |
| `HEARTBEAT` | Fixed-interval, drift-proof        | `persistent`     | Every 300 seconds          |

### Conversation Strategies

| Strategy     | Behavior                                                             | Use When                                                 |
| ------------ | -------------------------------------------------------------------- | -------------------------------------------------------- |
| `persistent` | Reuses the same conversation across all fires. Context accumulates.  | Dream consolidation, ongoing monitoring, stateful agents |
| `new`        | Creates a fresh conversation for each fire. Clean context each time. | Report generation, data pipelines, stateless tasks       |

## Configuration

### Creating a Schedule

```bash
curl -X POST http://localhost:7070/schedulestore/schedules \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "agent-123",
    "agentVersion": 0,
    "triggerType": "CRON",
    "cronExpression": "*/30 * * * *",
    "conversationStrategy": "persistent",
    "message": "heartbeat ping",
    "timeZone": "UTC",
    "enabled": true
  }'
```

### Cron Expression Reference

EDDI uses **standard 5-field cron expressions**:

```
┌────── minute (0-59)
│ ┌──── hour (0-23)
│ │ ┌── day of month (1-31)
│ │ │ ┌ month (1-12)
│ │ │ │ ┌ day of week (0-7, 0=Sun)
│ │ │ │ │
* * * * *
```

**Common patterns:**

| Expression     | Schedule                            |
| -------------- | ----------------------------------- |
| `0 2 * * *`    | Daily at 2:00 AM                    |
| `*/30 * * * *` | Every 30 minutes                    |
| `0 */4 * * *`  | Every 4 hours                       |
| `0 9 * * 1-5`  | Weekdays at 9:00 AM                 |
| `0 0 1 * *`    | First day of each month at midnight |

### Heartbeat Configuration

For heartbeat triggers, use `heartbeatIntervalSeconds` instead of `cronExpression`:

```json
{
  "agentId": "agent-123",
  "triggerType": "HEARTBEAT",
  "heartbeatIntervalSeconds": 300,
  "conversationStrategy": "persistent",
  "message": "heartbeat check",
  "enabled": true
}
```

Heartbeats are **drift-proof** — after a fire completes, the next fire is calculated as `lastFired + interval`, not `now + interval`.

### Schedule Fields

| Field                      | Type    | Default            | Description                                 |
| -------------------------- | ------- | ------------------ | ------------------------------------------- |
| `agentId`                  | string  | required           | Agent to trigger                            |
| `agentVersion`             | int     | `0` (latest)       | Agent version (0 = latest deployed)         |
| `triggerType`              | enum    | `CRON`             | `CRON` or `HEARTBEAT`                       |
| `cronExpression`           | string  | —                  | 5-field cron (for CRON type)                |
| `heartbeatIntervalSeconds` | long    | —                  | Interval in seconds (for HEARTBEAT type)    |
| `conversationStrategy`     | string  | varies             | `new` or `persistent`                       |
| `message`                  | string  | —                  | Message text sent to the agent on each fire |
| `userId`                   | string  | `system:scheduler` | User identity for the fire                  |
| `timeZone`                 | string  | `UTC`              | IANA timezone (e.g., `Europe/Vienna`)       |
| `environment`              | string  | `production`       | Deployment environment                      |
| `enabled`                  | boolean | `true`             | Whether the schedule is active              |
| `maxCostPerFire`           | double  | `-1` (unlimited)   | Dollar ceiling per fire                     |

### Managing Schedules

| Method   | Path                                    | Description                                      |
| -------- | --------------------------------------- | ------------------------------------------------ |
| `POST`   | `/schedulestore/schedules`              | Create a schedule                                |
| `GET`    | `/schedulestore/schedules`              | List all schedules (optional `?agentId=` filter) |
| `GET`    | `/schedulestore/schedules/{id}`         | Get a specific schedule                          |
| `PUT`    | `/schedulestore/schedules/{id}`         | Update a schedule                                |
| `DELETE` | `/schedulestore/schedules/{id}`         | Delete a schedule                                |
| `POST`   | `/schedulestore/schedules/{id}/enable`  | Enable a schedule                                |
| `POST`   | `/schedulestore/schedules/{id}/disable` | Disable a schedule                               |
| `POST`   | `/schedulestore/schedules/{id}/fire`    | Manually trigger a fire immediately              |

### Admin Endpoints

| Method | Path                                    | Description                               |
| ------ | --------------------------------------- | ----------------------------------------- |
| `GET`  | `/schedulestore/schedules/{id}/fires`   | Read fire history (optional `?limit=20`)  |
| `GET`  | `/schedulestore/schedules/admin/failed` | List all failed/dead-lettered fires       |
| `POST` | `/schedulestore/schedules/{id}/retry`   | Re-queue a dead-lettered schedule         |
| `POST` | `/schedulestore/schedules/{id}/dismiss` | Reset dead-letter without immediate retry |

## Dream Consolidation

Dream Consolidation is a specialized schedule that performs **background memory maintenance** on an agent's persistent user memories. It's configured in the agent's `UserMemoryConfig`, not as a standalone schedule.

### What It Does

1. **Stale entry pruning** — Removes outdated facts that are no longer relevant
2. **Contradiction detection** — Identifies conflicting memories (e.g., "user likes coffee" vs "user hates coffee") and logs them for review. Resolution is planned for a future version.
3. **Fact summarization** — Consolidates verbose entries into concise summaries

### Configuration

Dream consolidation is configured in the agent configuration:

```json
{
  "agentConfiguration": {
    "enableMemoryTools": true,
    "userMemoryConfig": {
      "dream": {
        "enabled": true,
        "schedule": "0 3 * * *",
        "detectContradictions": true,
        "contradictionResolution": "keep_newest",
        "pruneStaleAfterDays": 90,
        "summarizeInteractions": true,
        "summarizeMinEntries": 5,
        "summarizeTargetEntries": 2,
        "summarizeGroupBy": "category",
        "preserveAgentProvenance": false,
        "llmProvider": "anthropic",
        "llmModel": "claude-sonnet-4-6",
        "maxCostPerRun": 0.50,
        "batchSize": 50,
        "maxUsersPerRun": 1000
      }
    }
  }
}
```

> **Scope:** a dream cycle only touches memories the **firing agent** wrote (`sourceAgentId`). Set `crossAgentMaintenance: true` to maintain the user's whole memory set across agents — without it, agent A's `pruneStaleAfterDays` would delete agent B's memories and A's model endpoint would see B's private text.
>
> `maxSummarizationCalls` is **deprecated** in favour of `maxCostPerRun` (a call count is a poor budget — consolidations differ wildly in cost). It is still honoured as a secondary backstop if a stored config sets it explicitly, so existing configurations keep their bound.

### Cost Control

Dream cycles consume LLM tokens. Use `maxCostPerRun` (in the **Agent Configuration**) to set a dollar ceiling per run:

```json
{
  "agentConfiguration": {
    "userMemoryConfig": {
      "dream": {
        "maxCostPerRun": 0.50
      }
    }
  }
}
```

When the budget is exceeded, the agent stops processing. This prevents runaway costs on large memory stores.

> **Tip:** Use a cheaper model (e.g., `claude-sonnet-4-6` or `gpt-4o-mini`) for dream consolidation — the task doesn't require top-tier reasoning.

## Fire Logging

Every scheduled execution is logged. View fire history via the REST API:

```bash
# List recent fires for a schedule
curl http://localhost:7070/schedulestore/schedules/{scheduleId}/fires?limit=20

# List all failed fires across all schedules
curl http://localhost:7070/schedulestore/schedules/admin/failed?limit=50
```

### State Machine

Each schedule follows a state machine:

```
PENDING → CLAIMED → EXECUTING → COMPLETED
                              → FAILED → (retry) → PENDING
                              → DEAD_LETTERED → (manual retry/dismiss)
```

## Cluster Awareness

The `SchedulePollerService` is cluster-aware — in multi-instance deployments, only one instance executes each scheduled fire. This is achieved via atomic claim operations (`tryClaim`), preventing duplicate execution when running EDDI behind a load balancer.

## Best Practices

1. **Start with longer intervals** — Begin with hourly or daily schedules and increase frequency only if needed
2. **Use `persistent` strategy for stateful work** — Dream consolidation and monitoring agents benefit from accumulated context
3. **Set cost ceilings** — Always configure `maxCostPerFire` or `maxCostPerRun` for LLM-powered scheduled tasks
4. **Monitor fire logs** — Check for recurring failures that might indicate configuration issues
5. **Use cheap models for maintenance** — Background tasks rarely need expensive frontier models

## See Also

* [Managed Agents](/conversations-and-orchestration/managed-agents) — Intent-based agent routing
* [User Memory](/architecture-and-concepts/user-memory) — Persistent user memory (target of dream consolidation)
* [LLM Configuration](/agent-configuration/langchain) — Agent configuration reference
* [Metrics](/deployment-and-infrastructure/metrics) — Monitoring scheduled execution performance


# Capability Matching

> The `capabilityMatch` behavior rule condition enables **config-driven agent discovery**. An orchestrating agent can dynamically find other agents that declare a specific skill, without hardcoding agent IDs. This is the foundation of EDDI's A2A (Agent-to-Agent) soft routing.

## How It Works

```
┌───────────────────────────┐
│  Agent A (Orchestrator)   │
│  behavior.json:           │
│    condition:              │
│      capabilityMatch       │
│      skill: "translation"  │
│                            │
│  If SUCCESS → action:      │
│    "delegate_to_translator"│
└──────────┬────────────────┘
           │ queries registry
           ▼
┌───────────────────────────┐
│  CapabilityRegistryService │
│                            │
│  Index:                    │
│  "translation" →           │
│    Agent B (confidence:high)│
│    Agent C (confidence:med) │
└──────────┬────────────────┘
           │ matched agent IDs
           ▼
┌───────────────────────────┐
│  Conversation Memory:      │
│  capabilityMatch.results = │
│  ["agent-b-id", "agent-c"] │
└───────────────────────────┘
```

1. **Agent B and C** declare capabilities in their `AgentConfiguration`:

   ```json
   { "capabilities": [{ "skill": "translation", "confidence": "high" }] }
   ```
2. **Agent A** (the orchestrator) uses `capabilityMatch` in its behavior rules
3. When the condition evaluates, it queries the `CapabilityRegistryService`
4. Matching agent IDs are stored in memory as `capabilityMatch.results`
5. Downstream tasks (group orchestration, httpCalls, LLM tools) can consume the results

***

## Configuration

### behavior.json

```json
{
  "name": "Route to specialist",
  "actions": ["delegate_to_specialist"],
  "conditions": [
    {
      "type": "capabilityMatch",
      "configs": {
        "skill": "language-translation",
        "strategy": "highest_confidence",
        "minResults": "1"
      }
    }
  ]
}
```

### Config Keys

| Key          | Required | Default              | Description                                                       |
| ------------ | -------- | -------------------- | ----------------------------------------------------------------- |
| `skill`      | Yes      | —                    | Skill name to search for (case-insensitive)                       |
| `strategy`   | No       | `highest_confidence` | Selection strategy: `highest_confidence`, `round_robin`, or `all` |
| `minResults` | No       | `1`                  | Minimum number of matching agents for SUCCESS                     |

### Selection Strategies

| Strategy             | Behavior                                         |
| -------------------- | ------------------------------------------------ |
| `highest_confidence` | Sort matches by confidence (high → medium → low) |
| `round_robin`        | Randomize order (for load distribution)          |
| `all`                | Return all matches in natural order              |

***

## Template Variables

Config values support **Qute template expressions**, resolved against the conversation memory at evaluation time. This enables dynamic routing:

```json
{
  "type": "capabilityMatch",
  "configs": {
    "skill": "{properties.requiredSkill}",
    "strategy": "{context.routingStrategy}",
    "minResults": "1"
  }
}
```

The `skill` and `strategy` values are resolved using `IMemoryItemConverter.convert(memory)` — the same data map available to system prompts and httpCalls templates.

***

## Agent Capability Declaration

Agents declare capabilities in their `AgentConfiguration`:

```json
{
  "name": "Translation Agent",
  "capabilities": [
    {
      "skill": "language-translation",
      "confidence": "high",
      "attributes": {
        "languages": "en,de,fr,es",
        "domain": "legal"
      }
    },
    {
      "skill": "summarization",
      "confidence": "medium",
      "attributes": {}
    }
  ]
}
```

### Capability Fields

| Field        | Required | Default  | Description                                             |
| ------------ | -------- | -------- | ------------------------------------------------------- |
| `skill`      | Yes      | —        | Unique skill identifier (lowercased for indexing)       |
| `confidence` | No       | `medium` | Self-declared confidence level: `high`, `medium`, `low` |
| `attributes` | No       | `{}`     | Key-value metadata for fine-grained filtering           |

***

## Consuming Results

When `capabilityMatch` evaluates to SUCCESS, the matching agent IDs are stored in conversation memory:

```
Memory key: capabilityMatch.results
Value: ["agent-b-id", "agent-c-id"]
```

### Example 1: Action Delegation

The simplest pattern — match a skill, then fire an action that another task (e.g., LLM, httpCalls) reacts to:

```json
{
  "name": "Delegate to translator",
  "actions": ["call_translation_agent"],
  "conditions": [
    {
      "type": "capabilityMatch",
      "configs": {
        "skill": "language-translation",
        "strategy": "highest_confidence",
        "minResults": "1"
      }
    }
  ]
}
```

The LLM task or httpCalls task listens for the `call_translation_agent` action and can access the discovered agents via memory.

### Example 2: Dynamic Group Composition

Use the discovered agents to dynamically compose a group conversation:

**behavior.json:**

```json
{
  "name": "Assemble expert panel",
  "actions": ["create_expert_group"],
  "conditions": [
    {
      "type": "capabilityMatch",
      "configs": {
        "skill": "legal-analysis",
        "strategy": "all",
        "minResults": "2"
      }
    }
  ]
}
```

**System prompt (LLM task triggered by `create_expert_group`):**

```
The following agents have been identified as legal analysis experts:
{memory.current.capabilityMatch.results}

Use the createGroupConversation tool to assemble them into a discussion panel.
```

### Example 3: Template-Based Routing with Properties

Use PropertySetter to capture the user's intent, then route dynamically:

**property.json (PropertySetterTask):**

```json
{
  "actions": ["user_request_specialist"],
  "setOnActions": [{
    "actions": ["user_request_specialist"],
    "setProperties": [{
      "name": "requiredSkill",
      "valueString": "{memory.current.intent}",
      "scope": "conversation"
    }]
  }]
}
```

**behavior.json:**

```json
{
  "name": "Find specialist for user request",
  "actions": ["specialist_found"],
  "conditions": [
    {
      "type": "capabilityMatch",
      "configs": {
        "skill": "{properties.requiredSkill}",
        "strategy": "highest_confidence",
        "minResults": "1"
      }
    }
  ]
}
```

***

## Attribute Filtering

The `CapabilityRegistryService` also supports fine-grained attribute matching via the `findBySkillAndAttributes` API. This is available programmatically (e.g., from MCP tools or REST) but not yet exposed as a behavior rule config. Example:

```java
// Find translation agents that support German
var matches = registry.findBySkillAndAttributes(
    "language-translation",
    Map.of("languages", "de"),
    "highest_confidence"
);
```

Comma-separated attribute values are matched with `contains` — `"en,de,fr"` matches `"de"`.

***

## Metrics

The `CapabilityRegistryService` exposes metrics at `/q/metrics`:

| Metric                        | Description                        |
| ----------------------------- | ---------------------------------- |
| `eddi.capability.query.count` | Total number of capability queries |
| `eddi.capability.query.time`  | Query execution time distribution  |

***

## ContentTypeMatcher — Attachment Routing

A companion condition for routing based on **attachment MIME types**:

```json
{
  "name": "Process image attachments",
  "actions": ["analyze_image"],
  "conditions": [
    {
      "type": "contentTypeMatcher",
      "configs": {
        "mimeType": "image/*",
        "minCount": "1"
      }
    }
  ]
}
```

| Config     | Default | Description                                                      |
| ---------- | ------- | ---------------------------------------------------------------- |
| `mimeType` | —       | MIME type pattern (supports `*/*`, `image/*`, `application/pdf`) |
| `minCount` | `1`     | Minimum number of matching attachments                           |

This condition reads from the `attachments` memory key populated by the attachment pipeline (see `docs/attachments-guide.md`).


# Deployment Management of Agents

## Overview

**Deployment Management** controls the lifecycle of your agents across different environments. In EDDI, agents go through a **create → configure → deploy** workflow before they can process conversations.

### Why Deployment Management?

Deployment management provides:

* **Environment Isolation**: Test agents without affecting production
* **Version Control**: Deploy specific agent versions, roll back if needed
* **Gradual Rollout**: Test agents in `test` environment before deploying to `production`
* **Zero-Downtime Updates**: Deploy new versions while old ones are still running
* **Audit Trail**: Track what's deployed, when, and by whom

### EDDI Environments

| Environment      | Purpose                    | Access Control            |
| ---------------- | -------------------------- | ------------------------- |
| **`test`**       | Development and testing    | Same as production        |
| **`production`** | Live deployments (default) | Optional OAuth (Keycloak) |

### Deployment Lifecycle

```
1. CREATE Agent
   POST /agentstore/agents
   → Agent exists but is NOT deployed

2. DEPLOY Agent
   POST /administration/production/deploy/agent123?version=1
   → Agent becomes active and can handle conversations

3. USE Agent
   POST /agents/agent123/start
   → Users can now interact with the agent

4. UPDATE Agent
   Create new version → Deploy new version
   → Old version still available if specified

5. UNDEPLOY Agent
   POST /administration/production/undeploy/agent123
   → Agent stops processing new conversations
```

### Auto-Deploy Feature

* **`autoDeploy=true`**: Automatically deploy new versions when agent is updated
* **`autoDeploy=false`**: Manual deployment required for each version

This is useful for:

* **Development**: Auto-deploy to `test` for rapid iteration
* **Production**: Manual deployment to `production` for controlled releases

### Checking Deployment Status

You can check:

* **Single Agent Status**: Is agent123 deployed in production?
* **All Deployments**: List all deployed agents across environments
* **Version Info**: Which version is currently deployed?

## Deployment Operations

In this section we will discuss the deployment management of Agents, including deployment/undeployment, checking deployment status, and listing all deployed Agents.

After all the required resources for the agent have been created and configured (**`Dictionary`**, **`Behavior Rules`**, **`Output`**, **`Workflow`**, etc.) and the Agent is created through **`POST`** to **`/agentstore/agents`**, deployment management is key to having granular control over deployed agents.

## **Deployment of an Agent :**

The deployment of a specific agent is done through a **`POST`** to **`/administration/{environment}/deploy/{agentId}`**

### Deploy Agent REST API Endpoint

| Element       | Value                                                                                |
| ------------- | ------------------------------------------------------------------------------------ |
| HTTP Method   | `POST`                                                                               |
| API endpoint  | `/administration/{environment}/deploy/{agentId}`                                     |
| {environment} | (`Path parameter`):`String` deployment environment: `production` (default) or `test` |
| {agentId}     | (`Path parameter`):`String` id of the agent that you wish to **deploy**.             |

### Example *:*

*Request URL:*

`http://localhost:7070/administration/production/deploy/5aaf98e19f7dd421ac3c7de9?version=1&autoDeploy=true`

*Response Body:*

`no content`

*Response Code:*

`202`

## **Undeployment of an Agent**

The undeployment of a specific agent is done through a **`POST`** to **`/administration/{environment}/undeploy/{agentId}`**

### Undeploy Agent REST API Endpoint

| Element       | Value                                                                                |
| ------------- | ------------------------------------------------------------------------------------ |
| HTTP Method   | `POST`                                                                               |
| API endpoint  | `/administration/{environment}/undeploy/{agentId}`                                   |
| {environment} | (`Path parameter`):`String` deployment environment: `production` (default) or `test` |
| {agentId}     | (`Path parameter`):`String` id of the agent that you wish to **undeploy**.           |

### Example :

**Undeploy an agent**

*Request URL*

`http://localhost:7070/administration/production/undeploy/5aaf98e19f7dd421ac3c7de9?version=1`

*Response Body*

`no content`

*Response Code*

`202`

## **Check the deployment status of an agent:**

Check the deployment status of an agent is done through a **`GET`** to **`/administration/{environment}/deploymentstatus/{agentId}`**

Deployment status of an Agent REST API Endpoint

| Element       | Value                                                                                             |
| ------------- | ------------------------------------------------------------------------------------------------- |
| HTTP Method   | `GET`                                                                                             |
| Api endpoint  | `/administration/{environment}/deploymentstatus/{agentId}`                                        |
| {environment} | (`Path parameter`):`String` deployment environment: `production` (default) or `test`              |
| {agentId}     | (`Path parameter`):`String` id of the agent that you wish to **check** its **deployment status**. |
| Response      | `NOT_FOUND`, `IN_PROGRESS`, `ERROR` and `READY`.                                                  |

### Example\*:\*

*Request URL*

`http://localhost:7070/administration/production/deploymentstatus/5aaf98e19f7dd421ac3c7de9?version=1`

*Response Body*

`READY`

*Response Code*

`200`

## **List all deployed Agents:**

To list all deployed Agents, send a `GET` to `/deploymentstore/deployments`:

### List of Deployed Agents REST API Endpoint

| Element      | Value                          |
| ------------ | ------------------------------ |
| HTTP Method  | `GET`                          |
| API endpoint | `/deploymentstore/deployments` |

### Example:

*Request URL*

`http://localhost:7070/deploymentstore/deployments`

*Response Code*

`200`

*Response Body*

```json
[
  {
    "agentId": "5aaf90e29f7dd421ac3c7dd4",
    "agentVersion": 1,
    "environment": "production",
    "deploymentStatus": "deployed"
  },
  {
    "agentId": "5aaf98e19f7dd421ac3c7de9",
    "agentVersion": 1,
    "environment": "production",
    "deploymentStatus": "deployed"
  }
]
```

***

## Deleting an Agent

### Simple Delete (Soft-Delete)

Marks the agent as deleted but keeps it in the database. The agent can potentially be restored.

| Element      | Value                                                                                       |
| ------------ | ------------------------------------------------------------------------------------------- |
| HTTP Method  | `DELETE`                                                                                    |
| API endpoint | `/agentstore/agents/{id}?version={version}`                                                 |
| {id}         | (`Path parameter`) `String` agent ID                                                        |
| version      | (`Query parameter`) `Integer` version                                                       |
| permanent    | (`Query parameter`) `Boolean` default `false`. If `true`, permanently removes from database |

```
DELETE /agentstore/agents/5aaf98e19f7dd421ac3c7de9?version=1
→ 200 OK (soft-deleted)

DELETE /agentstore/agents/5aaf98e19f7dd421ac3c7de9?version=1&permanent=true
→ 200 OK (permanently removed)
```

### Cascade Delete

Deletes the agent **and all its child resources** in one operation. This is the recommended way to fully clean up an agent and avoid orphaned resources.

```
Agent
 └── Workflow 1
 │    ├── Behavior Set
 │    ├── HTTP Calls
 │    ├── Output Set
 │    ├── LangChain Config
 │    ├── Property Setter
 │    └── Parser (with dictionaries)
 └── Workflow 2
      └── ...
```

| Element      | Value                                                                                                  |
| ------------ | ------------------------------------------------------------------------------------------------------ |
| HTTP Method  | `DELETE`                                                                                               |
| API endpoint | `/agentstore/agents/{id}?version={version}&cascade=true&permanent=true`                                |
| cascade      | (`Query parameter`) `Boolean` default `false`. If `true`, deletes packages and all extension resources |
| permanent    | (`Query parameter`) `Boolean` default `false`. Recommended `true` with cascade                         |

#### Example

```
DELETE /agentstore/agents/5aaf98e19f7dd421ac3c7de9?version=1&cascade=true&permanent=true
→ 200 OK
```

This will:

1. Read the agent configuration to discover its packages
2. For each package, read its extensions and delete all resources (behavior sets, HTTP calls, output sets, langchains, property setters, parser dictionaries)
3. Delete each package
4. Delete the agent itself

> **Note:** Cascade delete is error-tolerant. If individual resource deletions fail (e.g., resource already deleted), the operation continues and the agent itself is still deleted. Failures are logged server-side.

> **Safety:** Cascade delete checks for shared references before deleting each resource. If a package is used by another agent, or an extension resource is used by another package, it will be **skipped** (not deleted). Only resources exclusively owned by the deleted agent are removed.

### Cascade Delete for Workflows

Workflows can also be individually cascade-deleted:

```
DELETE /packagestore/packages/{id}?version={version}&cascade=true&permanent=true
→ 200 OK (package + all extension resources deleted)
```

### Important: Undeploy Before Deleting

If the agent is currently deployed, you should **undeploy** it first:

```
POST /administration/production/undeploy/{agentId}?endAllActiveConversations=true
→ 202 Accepted

DELETE /agentstore/agents/{agentId}?version=1&cascade=true&permanent=true
→ 200 OK
```

***

## Orphan Detection and Cleanup

Over time, resources can become orphaned — they exist in the database but are no longer referenced by any agent or package. The orphan admin endpoint helps detect and clean up these resources.

### Scan for Orphans (Dry Run)

```
GET /administration/orphans
→ 200 OK
```

Returns a report listing all unreferenced resources across all stores (workflows, behavior sets, HTTP calls, output sets, LLMs, property setters, dictionaries, parsers).

| Element        | Value                                                                                      |
| -------------- | ------------------------------------------------------------------------------------------ |
| HTTP Method    | `GET`                                                                                      |
| API endpoint   | `/administration/orphans`                                                                  |
| includeDeleted | (`Query parameter`) `Boolean` default `false`. `true` also includes soft-deleted resources |

**Example Response:**

```json
{
  "totalOrphans": 3,
  "deletedCount": 0,
  "orphans": [
    {
      "resourceUri": "eddi://ai.labs.package/packagestore/packages/abc123?version=1",
      "type": "ai.labs.package",
      "name": "Unused Workflow",
      "deleted": false
    },
    {
      "resourceUri": "eddi://ai.labs.behavior/behaviorstore/behaviorsets/def456?version=1",
      "type": "ai.labs.behavior",
      "name": "Old Behavior Set",
      "deleted": true
    }
  ]
}
```

### Purge Orphans

```
DELETE /administration/orphans
→ 200 OK
```

Permanently deletes all orphaned resources. This is **irreversible** — it removes the current document *and* its entire version history.

| Element        | Value                                                                                                       |
| -------------- | ----------------------------------------------------------------------------------------------------------- |
| HTTP Method    | `DELETE`                                                                                                    |
| API endpoint   | `/administration/orphans`                                                                                   |
| includeDeleted | (`Query parameter`) `Boolean` default `false`. `true` also purges soft-deleted resources                    |
| 409 Conflict   | Returned when the reference scan is incomplete; **nothing is deleted**. Body: `{"error":"incomplete_scan"}` |

Pass the **same** `includeDeleted` value you used for the scan, so the purge acts on the set you reviewed.

> **Changed in 6.1.x:** `includeDeleted` was previously an *equality* filter — `true` matched only soft-deleted resources instead of adding them to the live ones — and this endpoint defaulted to `true` while the scan defaulted to `false`, so a scan followed by a purge operated on **disjoint** sets. `true` now means "live and soft-deleted", and the default is `false`. A client relying on the old default now purges *less*; pass `includeDeleted=true` to restore the wider sweep.

The purge refuses with **409** rather than proceeding when the referenced-resource scan could not be completed (an unreadable Agent or workflow, or a store type exceeding the scan ceiling). A partial reference set makes live, in-use resources look unreferenced, so purging against one could destroy working configuration.


# Agent Sync (Live & ZIP)

## Overview

Agent Sync lets you synchronize agent configurations between two running EDDI instances **without exporting/importing ZIP files**. It uses the same structural matching and content diffing pipeline as ZIP imports, but reads directly from a remote EDDI instance over HTTP.

### When to Use

| Scenario                                      | Use                                            |
| --------------------------------------------- | ---------------------------------------------- |
| One-off agent migration between environments  | ZIP Import/Export                              |
| Regular dev → staging → production promotions | **Agent Sync**                                 |
| Keeping multiple EDDI instances in sync       | **Agent Sync**                                 |
| Sharing agents with external teams            | ZIP Import/Export                              |
| CI/CD pipeline deployments                    | Either (Sync for live, ZIP for artifact-based) |

## Prerequisites

* Both EDDI instances must be reachable over HTTP/HTTPS
* The **source** instance must have the agent deployed
* If the source requires authentication, you'll need a valid Bearer token

## Workflow

### 1. List Remote Agents

First, discover which agents are available on the remote instance:

```bash
curl -X GET "http://localhost:7070/backup/import/sync/agents?sourceUrl=https://source-eddi.example.com" \
  -H "X-Source-Authorization: Bearer <token>"
```

**Response:** List of agent descriptors from the remote instance.

### 2. Preview Changes (Single Agent)

Before syncing, preview what would change:

```bash
curl -X POST "http://localhost:7070/backup/import/sync/preview?sourceUrl=https://source-eddi.example.com&sourceAgentId=remote-agent-id&sourceAgentVersion=1&targetAgentId=local-agent-id" \
  -H "X-Source-Authorization: Bearer <token>"
```

**Response:** An `ImportPreview` with resource diffs:

```json
{
  "resources": [
    {
      "resourceType": "agent",
      "action": "UPDATE",
      "originId": "remote-agent-id",
      "localId": "local-agent-id"
    },
    {
      "resourceType": "llm",
      "action": "UPDATE",
      "originId": "remote-llm-id",
      "localId": "local-llm-id"
    },
    {
      "resourceType": "behavior",
      "action": "SKIP",
      "originId": "remote-behavior-id",
      "localId": "local-behavior-id"
    }
  ]
}
```

**Actions explained:**

| Action     | Meaning                                                    |
| ---------- | ---------------------------------------------------------- |
| `CREATE`   | Resource doesn't exist locally — will be created           |
| `UPDATE`   | Resource exists locally — content differs, will be updated |
| `SKIP`     | Resource is identical — no changes needed                  |
| `CONFLICT` | Structural mismatch — review needed                        |

### 3. Preview Batch (Multiple Agents)

Preview sync for multiple agents at once. The request body is a JSON array of `SyncMapping` objects:

```bash
curl -X POST "http://localhost:7070/backup/import/sync/preview/batch?sourceUrl=https://source-eddi.example.com" \
  -H "Content-Type: application/json" \
  -H "X-Source-Authorization: Bearer <token>" \
  -d '[
    { "sourceAgentId": "agent-1", "sourceAgentVersion": 1, "targetAgentId": "local-1" },
    { "sourceAgentId": "agent-2", "sourceAgentVersion": 2, "targetAgentId": "local-2" }
  ]'
```

**Response:** A JSON array of `ImportPreview` objects, one per mapping.

### 4. Execute Sync

Once you've reviewed the preview and are satisfied:

```bash
curl -X POST "http://localhost:7070/backup/import/sync?sourceUrl=https://source-eddi.example.com&sourceAgentId=remote-agent-id&sourceAgentVersion=1&targetAgentId=local-agent-id" \
  -H "X-Source-Authorization: Bearer <token>"
```

You can also pass `selectedResources` and `workflowOrder` as query parameters for fine-grained control:

```bash
curl -X POST "http://localhost:7070/backup/import/sync?sourceUrl=https://source-eddi.example.com&sourceAgentId=remote-agent-id&sourceAgentVersion=1&targetAgentId=local-agent-id&selectedResources=res-1,res-2" \
  -H "X-Source-Authorization: Bearer <token>"
```

### 5. Execute Batch Sync

Sync multiple agents in one call. The request body is a JSON array of `SyncRequest` objects:

```bash
curl -X POST "http://localhost:7070/backup/import/sync/batch?sourceUrl=https://source-eddi.example.com" \
  -H "Content-Type: application/json" \
  -H "X-Source-Authorization: Bearer <token>" \
  -d '[
    {
      "sourceAgentId": "agent-1",
      "sourceAgentVersion": 1,
      "targetAgentId": "local-1",
      "selectedResources": null,
      "workflowOrder": null
    },
    {
      "sourceAgentId": "agent-2",
      "sourceAgentVersion": 2,
      "targetAgentId": "local-2",
      "selectedResources": ["res-a", "res-b"],
      "workflowOrder": null
    }
  ]'
```

> **Partial success:** If one agent fails during batch sync, the remaining agents still sync. The response indicates success/failure per agent.

## API Reference

| Method | Path                                | Purpose                   |
| ------ | ----------------------------------- | ------------------------- |
| `GET`  | `/backup/import/sync/agents`        | List remote agents        |
| `POST` | `/backup/import/sync/preview`       | Single-agent sync preview |
| `POST` | `/backup/import/sync/preview/batch` | Multi-agent sync preview  |
| `POST` | `/backup/import/sync`               | Execute single-agent sync |
| `POST` | `/backup/import/sync/batch`         | Execute multi-agent sync  |

### Parameters

**Query parameters (all endpoints):**

| Parameter            | Required          | Description                                |
| -------------------- | ----------------- | ------------------------------------------ |
| `sourceUrl`          | Yes               | Base URL of the source EDDI instance       |
| `sourceAgentId`      | Yes (single)      | Agent ID on the remote instance            |
| `sourceAgentVersion` | No                | Version to sync (null = latest)            |
| `targetAgentId`      | No                | Local agent to upgrade (null = create new) |
| `selectedResources`  | No (execute only) | Comma-separated resource IDs to sync       |
| `workflowOrder`      | No (execute only) | Desired workflow order after sync          |

> **Note:** `sourceUrl` and agent parameters are query parameters. Batch endpoints accept `SyncMapping[]` / `SyncRequest[]` as a JSON request body for the per-agent mappings.

**Request header:**

| Header                   | Required | Description                                     |
| ------------------------ | -------- | ----------------------------------------------- |
| `X-Source-Authorization` | No       | Bearer token for authenticated source instances |

## How Structural Matching Works

Agent Sync uses **structural matching** — not ID matching — to pair source and target resources. This means it works even when the source and target agents were created independently.

| Resource Type  | Matching Strategy                             | Rationale                                   |
| -------------- | --------------------------------------------- | ------------------------------------------- |
| **Agent**      | Direct (by `targetAgentId` parameter)         | User explicitly selects the target          |
| **Workflows**  | Position index in agent's workflow list       | Workflows have a defined order              |
| **Extensions** | `WorkflowStep.type` URI (e.g., `ai.labs.llm`) | Each type appears at most once per workflow |
| **Snippets**   | `PromptSnippet.name` (natural key)            | Names are unique by convention              |

### Key Design Decisions

* **In-place upgrade:** Target resource IDs are preserved. URI references, deployments, and triggers continue to work
* **Version increments:** Each updated resource gets a new version (history preserved)
* **Secret scrubbing:** API keys and vault references are **never** transferred. The target instance uses its own secrets
* **SSRF protection:** The remote URL is validated (HTTPS required in production, private IPs blocked)

## Upgrade Strategy (ZIP Import)

The same structural matching is available for ZIP imports using `strategy=upgrade`:

```bash
# Preview what would change
curl -X POST -H "Content-Type: application/zip" \
  --data-binary @agent-export.zip \
  "http://localhost:7070/backup/import/preview?targetAgentId=local-agent-id"

# Execute upgrade (updates existing resources in-place)
curl -X POST -H "Content-Type: application/zip" \
  --data-binary @agent-export.zip \
  "http://localhost:7070/backup/import?strategy=upgrade&targetAgentId=local-agent-id"
```

This is the same pipeline as Live Sync — the only difference is the transport (ZIP file vs HTTP).

## Selective Export

Export only the resources you want:

```bash
# 1. Preview the export tree
curl -X POST "http://localhost:7070/backup/export/agent-id/preview?agentVersion=1"

# 2. Select specific resources and export
curl -X POST "http://localhost:7070/backup/export/agent-id?agentVersion=1&selectedResources=res1,res2,res3"
```

The preview returns a resource tree with selectability flags. Agent and workflow skeletons are always included — you can deselect individual extensions, behavior rules, or prompt snippets.

## See Also

* [Import/Export an Agent](/getting-started/import-export-an-agent) — ZIP-based import/export (create and merge strategies)
* [Agent Sync Architecture](/reference/agent-sync-architecture) — Internal architecture and matching algorithm details
* [Deployment Management](/conversations-and-orchestration/deployment-management-of-agents) — Deploying agents after sync


# MCP Server

EDDI exposes its agent conversation and administration capabilities via the **Model Context Protocol (MCP)**, enabling AI assistants (Claude Desktop, IDE plugins, custom MCP clients) to interact with deployed agents and manage the platform programmatically.

## Transport

EDDI uses **Streamable HTTP** transport, served by the Quarkus MCP Server extension (`quarkus-mcp-server-http`).

| Endpoint                    | Description                           |
| --------------------------- | ------------------------------------- |
| `http://localhost:7070/mcp` | MCP server endpoint (default + admin) |

## Available Tools (74)

### Conversation Tools (11)

| Tool                    | Description                                                                                                                 |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `list_agents`           | List all deployed agents with status, version, and name                                                                     |
| `list_agent_configs`    | List all agent configurations (including undeployed)                                                                        |
| `create_conversation`   | Start a new conversation with a deployed agent                                                                              |
| `talk_to_agent`         | Send a message and get the agent's response                                                                                 |
| `chat_with_agent`       | Create a conversation and send a message in one call                                                                        |
| `read_conversation`     | Read conversation history, memory, and quick replies                                                                        |
| `read_conversation_log` | Read conversation log as formatted text                                                                                     |
| `list_conversations`    | List all conversations for a specific agent                                                                                 |
| `get_agent`             | Get an agent's full configuration (packages, name, description)                                                             |
| `discover_agents`       | Discover deployed agents enriched with intent mappings from agent triggers. Best way to find agents by purpose              |
| `chat_managed`          | Send a message using intent-based managed conversations (one conversation per intent+userId, auto-creates on first message) |

### Admin Tools (13)

| Tool                    | Description                                                                     |
| ----------------------- | ------------------------------------------------------------------------------- |
| `deploy_agent`          | Deploy an agent version to an environment                                       |
| `undeploy_agent`        | Undeploy an agent from an environment                                           |
| `get_deployment_status` | Get deployment status of a specific agent version                               |
| `list_workflows`        | List all packages (pipeline configurations)                                     |
| `create_agent`          | Create a new agent                                                              |
| `delete_agent`          | Delete an agent (with optional cascade)                                         |
| `update_agent`          | Update an agent's name/description and optionally redeploy                      |
| `read_workflow`         | Read a package's full pipeline configuration                                    |
| `read_resource`         | Read any resource config by type (behavior, langchain, httpcalls, output, etc.) |
| `list_agent_triggers`   | List all agent triggers (intent→agent mappings) for managed conversations       |
| `create_agent_trigger`  | Create an agent trigger mapping an intent to one or more agent deployments      |
| `update_agent_trigger`  | Update an existing agent trigger                                                |
| `delete_agent_trigger`  | Delete an agent trigger for a given intent                                      |

### Resource CRUD Tools (5)

| Tool                   | Description                                                                         |
| ---------------------- | ----------------------------------------------------------------------------------- |
| `update_resource`      | Update any resource config by type and ID. Returns the new version URI              |
| `create_resource`      | Create a new resource. Returns the new resource ID and URI                          |
| `delete_resource`      | Delete a resource (soft-delete by default, `permanent=true` for hard delete)        |
| `apply_agent_changes`  | Batch-cascade URI changes through package → agent in ONE pass, optionally redeploy  |
| `list_agent_resources` | Walk agent → packages → extensions to get a complete resource inventory in one call |

### Diagnostic Tools (2)

| Tool               | Description                                                                                |
| ------------------ | ------------------------------------------------------------------------------------------ |
| `read_agent_logs`  | Read server-side pipeline logs (errors, LLM timeouts) filtered by agent/conversation/level |
| `read_audit_trail` | Read per-task audit entries with LLM details, timing, cost, and tool calls                 |

### Setup Tools (2)

| Tool               | Description                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setup_agent`      | Create a fully working agent in one call: creates behavior rules, LangChain config, optional output/greeting, package, agent, and deploys. Supports built-in tools, quick replies, and sentiment analysis. Default: `anthropic`/`claude-sonnet-4-6`                                                                                                                                                             |
| `create_api_agent` | Create an agent from an OpenAPI 3.0/3.1 spec. Parses the spec, generates HttpCalls configs (grouped by API tag), creates the full pipeline, and deploys. Supports endpoint filtering, base URL override, auth header propagation, and `mcpServerUrls` to add an MCP server's tools alongside the generated ones. A generated write tool takes the whole request body as one `requestBody` parameter — see below |

> **The approval gate is not settable over MCP.** `POST /administration/agents/setup-api` accepts a `hitlConfig` on the request body, so a caller can provision an agent whose write tools are gated from v1 onward. The MCP `create_api_agent` tool deliberately has **no** such parameter and always passes `null`: it already provisions an agent with a caller-chosen endpoint filter, so letting the caller also choose the gate would make it a complete escape from whatever allow-list governs the agent doing the calling. Provisioning a gated agent goes through REST (`eddi-admin`).

### Schedule Management Tools (6)

| Tool                    | Description                                                                                                                                                                                         |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create_schedule`       | Create a new scheduled agent trigger (cron job or heartbeat). For CRON: provide `cronExpression`. For HEARTBEAT: provide `heartbeatIntervalSeconds`. Heartbeats default to persistent conversations |
| `list_schedules`        | List all scheduled agent triggers with name, type, cron/interval, status, next fire time, and fire count. Optionally filter by agentId                                                              |
| `read_schedule`         | Read a schedule's full configuration including recent fire history (last 10 executions)                                                                                                             |
| `delete_schedule`       | Delete a scheduled agent trigger                                                                                                                                                                    |
| `fire_schedule_now`     | Manually trigger a schedule fire immediately. Useful for testing or one-off executions                                                                                                              |
| `retry_failed_schedule` | Re-queue a dead-lettered schedule for another fire attempt after fixing the cause of failure                                                                                                        |

### Group Conversation Tools (11)

| Tool                         | Description                                                                                                                                                                   |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `describe_discussion_styles` | Rich descriptions of all 6 discussion styles with phase flows, member roles, and use cases                                                                                    |
| `list_groups`                | List all group configurations with name, style, member count                                                                                                                  |
| `read_group`                 | Read a group configuration's full details                                                                                                                                     |
| `create_group`               | Create a group (members, moderator, style, roles, member types, tasks). Supports nested groups via `memberTypes=GROUP` and pre-configured TASK\_FORCE tasks via `tasks` param |
| `update_group`               | Update a group configuration (full JSON replacement)                                                                                                                          |
| `delete_group`               | Delete a group configuration                                                                                                                                                  |
| `discuss_with_group`         | Start a multi-agent discussion on a question. Returns full transcript + synthesized answer                                                                                    |
| `read_group_conversation`    | Read a group conversation transcript                                                                                                                                          |
| `list_group_conversations`   | List past group discussions for a group, with state and timestamps                                                                                                            |
| `start_group_discussion`     | Start a discussion asynchronously (returns immediately with groupConversationId). Poll with `read_group_conversation`                                                         |
| `delete_group_conversation`  | Delete a group conversation and cascade-delete all member conversations                                                                                                       |

See [Group Conversations](/conversations-and-orchestration/group-conversations) for full style details, custom phases, and nested groups.

### HITL Tools (9)

Resolve Human-in-the-Loop approval gates over MCP — the counterpart to the REST HITL endpoints, at parity for both the regular (1:1) and group surfaces. Authorization mirrors REST exactly (per-conversation owner / `eddi-admin` / `eddi-approver` via the shared `HitlAccessGuard`); decisions are attributed server-side as `mcp:<principal>`. Mutating tools honour the `eddi.mcp.hitl.mutations.enabled` kill-switch and return structured errors (`errorCode` ∈ `NOT_FOUND | WRONG_STATE | FORBIDDEN | DISABLED | BAD_REQUEST`).

| Tool                               | Description                                                                                                                    |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `list_pending_approvals`           | List regular (1:1) conversations awaiting approval (owner-scoped; includes RULE and TOOL\_CALL pauses)                         |
| `get_approval_status`              | Read a paused conversation's status; summary reports `pauseType`, `detail=full` returns the snapshot incl. any tool-call batch |
| `resume_conversation`              | Resume with APPROVED/REJECTED (case-insensitive); resolves both RULE and TOOL\_CALL pauses                                     |
| `cancel_conversation`              | Cancel a paused or running conversation                                                                                        |
| `list_group_pending_approvals`     | List a group's conversations awaiting approval (owner-scoped)                                                                  |
| `list_all_group_pending_approvals` | Cross-group HITL inbox across all groups (owner-scoped)                                                                        |
| `get_group_approval_status`        | Read a paused group discussion's status (summary; `detail=full` returns the whole conversation)                                |
| `approve_group_phase`              | Approve/reject a paused phase, with optional `taskApprovals` JSON for TASK granularity; returns the resumed discussion         |
| `cancel_group_discussion`          | Cancel an in-progress or paused group discussion                                                                               |

See [HITL](https://github.com/labsai/EDDI/tree/main/docs/hitl.md#mcp-surface) for the full authority model, the kill-switch, and REST-endpoint parity.

### Memory Tools (8)

| Tool                       | Description                                                                               |
| -------------------------- | ----------------------------------------------------------------------------------------- |
| `list_user_memories`       | List all persistent memory entries for a user                                             |
| `get_visible_memories`     | Get memories visible to a specific agent, considering self/group/global visibility scopes |
| `search_user_memories`     | Search user memories by keyword across keys and values                                    |
| `get_memory_by_key`        | Get a specific memory entry by key for a user                                             |
| `upsert_user_memory`       | Create or update a persistent memory entry for a user                                     |
| `delete_user_memory`       | Delete a specific memory entry by ID                                                      |
| `delete_all_user_memories` | Delete all memory entries for a user (GDPR-compliant bulk erasure)                        |
| `count_user_memories`      | Count total memory entries for a user                                                     |

See [User Memory](/architecture-and-concepts/user-memory) for visibility scoping, recall order, and dream consolidation.

### GDPR Tools (2)

| Tool               | Description                                                                                                                     |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `delete_user_data` | Cascade-delete all user data across all stores (GDPR Art. 17 Right to Erasure). Requires `confirmation='CONFIRM'`. Irreversible |
| `export_user_data` | Export all data for a user (GDPR Art. 15/20 Right of Access / Data Portability)                                                 |

See [GDPR / CCPA Compliance](/security-and-compliance/gdpr-compliance) for data erasure, export, and retention details.

### Channel Integration Tools (5)

| Tool                         | Description                                                                                  |
| ---------------------------- | -------------------------------------------------------------------------------------------- |
| `list_channel_integrations`  | List all channel integrations with name, type, and target count                              |
| `read_channel_integration`   | Read a channel integration's full configuration                                              |
| `create_channel_integration` | Create a new channel integration (Slack, Teams, etc.) with platform config and agent targets |
| `update_channel_integration` | Update an existing channel integration                                                       |
| `delete_channel_integration` | Delete a channel integration (soft or permanent)                                             |

See [Slack Integration](/protocols-and-integration/slack-integration) for Slack-specific setup and multi-agent thread discussions.

## MCP Resources

EDDI also exposes its documentation as MCP **resources**, allowing AI agents to browse and read the docs programmatically.

| Resource             | Description                                               |
| -------------------- | --------------------------------------------------------- |
| `eddi://docs/index`  | List all available documentation pages                    |
| `eddi://docs/{name}` | Read a specific doc (e.g., `eddi://docs/getting-started`) |

Configure the docs path with: `eddi.docs.path` (default: `docs/`, in Docker: `/deployments/docs`).

### The same docs over REST

> **MCP resources do not reach an EDDI agent.** A resource is only usable by a client that asks for it, and EDDI's own MCP client never calls `resources/read` — it consumes *tools*. So `eddi://docs/*` made EDDI's documentation readable by a desktop MCP client and not by an agent running on EDDI, which is precisely backwards for an agent whose job is to explain the platform.

The same doc set is therefore served read-only over REST, where an agent generated from EDDI's OpenAPI spec picks it up as ordinary tools:

| Endpoint                          | Role                                                                            | Returns                                                     |
| --------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `GET /administration/docs`        | any of `eddi-admin`, `eddi-editor`, `eddi-user`, `eddi-approver`, `eddi-viewer` | JSON array of page names, without the `.md` suffix          |
| `GET /administration/docs/{name}` | same                                                                            | The page's markdown source as `text/plain`; `404` if absent |

> **Roles are enumerated, not inherited.** EDDI has no role hierarchy — JAX-RS `@RolesAllowed` and the MCP layer's `requireRole` are both literal `hasRole` checks — so `eddi-viewer` alone would refuse an `eddi-admin`. The widest read tier is spelled out because these are published documentation pages.

Both surfaces delegate to `DocsService`, which owns the filesystem access and the path-traversal guard.

> **The runtime doc set is smaller than the repository's.** The container image copies only top-level `docs/*.md` (non-recursive, so nothing under `docs/agent-configs/` or `docs/templates/` is reachable) and then removes `changelog.md`, `code-review-standards.md`, `incident-response.md` and `SUMMARY.md`. Call the index and read from it — do not assume a particular page exists.

## Quick Start

### Client Configuration

EDDI uses **Streamable HTTP** transport at `http://localhost:7070/mcp`. How you connect depends on your client's transport support.

#### Direct HTTP (Streamable HTTP clients)

Clients that natively support HTTP transport (e.g., IDE plugins, custom MCP clients) can connect directly:

```json
{
  "mcpServers": {
    "eddi": {
      "url": "http://localhost:7070/mcp"
    }
  }
}
```

#### Antigravity (Google)

Add EDDI as an MCP server in your Antigravity settings (`.gemini/config/settings.json` or workspace `.agents/settings.json`):

```json
{
  "mcpServers": {
    "eddi": {
      "serverUrl": "http://localhost:7070/mcp"
    }
  }
}
```

Antigravity connects natively via Streamable HTTP — no bridge required.

#### stdio Bridge (Claude Desktop, Cursor, Windsurf, etc.)

Many MCP clients — including Claude Desktop's `claude_desktop_config.json` — only support **stdio** transport (spawning a local subprocess). They cannot connect to HTTP endpoints directly.

Use [`mcp-remote`](https://github.com/geelen/mcp-remote) to bridge the gap. It runs as a local stdio process and proxies requests to EDDI's HTTP endpoint:

```json
{
  "mcpServers": {
    "eddi": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://localhost:7070/mcp"]
    }
  }
}
```

**Windows users** — if `npx` is not on your shell PATH, wrap via `cmd`:

```json
{
  "mcpServers": {
    "eddi": {
      "command": "cmd",
      "args": ["/c", "npx", "-y", "mcp-remote", "http://localhost:7070/mcp"]
    }
  }
}
```

> **What is `mcp-remote`?** An open-source npm package ([github.com/geelen/mcp-remote](https://github.com/geelen/mcp-remote)) that acts as an invisible bridge between stdio-only MCP clients and HTTP-based MCP servers. It handles protocol translation, session management, and authentication. Requires Node.js 18+.

### Example Workflow

```
1. list_agents → see deployed agents
2. create_conversation(agentId: "my-agent") → get conversationId
3. talk_to_agent(agentId: "my-agent", conversationId: "...", message: "Hello!") → get response
4. read_conversation_log(conversationId: "...") → see full history
```

### Discovering Agents by Purpose

```
1. discover_agents() → enriched list with intents per agent
2. list_agent_triggers() → see all intent→agent mappings
```

### Intent-Based Managed Chat

```
1. create_agent_trigger(config: {"intent":"support","agentDeployments":[{"agentId":"agent-123"}]})
2. chat_managed(intent: "support", userId: "user1", message: "Hello!") → auto-creates conversation
3. chat_managed(intent: "support", userId: "user1", message: "I need help") → reuses same conversation
```

### Inspecting Agent Configuration

```
1. list_agent_resources(agentId: "my-agent") → complete resource inventory in one call
2. read_resource(resourceType: "langchain", resourceId: "lc-456") → see LLM config details
```

### Modifying Resources + Cascade

```
1. read_resource("langchain", "lc-456") → get current config
2. update_resource("langchain", "lc-456", version: 1, config: {...}) → new version 2
3. apply_agent_changes(agentId, agentVersion, [{oldUri: "...?version=1", newUri: "...?version=2"}], redeploy: true)
```

### Debugging an Agent

```
1. read_agent_logs(agentId: "my-agent") → see pipeline errors, LLM timeouts
2. read_audit_trail(conversationId: "conv-123") → per-task execution details, LLM tokens, cost
```

### Scheduling a Cron Job

```
1. create_schedule(agentId: "my-agent", triggerType: "CRON", cron: "0 9 * * MON-FRI",
     message: "Daily morning check-in", name: "Weekday Morning Check")
   → { scheduleId: "sched-1", description: "At 09:00 on every weekday", nextFire: "..." }
2. list_schedules() → see all scheduled triggers with status
3. fire_schedule_now(scheduleId: "sched-1") → test immediately
4. read_schedule(scheduleId: "sched-1") → see full config + fire logs
```

### Setting Up a Heartbeat

```
1. create_schedule(agentId: "my-agent", triggerType: "HEARTBEAT", heartbeatIntervalSeconds: 300,
     name: "Health Heartbeat")
   → { scheduleId: "hb-1", description: "Every 5 minutes", conversationStrategy: "persistent" }
   # Heartbeats default to: persistent conversation, "heartbeat" message, drift-proof scheduling
2. read_schedule(scheduleId: "hb-1") → check next fire time and conversation ID
3. retry_failed_schedule(scheduleId: "hb-1") → requeue if dead-lettered
```

### Running a Multi-Agent Discussion

```
1. describe_discussion_styles → see all available styles with examples
2. create_group(name: "Architecture Review", memberAgentIds: "expert-1,expert-2",
     moderatorAgentId: "moderator", style: "PEER_REVIEW")
   → { groupId: "g1" }
3. discuss_with_group(groupId: "g1", question: "Should we use microservices?")
   → { transcript: [...], synthesizedAnswer: "Based on all perspectives..." }
4. list_group_conversations(groupId: "g1") → browse past discussions
```

***

## Tool Reference — Agent Discovery & Managed Conversations

EDDI provides **two tiers** of conversation management:

| Tier          | Tools                                   | Conversations                       | Use Case                                 |
| ------------- | --------------------------------------- | ----------------------------------- | ---------------------------------------- |
| **Low-level** | `create_conversation` + `talk_to_agent` | Multiple per user, manually managed | Custom apps, multi-conversation UIs      |
| **Managed**   | `chat_managed`                          | One per intent+userId, auto-created | Single-window chat, intent-based routing |

The managed tier relies on **agent triggers** — mappings from an *intent* string to one or more agent deployments. Use the discovery and trigger tools below to configure and interact with this system.

### `discover_agents`

Discover deployed agents with their capabilities. Returns an enriched list of deployed agents, cross-referenced with intent mappings from agent triggers. This is the **best way to find agents by purpose**.

**Parameters:**

| Parameter     | Type   | Required | Default        | Description                                              |
| ------------- | ------ | -------- | -------------- | -------------------------------------------------------- |
| `filter`      | string | No       | `""`           | Filter agents by name (case-insensitive substring match) |
| `environment` | string | No       | `"production"` | Environment: `production`, `production`, or `test`       |

**Response:**

```json
{
  "count": 80,
  "agents": [
    {
      "agentId": "692f7fe8...",
      "name": "Bob Marley 2",
      "description": "gemini powered Agent",
      "version": 1,
      "status": "READY",
      "environment": "production",
      "intents": ["bob-marley-2-692f7fe8d6c14292d2b7f70c"]
    },
    {
      "agentId": "64513b3c...",
      "name": "Agent Father",
      "description": "Agent to create Connector Agents...",
      "version": 110,
      "status": "READY",
      "environment": "production"
    }
  ]
}
```

> **Note:** The `intents` array only appears for agents that have agent triggers configured. Agents without triggers are still returned — they can be interacted with via `chat_with_agent` (low-level tier) but not via `chat_managed`.

***

### `chat_managed`

Send a message to an agent using **intent-based managed conversations**. Unlike `chat_with_agent` (which requires a agentId and creates multiple conversations), this tool uses an *intent* to find the right agent and maintains **exactly one conversation per intent+userId** — like a single chat window.

The conversation is auto-created on first message and reused on subsequent calls. Requires an agent trigger to be configured for the intent (see `list_agent_triggers` / `create_agent_trigger`).

**Parameters:**

| Parameter     | Type   | Required | Description                                                              |
| ------------- | ------ | -------- | ------------------------------------------------------------------------ |
| `intent`      | string | **Yes**  | Intent that maps to an agent trigger. E.g. `"customer_support"`          |
| `userId`      | string | **Yes**  | User ID for conversation management (one conversation per intent+userId) |
| `message`     | string | **Yes**  | The user message to send                                                 |
| `environment` | string | No       | Environment: `production` (default), `production`, or `test`             |

**Response:**

```json
{
  "environment": "production",
  "conversationId": "69bc8b93...",
  "agentId": "692f7fe8...",
  "userId": "user-123",
  "intent": "bob-marley-2-692f7fe8...",
  "actions": ["send_message", "unknown"],
  "conversationState": "READY",
  "response": {
    "conversationOutputs": [{
      "output": [{ "type": "text", "text": "Hello there! ..." }]
    }],
    "conversationSteps": [...]
  }
}
```

**Behavior:**

* **First call** with a new intent+userId: creates a new conversation and sends the message
* **Subsequent calls** with the same intent+userId: reuses the existing conversation (like continuing in the same chat window)
* Returns an error if no agent trigger is configured for the given intent

***

### `list_agent_triggers`

List all agent triggers (intent→agent mappings). Returns all configured intents with their agent deployments. Agent triggers enable intent-based conversation management via `chat_managed`.

**Parameters:** None.

**Response:**

```json
{
  "count": 48,
  "triggers": [
    {
      "intent": "customer_support",
      "agentDeployments": [
        {
          "environment": "production",
          "agentId": "6544db9b...",
          "initialContext": {}
        }
      ]
    }
  ]
}
```

> **Tip:** Each trigger can map to **multiple agent deployments** — useful for A/B testing or environment-specific routing.

***

### `create_agent_trigger`

Create an agent trigger that maps an intent to one or more agents. Once created, the intent can be used with `chat_managed` to talk to the agent.

**Parameters:**

| Parameter | Type          | Required | Description                                   |
| --------- | ------------- | -------- | --------------------------------------------- |
| `config`  | string (JSON) | **Yes**  | Full trigger configuration (see schema below) |

**Config schema:**

```json
{
  "intent": "customer_support",
  "agentDeployments": [
    {
      "agentId": "64513b3c...",
      "environment": "production",
      "initialContext": {
        "language": { "type": "string", "value": "en" }
      }
    }
  ]
}
```

| Field                               | Type   | Required | Description                                                                        |
| ----------------------------------- | ------ | -------- | ---------------------------------------------------------------------------------- |
| `intent`                            | string | **Yes**  | Unique intent identifier. Convention: `slug-agentId` (e.g. `support-agent-abc123`) |
| `agentDeployments`                  | array  | **Yes**  | List of agent deployments this intent routes to                                    |
| `agentDeployments[].agentId`        | string | **Yes**  | The agent ID to route messages to                                                  |
| `agentDeployments[].environment`    | string | No       | Deployment environment (default: `production`)                                     |
| `agentDeployments[].initialContext` | object | No       | Key-value pairs injected into the conversation context on creation                 |

**Response:**

```json
{ "intent": "customer_support", "status": 200, "action": "created" }
```

***

### `update_agent_trigger`

Update an existing agent trigger. Changes the agent deployments for a given intent (e.g., to point to a new agent version, add A/B routing, or change the initial context).

**Parameters:**

| Parameter | Type          | Required | Description                                                                |
| --------- | ------------- | -------- | -------------------------------------------------------------------------- |
| `intent`  | string        | **Yes**  | The intent to update                                                       |
| `config`  | string (JSON) | **Yes**  | Full updated trigger configuration (same schema as `create_agent_trigger`) |

**Response:**

```json
{ "intent": "customer_support", "status": 200, "action": "updated" }
```

***

### `delete_agent_trigger`

Delete an agent trigger for a given intent. After deletion, `chat_managed` calls with this intent will return an error. Existing conversations are **not** deleted — they become orphaned but can still be read.

**Parameters:**

| Parameter | Type   | Required | Description          |
| --------- | ------ | -------- | -------------------- |
| `intent`  | string | **Yes**  | The intent to delete |

**Response:**

```json
{ "intent": "customer_support", "status": 200, "action": "deleted" }
```

***

### End-to-End Example: Setting Up Managed Chat

```
# 1. Create an agent (using setup_agent or the Manager UI)
setup_agent(name: "Support Agent", systemPrompt: "You are a helpful support agent...", ...)
→ { agentId: "abc123", version: 1, status: "deployed" }

# 2. Create a trigger mapping an intent to this agent
create_agent_trigger(config: {
  "intent": "customer_support",
  "agentDeployments": [{ "agentId": "abc123", "environment": "production" }]
})

# 3. Chat using the intent — conversation auto-created
chat_managed(intent: "customer_support", userId: "user-1", message: "I need help with billing")
→ { conversationId: "conv-789", response: { output: "I'd be happy to help..." } }

# 4. Continue the same conversation (same conversationId reused)
chat_managed(intent: "customer_support", userId: "user-1", message: "Can you check order #1234?")
→ { conversationId: "conv-789", response: { output: "Let me look that up..." } }

# 5. Different user gets their own conversation
chat_managed(intent: "customer_support", userId: "user-2", message: "Hello")
→ { conversationId: "conv-999", response: { output: "Welcome! How can I help?" } }

# 6. Discover what's available
discover_agents(filter: "Support") → shows the agent with its intent
```

## Configuration

In `application.properties`:

```properties
# MCP Server — Streamable HTTP at /mcp
quarkus.mcp-server.http.root-path=/mcp

# Documentation path for MCP resources (default: docs/)
eddi.docs.path=docs
```

## Tool Filtering

EDDI uses a **whitelist-based `ToolFilter`** (`McpToolFilter.java`) to control which tools are exposed via MCP.

**Why?** EDDI's langchain4j integration registers internal agent tools (calculator, datetime, websearch, etc.) that are meant ONLY for agent pipeline execution — not for external MCP clients. The `ToolFilter` SPI only sees a tool's *name* (not its declaring class or annotation type), so the whitelist is by name. It currently exposes all 74 intended tools — conversation, admin/resource/schedule/channel, setup, group, **HITL approvals** (`McpHitlTools`), **persistent user memory** (`McpMemoryTools`), and **GDPR/CCPA** (`McpGdprTools`).

To add a new MCP tool: add its name to the `MCP_TOOLS` set in `McpToolFilter.java`. A quarkus-MCP `@Tool` has no other invocation path, so a tool that is *not* whitelisted is unreachable dead code. `McpToolFilterTest.test_allMcpToolMethods_areWhitelisted()` auto-discovers every `@Tool` in the `engine.mcp` package and fails the build if any is missing from the whitelist — so forgetting this step is caught by CI.

## Authentication & Authorization

* The MCP endpoint inherits EDDI's existing OIDC/Keycloak authentication
* When auth is enabled (`quarkus.oidc.tenant-enabled=true`), MCP clients must provide valid tokens
* Authorization is enforced **in-code**, not via `@RolesAllowed`: most tools call `requireRole(identity, authEnabled, "<role>")` (`McpToolUtils`), and the HITL tools use the shared `HitlAccessGuard` (per-conversation owner / `eddi-admin` / `eddi-approver`). When `authorization.enabled=false` (the default dev posture) `requireRole` is a no-op — production is guarded by `AuthStartupGuard`, which fails startup if OIDC is disabled.
* **Future**: Per-agent MCP access control via agent configuration for multi-tenant SaaS

### Role Mapping

These are the **actual Keycloak role strings** the tools check (not aliases). Roles are additive in intent — grant an editor/admin the read scope too. For exact per-tool roles see the code (`requireRole` calls) and the per-category sections above (HITL / Memory / GDPR).

| Role            | Scope                                                                                                                                                                                                                                                                                                                       |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eddi-viewer`   | Read-only + running conversations: `list_*`, `read_*`, `get_*`, `discover_agents`, `chat_with_agent`/`talk_to_agent`/`chat_managed`, `read_agent_logs`/`read_audit_trail`, and the memory **read** tools (`list_user_memories`, `get_visible_memories`, `search_user_memories`, `get_memory_by_key`, `count_user_memories`) |
| `eddi-editor`   | Viewer + authoring: `setup_agent`, `create_api_agent`, group create/update, resource create/update, trigger/schedule/channel authoring                                                                                                                                                                                      |
| `eddi-admin`    | Editor + destructive/deployment ops: `deploy_agent`/`undeploy_agent`, `delete_*`, resource delete, and the **memory writes** (`upsert_user_memory`, `delete_user_memory`, `delete_all_user_memories`) + **GDPR** tools (`delete_user_data`, `export_user_data`)                                                             |
| `eddi-approver` | Decide HITL approvals (with the conversation owner and `eddi-admin`): `resume_conversation`, `approve_group_phase`, `cancel_*`, `*_pending_approvals`, `*_approval_status` — see [HITL](https://github.com/labsai/EDDI/tree/main/docs/hitl.md#who-may-decide)                                                               |

## Sentiment Monitoring

Agents created with `enableSentimentAnalysis=true` (via `setup_agent` or `create_api_agent`) include sentiment data in every LLM response. The sentiment object includes: `score` (-1.0 to +1.0), `trend`, `emotions`, `intent`, `urgency`, `confidence`, and `topicTags`.

This data is stored in conversation memory and can be:

* Read via `read_conversation` (in the conversation snapshot)
* Aggregated for monitoring dashboards (Manager UI log panel)
* Used for alerting (e.g., negative sentiment spike triggers notification)

## Architecture

```
┌──────────────┐     ┌──────────────────────┐
│  MCP Client  │────▶│ quarkus-mcp-server   │
│ (Claude,IDE) │◀────│ Streamable HTTP /mcp │
└──────────────┘     └──────────┬───────────┘
                                │
                  ┌─────────────┼─────────────┐
                  ▼             ▼              ▼
         ┌────────────┐ ┌────────────┐ ┌────────────┐
         │ McpConv.   │ │ McpAdmin   │ │ McpSetup   │
         │   Tools    │ │   Tools    │ │   Tools    │
         └─────┬──────┘ └─────┬──────┘ └─────┬──────┘
               │              │               │
         ┌─────▼──────┐ ┌─────▼──────┐ ┌─────▼──────┐
         │ REST API   │ │ REST API   │ │ REST API   │
         │ endpoints  │ │ endpoints  │ │ + OpenAPI  │
         └────────────┘ └────────────┘ └────────────┘

         ┌──────────────────────────────────────────┐
         │         McpDocResources                   │
         │   @Resource / @ResourceTemplate           │
         │   eddi://docs/{name}  (filesystem I/O)    │
         └──────────────────────────────────────────┘
```

## MCP Client — Agents as MCP Consumers

In addition to acting as an MCP server, EDDI agents can also **consume external MCP servers** as tool providers. This enables agents to call tools exposed by other MCP-compatible services during conversations.

### Configuration

External MCP servers are configured as **`mcpcalls` workflow extensions** — a first-class, versioned configuration resource (the MCP equivalent of `httpcalls`). There is **no** inline MCP server array on the LLM task.

**Step 1 — create an `mcpcalls` configuration** (`POST /mcpcallsstore/mcpcalls`), one per MCP server:

```json
{
  "mcpServerUrl": "http://localhost:7070/mcp",
  "name": "eddi-docs",
  "transport": "http",
  "apiKey": "${vault:mcp-api-key}",
  "timeoutMs": 30000,
  "toolsWhitelist": ["read_docs", "list_docs"],
  "toolsBlacklist": []
}
```

| Field            | Type      | Required | Default  | Description                                                                                                                                                                    |
| ---------------- | --------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `mcpServerUrl`   | string    | **Yes**  | —        | MCP server URL                                                                                                                                                                 |
| `name`           | string    | No       | —        | Human-readable name for logging                                                                                                                                                |
| `transport`      | string    | No       | `"http"` | Only Streamable HTTP is implemented; an unimplemented value is rejected as invalid configuration rather than silently substituted                                              |
| `apiKey`         | string    | No       | —        | API key, sent as `Authorization: Bearer <key>`. Resolved through global variables and `${vault:key}` references, or `${caller:token}` to call as the chatting user (see below) |
| `timeoutMs`      | long      | No       | `30000`  | Connection and request timeout in milliseconds                                                                                                                                 |
| `toolsWhitelist` | string\[] | No       | —        | If non-empty, only these tool names are exposed (names as returned by the server's `tools/list`)                                                                               |
| `toolsBlacklist` | string\[] | No       | —        | Tool names to exclude. Applied *after* the whitelist                                                                                                                           |
| `mcpCalls`       | object\[] | No       | —        | Deterministic, action-triggered tool bindings (see *Pipeline mode* below). Omit for agent-mode-only servers                                                                    |

#### How `create_api_agent` builds a write tool's body

A generated `POST`/`PUT`/`PATCH` tool takes the **entire request body as a single `requestBody` parameter**, whose description names the schema's properties, their types, and which are required. The model writes the JSON itself.

It is worth knowing why, because the obvious alternative is worse. Decomposing the schema into one parameter per property means every one becomes *required* (an `ApiCall`'s parameter map has nowhere to record optionality), so a `PATCH` of one field forces the model to restate all the others and a partial update silently becomes a full overwrite. It also substitutes model-written values into JSON unescaped, so a value containing a quote can break the body or add fields the schema never declared.

The whole-body form matters most under [HITL approval](https://github.com/labsai/EDDI/tree/main/docs/hitl.md): the approval card shows tool **arguments**, so "what the approver sees is what gets sent" only holds while the body is one of them.

#### Calling an MCP server as the chatting user

Set `apiKey` to `${caller:token}` and the tool call carries the identity of the person chatting, instead of a standing service credential:

```json
{ "mcpServerUrl": "https://eddi.example/mcp", "apiKey": "${caller:token}" }
```

The same guarantees apply as for API call headers — same origin only, fails closed rather than sending a placeholder, never persisted. See [`httpcalls.md`](/agent-configuration/httpcalls#calling-as-the-signed-in-user).

Two behaviours worth knowing, because they are deliberate:

* **Only tool calls carry the caller.** The `initialize` handshake and `tools/list` are sent unauthenticated, because the client is cached: a session opened with one user's token would be reused by everyone after them, and a tool list reflecting one user's permissions would be offered to the next. If your server requires authentication to *list* tools, use a static key.
* **Clients are cached per credential, not per URL.** Two agents pointing at the same server with different keys get separate clients. A caller-bound config still yields one shared client — the credential is applied per request, so there is no client per user.

A `${caller:token}` key with `eddi.caller-identity.enabled=false` is rejected as invalid configuration when the server is validated, rather than failing on every tool call.

**Step 2 — add an `mcpcalls` step to the agent's workflow**, before the LLM step:

```json
{
  "workflowSteps": [
    { "type": "eddi://ai.labs.parser",   "config": { "uri": "eddi://ai.labs.parser/parserstore/parsers/<id>?version=1" } },
    { "type": "eddi://ai.labs.behavior", "config": { "uri": "eddi://ai.labs.rules/rulestore/rulesets/<id>?version=1" } },
    { "type": "eddi://ai.labs.mcpcalls", "config": { "uri": "eddi://ai.labs.mcpcalls/mcpcallsstore/mcpcalls/<id>?version=1" } },
    { "type": "eddi://ai.labs.llm",      "config": { "uri": "eddi://ai.labs.llm/llmstore/llms/<id>?version=1" } }
  ]
}
```

A workflow may contain any number of `mcpcalls` steps — one per MCP server.

### Two Modes, One Configuration

* **Agent mode** — `AgentOrchestrator.discoverMcpCallTools()` traverses the agent → workflow → every `mcpcalls` step at execution time, connects to each server, applies that config's whitelist/blacklist, and hands the surviving tools to the LLM. The LLM calls them reactively. Controlled by `enableMcpCallTools` on the LLM task (`langchain.json`), **default `true`** — no per-server opt-in is needed:

  ```json
  { "tasks": [ { "type": "anthropic", "enableMcpCallTools": false } ] }
  ```
* **Pipeline mode** — `McpCallsTask` (`eddi://ai.labs.mcpcalls`, pipeline position `Parser → Rules → HttpCalls → McpCalls → LLM → Output`) matches behavior-rule actions against `mcpCalls[].actions` and invokes the named tool deterministically, with **no LLM involved**. Only active when `mcpCalls` is non-empty.

Both modes read the same `mcpcalls` configuration; they are not mutually exclusive.

### Using `setup_agent` with MCP Servers

```
setup_agent(
  agentName: "My Agent",
  systemPrompt: "You are helpful",
  mcpServerUrls: "http://localhost:7070/mcp, https://tools.example.com/mcp",
  ...
)
```

The `mcpServerUrls` parameter accepts a comma-separated list of URLs. For each URL, `AgentSetupService` creates one `mcpcalls` configuration (`transport: "http"`, `timeoutMs: 30000`, no whitelist/blacklist, no `mcpCalls` bindings — i.e. agent-mode only) and inserts a matching `eddi://ai.labs.mcpcalls` step into the generated workflow ahead of the LLM step. Nothing is written inline into the LLM configuration.

### Architecture

```
┌──────────────┐     ┌──────────────────────┐
│  User sends  │────▶│       LlmTask         │
│   message    │     │  (EDDI pipeline)      │
└──────────────┘     └──────────┬────────────┘
                                │
                    ┌───────────┼───────────┐
                    ▼           ▼           ▼
           ┌──────────┐ ┌──────────┐ ┌──────────┐
           │ Built-in │ │  Custom  │ │   MCP    │
           │  Tools   │ │  Tools   │ │  Tools   │
           │(calc,dt) │ │(HttpCall)│ │(external)│
           └──────────┘ └──────────┘ └────┬─────┘
                                          │
                              ┌───────────┼───────────┐
                              ▼                       ▼
                    ┌──────────────┐       ┌──────────────┐
                    │ MCP Server 1 │       │ MCP Server 2 │
                    │ (EDDI docs)  │       │ (3rd party)  │
                    └──────────────┘       └──────────────┘
```

### Key Behaviors

* **Graceful degradation**: Failed MCP connections log warnings but never kill the pipeline
* **Connection caching**: `McpToolProviderManager` reuses connections across conversation turns
* **Budget/rate-limiting**: MCP tools are subject to the same `ToolExecutionService` controls as built-in tools
* **Vault references**: API keys support `${vault:key}` syntax via `SecretResolver`
* **Clean shutdown**: All MCP clients are closed on application shutdown via `@PreDestroy`


# A2A Protocol

> **Status:** Available since EDDI v6.0.0\
> **Spec:** [Google A2A Protocol](https://github.com/google/A2A)

EDDI implements the Agent-to-Agent (A2A) protocol for distributed peer-to-peer agent communication. Agents can **expose** their capabilities via Agent Cards, and **consume** remote A2A agents as tools.

***

## Server — Exposing Agents via A2A

### Enable A2A for an Agent

Add A2A fields to your agent configuration:

```json
{
  "a2aEnabled": true,
  "description": "Customer support agent specializing in order tracking",
  "a2aSkills": ["order-tracking", "refund-processing"],
  "workflows": ["eddi://ai.labs.workflow/workflowstore/workflows/..."]
}
```

| Field         | Type      | Default                          | Description                                   |
| ------------- | --------- | -------------------------------- | --------------------------------------------- |
| `a2aEnabled`  | boolean   | `false`                          | Opt-in flag for A2A discovery                 |
| `description` | string    | `"EDDI conversational AI agent"` | Human-readable description for the Agent Card |
| `a2aSkills`   | string\[] | `["chat"]`                       | Skills advertised in the Agent Card           |

### Endpoints

| Method | Path                               | Description                                  |
| ------ | ---------------------------------- | -------------------------------------------- |
| `GET`  | `/.well-known/agent.json`          | Default Agent Card (first A2A-enabled agent) |
| `GET`  | `/a2a/agents/{agentId}/agent.json` | Per-agent Agent Card                         |
| `GET`  | `/a2a/agents`                      | List all A2A-enabled agents                  |
| `POST` | `/a2a/agents/{agentId}`            | JSON-RPC 2.0 endpoint                        |

### JSON-RPC Methods

| Method         | Description                                   |
| -------------- | --------------------------------------------- |
| `tasks/send`   | Send a message and get a synchronous response |
| `tasks/get`    | Retrieve task status by task ID               |
| `tasks/cancel` | Cancel (end) a task's conversation            |

### Example: Send a Message

```json
{
  "jsonrpc": "2.0",
  "method": "tasks/send",
  "id": "req-1",
  "params": {
    "id": "task-1",
    "message": {
      "role": "user",
      "parts": [{ "type": "text", "text": "Track order #12345" }]
    }
  }
}
```

### Configuration Properties

| Property            | Default                 | Description                         |
| ------------------- | ----------------------- | ----------------------------------- |
| `eddi.a2a.enabled`  | `true`                  | Master toggle for all A2A endpoints |
| `eddi.a2a.base-url` | `http://localhost:7070` | Base URL used in Agent Card URLs    |

***

## Client — Consuming Remote A2A Agents as Tools

Configure remote A2A agents in your LLM task configuration. They are discovered and merged into the tool-calling loop alongside built-in, MCP, and httpcall tools.

### LLM Task Configuration

```json
{
  "systemMessage": "You are an orchestrator agent...",
  "a2aAgents": [
    {
      "url": "https://remote-eddi.example.com/a2a/agents/support-agent",
      "name": "support-agent",
      "apiKey": "${vault:remote-agent-key}",
      "timeoutMs": 30000,
      "skillsFilter": ["order-tracking"]
    }
  ]
}
```

| Field          | Type      | Default         | Description                                                              |
| -------------- | --------- | --------------- | ------------------------------------------------------------------------ |
| `url`          | string    | *required*      | Base URL of the remote A2A agent                                         |
| `name`         | string    | from Agent Card | Display name (used in tool naming)                                       |
| `apiKey`       | string    | —               | **Must be a vault reference** (`${vault:...}`) to prevent secret leakage |
| `timeoutMs`    | long      | `30000`         | Timeout for A2A operations                                               |
| `skillsFilter` | string\[] | all skills      | Only expose specific skills (by id or name)                              |

> **⚠️ Security:** Always use vault references (`${vault:my-key}`) for API keys. Raw keys trigger a runtime warning and risk leakage in configuration exports. See [Secrets Vault](/security-and-compliance/secrets-vault).

### How It Works

1. **Discovery:** `A2AToolProviderManager` fetches the Agent Card from `{url}/agent.json`
2. **Mapping:** Each skill becomes a `ToolSpecification` with a `message` parameter
3. **Execution:** When the LLM calls the tool, a JSON-RPC `tasks/send` request is sent
4. **Caching:** Agent Cards are cached for 5 minutes to avoid redundant fetches

### Tool Naming

Tools are named `{agentName}_{skillId}`, sanitized to `[a-z0-9_]`. Example: `support_agent_order_tracking`.

***

## Architecture

```
┌─────────────────────────────────────────────────────────┐
│  EDDI Instance A (Server)                               │
│                                                         │
│  AgentConfiguration ─→ AgentCardService ─→ Agent Card   │
│  RestA2AEndpoint ←── JSON-RPC ←── A2ATaskHandler        │
│                                    ↓                    │
│                            ConversationService.say()    │
└─────────────────────────────────────────────────────────┘
          ▲                           │
          │  GET /agent.json          │  POST tasks/send
          │  (discovery)              │  (execution)
          │                           ▼
┌─────────────────────────────────────────────────────────┐
│  EDDI Instance B (Client)                               │
│                                                         │
│  LlmConfiguration.Task.a2aAgents[]                      │
│            ↓                                            │
│  A2AToolProviderManager ─→ ToolSpecification            │
│            ↓                                            │
│  AgentOrchestrator (merged with MCP + httpcall tools)   │
└─────────────────────────────────────────────────────────┘
```

## Key Files

| File                                           | Purpose                                       |
| ---------------------------------------------- | --------------------------------------------- |
| `engine/a2a/A2AModels.java`                    | Protocol records (Agent Card, JSON-RPC, Task) |
| `engine/a2a/AgentCardService.java`             | Generates Agent Cards from agent configs      |
| `engine/a2a/A2ATaskHandler.java`               | Bridges JSON-RPC to ConversationService       |
| `engine/a2a/RestA2AEndpoint.java`              | JAX-RS endpoints                              |
| `modules/llm/impl/A2AToolProviderManager.java` | Client-side discovery and tool execution      |
| `modules/llm/model/LlmConfiguration.java`      | `A2AAgentConfig` configuration model          |
| `configs/agents/model/AgentConfiguration.java` | `a2aEnabled`, `a2aSkills`, `description`      |


# Slack Integration

> **Status**: Production-ready · **Since**: v6.0.0

EDDI's Slack integration enables conversational AI agents — including multi-agent group discussions — to operate natively in Slack channels and direct messages. It supports 1:1 agent conversations, live-streamed panel discussions with multiple agents, trigger-keyword routing, and context-aware threaded follow-ups.

## Quick Setup

### 1. Create a Slack App

1. Go to [api.slack.com/apps](https://api.slack.com/apps) → **Create New App**
2. Choose **From a manifest** or **From scratch**

### 2. Configure OAuth & Permissions

Add these **Bot Token Scopes**:

| Scope               | Purpose                           |
| ------------------- | --------------------------------- |
| `chat:write`        | Post messages to channels and DMs |
| `app_mentions:read` | Respond to @mentions in channels  |
| `channels:read`     | Read channel metadata             |
| `channels:history`  | Read message events in channels   |
| `im:read`           | Read direct message metadata      |
| `im:history`        | Receive DM events                 |
| `im:write`          | Send DM responses                 |

### 3. Install to Workspace

1. Go to **Install App** → **Install to Workspace**
2. Copy the **Bot User OAuth Token** (starts with `xoxb-`)
3. Copy the **Signing Secret** from **Basic Information**

### 4. Store Credentials in Vault

Store your Slack credentials in EDDI's Secrets Vault:

```bash
curl -X POST http://localhost:7070/secretstore/keys \
  -H "Content-Type: application/json" \
  -d '{"keyName":"slack-bot-token","secretValue":"xoxb-your-token-here"}'

curl -X POST http://localhost:7070/secretstore/keys \
  -H "Content-Type: application/json" \
  -d '{"keyName":"slack-signing-secret","secretValue":"your-signing-secret"}'
```

### 5. Configure Channel Integration

There are two configuration methods. The **recommended** approach uses `ChannelIntegrationConfiguration` (new-style); the legacy `ChannelConnector` on agents is supported for backward compatibility.

#### Recommended: ChannelIntegrationConfiguration

Create a channel integration with trigger-keyword routing:

```bash
curl -X POST http://localhost:7070/channelstore/channels \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Main Slack Channel",
    "channelType": "slack",
    "platformConfig": {
      "channelId": "C0123ABCDEF",
      "botToken": "${vault:slack-bot-token}",
      "signingSecret": "${vault:slack-signing-secret}"
    },
    "defaultTargetName": "default",
    "targets": [
      {
        "name": "default",
        "type": "AGENT",
        "targetId": "your-agent-id",
        "triggers": []
      },
      {
        "name": "panel",
        "type": "GROUP",
        "targetId": "your-group-id",
        "triggers": ["panel", "group", "discuss"]
      },
      {
        "name": "debate",
        "type": "GROUP",
        "targetId": "your-debate-group-id",
        "triggers": ["debate"]
      }
    ]
  }'
```

With this configuration:

* `@EDDI hello` → routes to the default agent
* `@EDDI panel: Should we use microservices?` → triggers the group discussion
* `@EDDI debate: REST vs GraphQL` → triggers the debate group

#### Legacy: ChannelConnector on Agent

Add a `ChannelConnector` to your agent configuration:

```json
{
  "channels": [
    {
      "type": "slack",
      "config": {
        "channelId": "C0123ABCDEF",
        "botToken": "${vault:slack-bot-token}",
        "signingSecret": "${vault:slack-signing-secret}",
        "groupId": "optional-group-id-for-discussions"
      }
    }
  ]
}
```

> **Note**: When both a `ChannelIntegrationConfiguration` and a legacy `ChannelConnector` cover the same `channelId`, the new-style config always wins.

### 6. Enable Direct Messages (App Home)

For the bot to accept DMs, you must enable the Messages Tab:

1. Go to **App Home** → **Show Tabs**
2. Enable **Messages Tab** (toggle on)
3. ✅ Check **"Allow users to send Slash commands and messages from the messages tab"**

> ⚠️ If this checkbox is unchecked, users will see "Sending messages to this app has been turned off" and cannot DM the bot.

### 7. Enable Event Subscriptions in Slack

> ⚠️ **This step must come last.** When you set the Request URL, Slack immediately sends a signed `url_verification` challenge. EDDI verifies this using the signing secrets from step 4. If no agent is configured yet, verification fails and Slack rejects the URL.

1. Go to **Event Subscriptions** → Enable
2. Set the **Request URL** to: `https://<your-eddi-host>/integrations/slack/events`
3. Slack will verify the URL (you should see a green checkmark)
4. Subscribe to **Bot Events**:
   * `app_mention` — triggers when the bot is @mentioned in a channel
   * `message.im` — triggers on direct messages to the bot
   * `message.channels` — enables thread-reply continuity without @mention
5. Click **Save Changes**

***

## Architecture

```
Slack Workspace(s)                       EDDI Cluster
─────────────────                        ─────────────────────────
┌─────────────┐   Events API (HTTPS)    ┌─────────────────────────┐
│ Slack App    │ ───────────────────────→│ RestSlackWebhook        │
│ (per wksp)   │                         │   ├─ Try all secrets    │
└─────────────┘                         │   └─ Dedup events       │
                                        └───────────┬─────────────┘
                                                    │ async
                                        ┌───────────▼─────────────┐
                                        │ SlackEventHandler        │
                                        │   ├─ Route via triggers  │
                                        │   ├─ DM fallback         │
                                        │   └─ Per-channel token   │
                                        └───────────┬─────────────┘
                                                    │
                              ┌──────────────────────┼───────────────────┐
                              ▼                      ▼                   ▼
                    ┌─────────────────┐   ┌──────────────────┐  ┌───────────────┐
                    │ ConversationSvc │   │ GroupConvSvc     │  │ SlackWebAPI   │
                    │ (1:1 agent)     │   │ (multi-agent)    │  │ (post msgs)   │
                    └─────────────────┘   └──────────────────┘  └───────────────┘
```

### Key Components

| Component                      | Responsibility                                                                              |
| ------------------------------ | ------------------------------------------------------------------------------------------- |
| `RestSlackWebhook`             | JAX-RS endpoint, multi-secret signature verification, URL challenge, event dispatching      |
| `SlackSignatureVerifier`       | HMAC-SHA256 verification with multi-secret support and 5-minute replay protection           |
| `SlackEventHandler`            | Core event logic: DM/channel routing, trigger keywords, group triggers, follow-up detection |
| `ChannelTargetRouter`          | Maps Slack channels → agents/groups with trigger-keyword matching and credential resolution |
| `SlackGroupDiscussionListener` | Streams multi-agent discussions into Slack with header+thread UX                            |
| `SlackWebApiClient`            | HTTP client for `chat.postMessage` with Markdown→mrkdwn conversion                          |

### Credential Flow

```
ChannelIntegrationConfiguration
  ├─ platformConfig.botToken: "${vault:slack-bot-token}"
  └─ platformConfig.signingSecret: "${vault:slack-signing-secret}"
        │
        ▼
ChannelTargetRouter (60s cache refresh)
  ├─ SecretResolver resolves vault references
  ├─ channelType:channelId → resolved config + targets
  └─ allSigningSecrets set (for webhook verification)
        │
        ├──→ RestSlackWebhook: verify(signature, allSigningSecrets)
        └──→ SlackEventHandler: postMessage(resolvedBotToken, ...)
```

***

## Features

### 1:1 Agent Conversations

@mention the bot in a channel:

```
@EDDI What's our Q4 revenue forecast?
```

The bot responds in a thread under the user's message.

### Direct Messages (DMs)

Send a message directly to the bot — no @mention needed:

```
Hello, what can you do?
```

DMs are automatically routed to the default agent from any configured Slack integration. Since DM channel IDs are dynamic (unique per user-bot pair), they don't need explicit channel configuration — EDDI resolves to the first available Slack integration's default target.

> **Note**: DMs use `message.im` events (Slack does not fire `app_mention` in DMs). Make sure `message.im` is subscribed in your Slack app's event settings.

### Trigger Keywords

Use colon-delimited trigger keywords to route to specific targets:

```
@EDDI panel: Should we adopt microservices?     → routes to "panel" target
@EDDI debate: REST vs GraphQL                   → routes to "debate" target
@EDDI architect: Review this design              → routes to "architect" target
```

Triggers are case-insensitive. The text after the colon becomes the message sent to the target agent/group. Messages without a trigger keyword route to the default target.

Type `@EDDI help` to see available trigger keywords for the channel.

### Multi-Agent Group Discussions

When a trigger keyword routes to a GROUP target, a multi-agent panel discussion starts. All configured agents in the group participate in a live discussion streamed to Slack.

#### UX Pattern: Header + Thread

All discussion styles use the same UX pattern — **header at channel level, full content in thread**:

```
User: @EDDI panel: Should we rewrite in Rust?

🗣️ *round table discussion started* — 3 agents participating
> _Should we rewrite in Rust?_

🟢 *Backend Expert*
_Rust would give us memory safety and performance..._ (preview)
  └─ [full response in thread]
  └─ 💬 *Frontend Expert* → *Backend Expert*: I agree on safety, but... (peer feedback)

🟢 *Frontend Expert*
_From the frontend perspective, the tooling is still maturing..._
  └─ [full response in thread]
  └─ 🔄 *Frontend Expert (revised)*: After hearing feedback... (revision)

📋 *Panel Synthesis* (by Moderator)
_The panel recommends a hybrid approach..._ (preview)
  └─ [full synthesis in thread]
```

This pattern keeps the channel scannable while preserving full discussion detail in threads.

#### Discussion Styles in Slack

Each style produces a distinct phase flow, but all use the same header+thread UX:

| Style                | Phases                                                | Slack Behavior                                                                            |
| -------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| **ROUND TABLE**      | Opinion → Synthesis                                   | Each agent posts a channel header; moderator synthesizes                                  |
| **PEER REVIEW**      | Opinion → Critique → Revision → Synthesis             | Peer feedback threads under the target agent's header                                     |
| **DEVIL'S ADVOCATE** | Opinion → Challenge → Defense → Synthesis             | Challenger threads under the original agent's header                                      |
| **DEBATE**           | Pro Arguments → Con Arguments → Rebuttals → Judge     | PRO and CON agents post separate headers; rebuttals thread under opponents                |
| **DELPHI**           | Anonymous Round 1 → Round 2 (convergence) → Synthesis | Each round's opinions post as headers; convergence visible across rounds                  |
| **TASK FORCE**       | Plan → Execute → Verify → Synthesis                   | Moderator posts plan; agents post task results; verifiers thread under targets; synthesis |

#### TASK\_FORCE Events in Slack

The `SlackGroupDiscussionListener` handles TASK\_FORCE-specific events:

| Event                               | Slack Rendering                                                                              |
| ----------------------------------- | -------------------------------------------------------------------------------------------- |
| `onTaskPlanCreated`                 | Posts "📝 *Task plan created*" (or "pre-configured") with numbered task list and assignments |
| `onSpeakerComplete` (EXECUTE phase) | Each agent's task result posts as a channel-level header + thread reply                      |
| `onTaskVerified`                    | Posts ✅/❌ with task subject, pass/fail status, and moderator feedback                        |
| `onGroupComplete`                   | Posts "📋 *Panel Synthesis*" with preview + full content in thread                           |

#### Peer Feedback Threading

In styles with agent-to-agent feedback (PEER\_REVIEW, DEVIL\_ADVOCATE, DEBATE), feedback is posted as a **thread reply under the target agent's channel header**. This creates a natural conversation flow:

```
🟢 *Alice*                         ← channel-level header
  └─ I believe we should...        ← full response (thread)
  └─ 💬 *Bob* → *Alice*: I disagree because...    ← peer feedback (thread)
  └─ 💬 *Carol* → *Alice*: I agree, and also...   ← peer feedback (thread)
  └─ 🔄 *Alice (revised)*: After hearing feedback...  ← revision (thread)
```

### Context-Aware Follow-ups

After a discussion, users can reply in an agent's thread to ask follow-up questions:

```
Alice's header: 🟢 *Alice*
  └─ [original contribution]
  └─ 💬 Bob → Alice: I disagree...
  └─ User: "Alice, can you address Bob's concerns?"
  └─ Alice: [responds with full context of the discussion + peer feedback]
```

The follow-up system:

1. Detects the thread reply is under an agent's message
2. Retrieves the agent's discussion context (contribution + feedback received)
3. Injects that context into the prompt
4. Routes to the correct agent for a contextual response

### Markdown Conversion

Agent responses often contain standard Markdown. The `SlackWebApiClient` automatically converts to Slack's `mrkdwn` format at the egress point:

| Markdown             | Slack mrkdwn                   |
| -------------------- | ------------------------------ |
| `**bold**`           | `*bold*`                       |
| `# Heading`          | `*Heading*` (bold)             |
| `~~strike~~`         | `~strike~`                     |
| `---`                | `───────────` (Unicode line)   |
| Tables (`\| col \|`) | Wrapped in ` ``` ` code blocks |
| Code blocks          | Preserved unchanged            |

***

## Enterprise & Clustering

### Multi-Workspace Support

Each `ChannelIntegrationConfiguration` can use different bot tokens and signing secrets, allowing a single EDDI instance to serve multiple Slack workspaces. The `ChannelTargetRouter` caches all credentials and the `SlackSignatureVerifier` tries all known signing secrets during webhook verification.

### Retry Logic

All Slack API calls use **exponential backoff** (3 attempts, 500ms/1s/2s base). Failed messages are logged but don't crash the event handler.

### Event Deduplication

Slack retries webhook deliveries on timeout. EDDI uses an in-memory cache (`ICache`) to deduplicate events by `event_id`, preventing duplicate processing.

### Follow-up Memory Management

Active group discussion contexts use EDDI's `ICache` infrastructure with **TTL-based expiration** (2 hours for group listeners, 10 minutes for event dedup). This prevents unbounded memory growth from long-lived discussions.

### Thread Safety

* `ChannelTargetRouter` uses volatile reference swaps with an `AtomicBoolean` refresh gate — no thundering herd on cache expiry
* Event processing runs on virtual threads — non-blocking, scales to thousands of concurrent events
* The `CountDownLatch` in `SlackGroupDiscussionListener` signals completion cleanly without polling

### Cluster Considerations

When running EDDI as a multi-instance cluster behind a load balancer:

1. **Webhook Delivery**: Slack sends each event to ONE URL. The load balancer routes to one EDDI instance. Event dedup is per-instance (ICache), which is fine — Slack only delivers to one endpoint.
2. **Conversation State**: Conversations are stored in MongoDB, so any instance can handle follow-up messages. The `IConversationService` load-balances naturally.
3. **Group Discussion Affinity**: A group discussion runs on the instance that received the trigger. Since the `SlackGroupDiscussionListener` streams directly to Slack API, this is instance-local and correct. Follow-up context is cached per-instance in ICache — if a follow-up routes to a different instance, it gracefully falls back to a standard conversation (no context injection, but no error).
4. **NATS Integration**: When `eddi.messaging.type=nats`, conversation processing is ordered via NATS JetStream subjects. The Slack webhook handler still handles event dispatch locally (Slack only talks to one instance), but conversation execution benefits from NATS-backed ordering, retry (3 attempts), and dead-letter queuing.

***

## Configuration Reference

### ChannelIntegrationConfiguration (Recommended)

```json
{
  "name": "Production Slack",
  "channelType": "slack",
  "platformConfig": {
    "channelId": "C0123ABCDEF",
    "botToken": "${vault:slack-bot-token}",
    "signingSecret": "${vault:slack-signing-secret}"
  },
  "defaultTargetName": "default",
  "targets": [
    {
      "name": "default",
      "type": "AGENT",
      "targetId": "agent-id",
      "triggers": []
    },
    {
      "name": "panel",
      "type": "GROUP",
      "targetId": "group-id",
      "triggers": ["panel", "group"]
    }
  ]
}
```

| Key                            | Required | Description                                                  |
| ------------------------------ | -------- | ------------------------------------------------------------ |
| `channelType`                  | ✅        | Must be `"slack"`                                            |
| `platformConfig.channelId`     | ✅        | Slack channel ID (e.g., `C0123ABCDEF`)                       |
| `platformConfig.botToken`      | ✅        | Bot User OAuth Token. Use vault reference.                   |
| `platformConfig.signingSecret` | ✅        | Slack Signing Secret. Use vault reference.                   |
| `defaultTargetName`            | ✅        | Name of the target used when no trigger keyword matches      |
| `targets[].name`               | ✅        | Target name (must match `defaultTargetName` for the default) |
| `targets[].type`               | ✅        | `AGENT` or `GROUP`                                           |
| `targets[].targetId`           | ✅        | Agent ID or Group Config ID                                  |
| `targets[].triggers`           | ❌        | List of trigger keywords (case-insensitive)                  |

### Legacy ChannelConnector (on Agent)

```json
{
  "type": "slack",
  "config": {
    "channelId": "C0123ABCDEF",
    "botToken": "${vault:slack-bot-token}",
    "signingSecret": "${vault:slack-signing-secret}",
    "groupId": "optional-group-id"
  }
}
```

***

## Retry & Error Handling

### Retry Policy

All outgoing Slack API calls (`chat.postMessage`) use exponential backoff:

| Attempt | Backoff         | Cumulative Wait |
| ------- | --------------- | --------------- |
| 1       | 0ms (immediate) | 0ms             |
| 2       | 500ms           | 500ms           |
| 3       | 1000ms          | 1500ms          |

Only **retryable failures** trigger retry:

* HTTP 429 (Rate Limited)
* HTTP 500, 502, 503, 504 (Server Error)
* Network errors (connection refused, timeout, DNS failure)

**Non-retryable failures** (HTTP 200 + `ok:false`) are logged and skipped:

* `channel_not_found` — bot not in channel
* `invalid_auth` — bad token
* `not_in_channel` — bot not invited

### What Happens After Retry Exhaustion

After 3 failed attempts, the message is **permanently lost from the user's perspective**. The system:

1. Logs a structured error for operator alerting:

   ```
   SLACK_DELIVERY_FAILED | channel=C0123 | threadTs=12345.000 | textLength=450 | attempts=3 | error=...
   ```
2. The agent's response **still exists in conversation memory** (MongoDB). Operators can manually retrieve it via the conversation API.
3. The user sees no response in Slack — they can try sending the message again.

**Recommended monitoring**: Set up a log alert for `SLACK_DELIVERY_FAILED` in your observability stack (Grafana, Datadog, etc.) to catch delivery failures.

### Group Discussion Resilience

During a multi-agent group discussion, individual Slack post failures do **not** abort the discussion. The `SlackGroupDiscussionListener` uses a fire-and-forget wrapper (`postSafe`) that catches delivery exceptions and continues. Users may see a missing agent contribution, but the discussion completes and synthesis is delivered.

***

## Troubleshooting

### Bot doesn't respond to @mentions

| Check                      | Fix                                                                       |
| -------------------------- | ------------------------------------------------------------------------- |
| Integration configured?    | Create a `ChannelIntegrationConfiguration` with the channel's `channelId` |
| Bot token configured?      | `platformConfig.botToken` should reference a vault key                    |
| Bot in channel?            | Invite the bot to the channel in Slack                                    |
| Event subscription active? | Check **Event Subscriptions** in Slack app settings                       |
| Request URL verified?      | Slack must have verified `https://<host>/integrations/slack/events`       |
| Signing secret set?        | Without a signing secret, webhook verification fails (HTTP 403)           |

### Bot doesn't respond to DMs

| Check                                   | Fix                                                                                     |
| --------------------------------------- | --------------------------------------------------------------------------------------- |
| "Sending messages has been turned off"? | **App Home** → Messages Tab → ✅ check "Allow users to send Slash commands and messages" |
| `message.im` subscribed?                | Add `message.im` to Bot Events in Slack app settings                                    |
| `im:history` scope?                     | Add `im:history` to Bot Token Scopes and reinstall the app                              |
| `im:write` scope?                       | Add `im:write` to Bot Token Scopes and reinstall the app                                |
| Any Slack integration configured?       | DMs fall back to the first available Slack integration's default target                 |

### Signature verification fails (HTTP 403)

| Check                         | Fix                                                                            |
| ----------------------------- | ------------------------------------------------------------------------------ |
| Signing secret correct?       | Copy from **Basic Information** in Slack app settings, store in vault          |
| Clock drift?                  | Timestamp validation uses 5-minute window — sync clocks                        |
| Reverse proxy stripping body? | The raw body must reach EDDI unchanged for HMAC verification                   |
| No agents configured?         | At least one deployed agent must have a Slack integration with `signingSecret` |

### Messages appear duplicated

Slack retries events up to 3 times if it doesn't receive HTTP 200 within 3 seconds. EDDI deduplicates by `event_id` using an in-memory cache (TTL: 10 minutes). If you see duplicates:

* Check EDDI response time — if pipeline processing blocks the webhook endpoint, Slack will retry
* The webhook endpoint responds immediately (async processing) — if you see slow responses, check network/proxy latency

### Group discussion times out

The `registerAgentThreadMappings` task waits up to 300 seconds. If the group discussion takes longer:

* Check agent LLM response times
* Consider using fewer agents or simpler discussion styles

***

## Building Custom Channel Integrations

This section is a guide for developers building integrations for other platforms (Teams, Discord, Telegram, etc.) based on lessons learned from the Slack implementation.

### Architecture Pattern

Every channel integration follows the same layered pattern:

```
┌─────────────────────┐
│  REST Webhook        │  ← Platform-specific webhook endpoint
│  (RestSlackWebhook)  │     Verify signatures, respond fast, dispatch async
└──────────┬──────────┘
           │
┌──────────▼──────────┐
│  Event Handler       │  ← Core routing logic
│  (SlackEventHandler) │     Dedup, route to agent/group, manage conversations
└──────────┬──────────┘
           │
┌──────────▼──────────┐
│  Channel Router      │  ← Map platform IDs → EDDI agents + credentials
│  (ChannelTargetRouter)│     Trigger-keyword matching, vault-backed secrets
└──────────┬──────────┘
           │
┌──────────▼──────────┐
│  API Client          │  ← Platform's outgoing API (send messages)
│  (SlackWebApiClient) │     Retryable exceptions, Markdown→mrkdwn conversion
└──────────────────────┘
```

### Key Lessons from the Slack Implementation

| Lesson                                         | Why                                                                                                                     |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Never catch-and-swallow in the API client**  | The retry wrapper in the handler needs to see failures. Throw for retryable, return null for non-retryable.             |
| **Always use TTL caches, not just size-based** | Size-based caches keep stale entries indefinitely in low-traffic systems. Use `ICacheFactory.getCache(name, Duration)`. |
| **Use Jackson, not string manipulation**       | Manual JSON escaping misses control characters. Manual JSON parsing is fragile. Jackson handles both correctly.         |
| **Gate cache refresh with AtomicBoolean**      | Under load, many threads hit the refresh simultaneously. CAS-gate ensures only one thread refreshes.                    |
| **Use CountDownLatch, not polling**            | Polling wastes CPU and has latency. CountDownLatch signals instantly.                                                   |
| **Fire-and-forget in listeners**               | A failed Slack post should not crash the entire multi-agent discussion. Wrap in try/catch.                              |
| **Structured exhaustion logs**                 | After retry exhaustion, log enough context (channel, thread, text length, error) for operator recovery.                 |
| **Never leak internal IDs to users**           | Error messages should be generic. Log the details server-side.                                                          |
| **All credentials in config**                  | Per-channel credentials via vault references. No server-level secrets.                                                  |
| **Convert formatting at the egress point**     | Markdown→mrkdwn conversion in the API client ensures consistent rendering across all code paths.                        |


# Security

**Version: 6.2.0**

This document describes the security measures applied to EDDI's AI Agent Tooling system, particularly for tools that execute in response to LLM-generated arguments, as well as the Keycloak-based authentication layer.

***

## Authentication — Keycloak OIDC

**Version: ≥6.0.0**

EDDI supports optional authentication via [Keycloak](https://www.keycloak.org/) using the Quarkus OIDC extension. Authentication is **disabled by default** — the system runs open (no login required) unless explicitly enabled.

### Architecture

EDDI uses **bearer-only (service) mode** — the backend never redirects to Keycloak. The Manager SPA and Chat UI handle login via `keycloak-js`, then send Bearer tokens to the backend for validation.

```
Browser (EDDI Manager / Chat UI)
    │
    ├── keycloak-js → Keycloak login → JWT access token
    │
    ├── Authorization: Bearer <token> → EDDI backend
    │                                      │
    │                                      ├── Quarkus OIDC validates token via JWKS
    │                                      ├── SecurityIdentity populated
    │                                      └── RestAgentManagement checks identity
    │
    └── Token refresh (automatic, every 30s before expiry)
```

> **Note:** The backend runs with `application-type=service` (bearer-only). It does not handle authorization code flows or login redirects. All login UI is handled client-side.

### Quick Setup with Installer

The easiest way to enable auth is to use the installer:

```bash
# Linux / macOS
bash install.sh --with-auth

# PowerShell
.\install.ps1 -WithAuth
```

This starts Keycloak alongside EDDI with pre-configured realm, clients, and test users:

| User     | Password | Role   | Notes                                                   |
| -------- | -------- | ------ | ------------------------------------------------------- |
| `eddi`   | `eddi`   | admin  | Full access, forced password change on first login      |
| `viewer` | `viewer` | viewer | Read-only access, forced password change on first login |

### Configuration Properties

| Property                        | Type           | Default                             | Description                                     |
| ------------------------------- | -------------- | ----------------------------------- | ----------------------------------------------- |
| `quarkus.oidc.enabled`          | **Build-time** | `true`                              | Extension active — must be `true` at build time |
| `quarkus.oidc.tenant-enabled`   | **Runtime**    | `false`                             | Enables/disables auth enforcement               |
| `quarkus.oidc.auth-server-url`  | Runtime        | `http://localhost:8180/realms/eddi` | Keycloak realm URL                              |
| `quarkus.oidc.client-id`        | Runtime        | `eddi-backend`                      | OIDC client ID (bearer-only)                    |
| `quarkus.oidc.application-type` | Runtime        | `service`                           | Bearer-only mode (no login redirects)           |
| `authorization.enabled`         | Runtime        | `${quarkus.oidc.tenant-enabled}`    | Fine-grained `@RolesAllowed` authorization      |

> **Important:** `quarkus.oidc.enabled` is a **build-time** property — it cannot be changed at container start. The OIDC extension must always be active in the binary. Use `quarkus.oidc.tenant-enabled` (runtime) to toggle auth on/off via environment variables.

### Enabling Auth at Container Start

```bash
docker run -e QUARKUS_OIDC_TENANT_ENABLED=true \
           -e QUARKUS_OIDC_AUTH_SERVER_URL=http://keycloak:8080/realms/eddi \
           -e QUARKUS_OIDC_CLIENT_ID=eddi-backend \
           -e QUARKUS_OIDC_APPLICATION_TYPE=service \
           labsai/eddi:latest
```

### Auth Permissions

When OIDC is enabled, the following permission rules apply (see `application.properties`):

| Path Pattern                                          | Policy                                                                                   |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `/q/metrics/*`, `/q/health/*`                         | **Permit** — Infrastructure endpoints                                                    |
| `/`, `/manage`, `/manage/*`, `/chat`, `/chat/*`       | **Permit** — SPA entry points (the SPA loads and handles Keycloak login via keycloak-js) |
| `/agents/production/*`                                | **Permit** — Production conversation endpoints (public-facing)                           |
| `/scripts/*`, `/fonts/*`, `/css/*`, `/js/*`, `/img/*` | **Permit** — Static assets for Manager SPA                                               |
| `/*` (catch-all)                                      | **Authenticated** — All other API endpoints require a valid Bearer token                 |

### RestAgentManagement Gate

`RestAgentManagement.checkUserAuthIfApplicable()` enforces per-request auth:

```java
if (checkForUserAuthentication &&
        !production.equals(userConversation.getEnvironment()) &&
        identity.isAnonymous()) {
    throw new UnauthorizedException();
}
```

* When `quarkus.oidc.tenant-enabled=false` → `checkForUserAuthentication=false` → all requests pass
* When `quarkus.oidc.tenant-enabled=true` → only authenticated users can access production endpoints
* Requests to `/production/` environments always pass regardless of auth status

### Local Development Keycloak

The EDDI-Manager repo provides a docker-compose for local Keycloak:

```bash
docker compose -f docker-compose.keycloak.yml up
```

This starts Keycloak 26 on port 8180 with:

* **Realm**: `eddi`
* **Clients**: `eddi-manager` (SPA, public), `eddi-backend` (bearer-only)
* **Roles**: `admin`, `editor`, `viewer`
* **Test users**: `eddi`/`eddi` (admin), `viewer`/`viewer` (read-only)

***

## Threat Model

When an LLM is given access to tools, every argument it supplies must be treated as **untrusted input**. An attacker can craft prompts that cause the LLM to pass malicious arguments to tools — a class of attacks known as **prompt injection**. EDDI mitigates these risks at the tool-execution layer so that individual tools do not need to implement their own defences.

***

## Caller Identity Forwarding — `CallerIdentityResolver`

**Applies to:** apicall headers, and an MCP server's `apiKey`, that reference `${caller:token}` / `${caller:userId}`.

An agent that calls an API needs a credential. Baking a static one into the config is the wrong shape when the API is EDDI's own: an OIDC token expires within the hour, cannot be least-privilege, and attributes every action to a single synthetic principal. Instead, a header may reference the authenticated caller, and EDDI substitutes that user's own token while building the request.

```json
"headers": { "Authorization": "Bearer ${caller:token}" }
```

Forwarding a user's token is only safe under strict conditions, so resolution fails the call loudly rather than degrading quietly:

| Control                      | Behaviour                                                                                                                                                                                                                                                                                                                  |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Same-origin only**         | The token is released only when the outbound call targets the exact `scheme://host:port` the caller addressed. That origin comes from the inbound request, never from configuration — so an agent config naming a third-party host cannot exfiltrate the token, and no allow-list is required for this to hold by default. |
| **Headers only**             | A token reference in a query parameter, request body or request path is rejected — only a header is ever substituted. `${caller:userId}` is permitted in headers and query parameters.                                                                                                                                     |
| **Authenticated turns only** | The identity is captured from the request driving the turn. Scheduled jobs and triggers have no caller and cannot satisfy the reference.                                                                                                                                                                                   |
| **Fails closed**             | An unsatisfiable reference throws rather than resolving to an empty string, which would send `Bearer` and surface downstream as a confusing `401`.                                                                                                                                                                         |
| **Never persisted**          | Resolution happens while building the request; `scrubSensitiveHeaders` strips authorization headers before the request is written to conversation memory.                                                                                                                                                                  |

**Thread safety.** A conversation turn is built on the request thread but executed on pool threads, where request-scoped beans no longer resolve. The identity is captured while the request context is live and bound to the executing thread by `CallerIdentityContext`, always cleared in a `finally` — those threads are reused across conversations, so a leaked binding would be readable by the next caller's turn.

Set `eddi.caller-identity.enabled=false` to forbid the feature outright.

***

## SSRF Protection — `UrlValidationUtils`

**Applies to:** PDF Reader, Web Scraper, and any future tool that fetches remote resources.

Server-Side Request Forgery (SSRF) occurs when an attacker tricks a server-side application into making requests to internal services. EDDI prevents this with `UrlValidationUtils.validateUrl(url)`:

### Scheme Allowlist

Only `http` and `https` URLs are accepted. All other schemes are rejected:

| Blocked     | Example                       |
| ----------- | ----------------------------- |
| `file://`   | `file:///etc/passwd`          |
| `ftp://`    | `ftp://internal-server/data`  |
| `jar://`    | `jar:file:///app.jar!/secret` |
| `gopher://` | `gopher://127.0.0.1:25/...`   |

### Private / Internal IP Blocking

DNS resolution is performed and the resolved address is checked before any connection is made:

| Range            | Description                   |
| ---------------- | ----------------------------- |
| `127.0.0.0/8`    | Loopback addresses            |
| `10.0.0.0/8`     | Private network (Class A)     |
| `172.16.0.0/12`  | Private network (Class B)     |
| `192.168.0.0/16` | Private network (Class C)     |
| `169.254.0.0/16` | Link-local (AWS/GCP metadata) |
| `fd00::/8`       | IPv6 unique-local             |
| `fe80::/10`      | IPv6 link-local               |
| `::1`            | IPv6 loopback                 |

### Cloud Metadata Endpoint Blocking

Cloud provider metadata services are explicitly blocked by IP and hostname:

* `169.254.169.254` (AWS, GCP, Azure metadata)
* `metadata.google.internal` (GCP)

### Internal Hostname Blocking

Hostnames that indicate internal services are rejected:

* `localhost`
* Any hostname ending in `.local`
* Any hostname ending in `.internal`

### Usage

```java
import static ai.labs.eddi.modules.langchain.tools.UrlValidationUtils.validateUrl;

// In any tool method that accepts a URL:
validateUrl(url); // throws IllegalArgumentException if blocked
```

***

## Sandboxed Math Evaluation — `SafeMathParser`

**Applies to:** Calculator tool.

### Problem

The original implementation used Java's `ScriptEngine` (Nashorn/Rhino) to evaluate math expressions. A malicious expression could execute arbitrary JavaScript:

```
// DANGEROUS — would execute arbitrary code in old implementation:
java.lang.Runtime.getRuntime().exec('rm -rf /')
```

### Solution

The Calculator tool now uses `SafeMathParser`, a **recursive-descent parser** written in pure Java. It:

* Recognises only numeric literals, arithmetic operators (`+`, `-`, `*`, `/`, `%`, `^`), and parentheses
* Supports a fixed allowlist of math functions (`sqrt`, `pow`, `abs`, `sin`, `cos`, `log`, `exp`, etc.)
* Supports only two constants (`PI`, `E`)
* Has **no code execution capability** — unrecognised tokens cause an immediate parse error
* Requires no external dependencies (no Rhino/Nashorn/GraalJS)

### Allowed Grammar

```
expression → term (('+' | '-') term)*
term       → power (('*' | '/' | '%') power)*
power      → unary ('^' unary)*
unary      → ('-' | '+')? primary
primary    → NUMBER | FUNCTION '(' args ')' | '(' expression ')' | CONSTANT
```

### Supported Functions

`sqrt`, `pow`, `abs`, `ceil`, `floor`, `round`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `log`, `log10`, `exp`, `signum`/`sign`, `toRadians`, `toDegrees`, `cbrt`, `min`, `max`

***

## Tool Execution Pipeline

All tool invocations — both built-in and HTTP-call-based — are routed through `ToolExecutionService.executeToolWrapped()`. This ensures consistent security and operational controls:

```
Tool Call ──▶ Rate Limiter ──▶ Cache Check ──▶ Execute Tool ──▶ Cost Tracker ──▶ Result
```

### Rate Limiting

* **Algorithm:** Token-bucket per **dispatch name** — the `@Tool` method the model called
* **Configuration:** `enableRateLimiting` (default `true`), `defaultRateLimit` (default `100`), `toolRateLimits` (per-tool overrides)
* **Key resolution:** a `toolRateLimits` entry may be keyed on the dispatch name (`searchWeb`) or on the built-in slug (`websearch`, the same token as `builtInToolsWhitelist`). The dispatch name is checked first, then the slug, then `defaultRateLimit`
* **Bucket granularity:** a slug-keyed limit sets the value for every operation of that tool but each operation keeps its **own** bucket. `{"websearch": 30}` grants `searchWeb`, `searchNews` and `searchWikipedia` 30 calls/minute *each*. To bound a single operation, key it by dispatch name
* **Behaviour:** Requests exceeding the limit receive a "Rate limit exceeded" error message returned to the LLM, which can then retry or use a different approach

### Smart Caching

* **Key:** `scopeTag|toolName:arguments`. Arguments longer than 2048 characters are replaced by their SHA-256 hex digest to keep keys bounded; shorter arguments are inlined verbatim
* **Scope tag — this is a data-isolation boundary:**
  * `u:<first 32 hex chars of SHA-256(userId)>` for `user` scope (the default). The raw user id never appears in a key
  * `c:<conversationId>` for `conversation` scope
  * `g` for `global` scope
  * When `user` scope is in effect but no user id is available, the entry falls back to the narrower `c:` partition. If neither a user id nor a conversation id is available, **no tag can be derived and the cache is bypassed entirely** — nothing is read and nothing is stored. A placeholder is deliberately never substituted, because that would put every unattributable request back into one shared partition
* **Configuration:** `enableToolCaching` (default `true`), `toolCacheScopes` (per-tool overrides, keyed on the dispatch name or the built-in slug — dispatch name wins, same vocabulary as `toolRateLimits` and `toolPricing`), `defaultToolCacheScope` (task-level default, effectively `user`)
* **Unparseable tokens fail safe:** a `toolCacheScopes` value that does not parse resolves to `user` and is logged at WARN — never to `defaultToolCacheScope`, so a typo in an override that was written to *narrow* one tool cannot promote it onto a `global` partition
* **Behaviour:** A cached result is only ever served back inside its own partition. With the default `user` scope, one authenticated user's tool result is never returned to another. Set a tool to `global` only when its result depends purely on its arguments and never on who is asking — that is an explicit, per-tool opt-in to cross-user reuse
* **Expiry:** Each entry expires on its own per-tool TTL, measured from the write (`weather` 300s, `websearch` 1800s, `news` 600s, `calculator` 7 days, 300s for tools with no table entry — see `GET /llm/tools/cache/ttl/{toolName}`). The TTL is matched against the dispatch name first and the slug second, so `searchNews` gets the `news` entry rather than its tool's `websearch` entry. Size-based eviction (`tool-results` holds 10 000 entries) is the secondary bound. A stale or poisoned result cannot outlive its TTL

### Cost Tracking

* **Configuration:** `enableCostTracking` (default `true`), `toolPricing` (per-call price overrides), `maxBudgetPerConversation` (no default — unlimited), `enforceBudget` (default `false`, deployment fallback `eddi.tools.budget.enforce-by-default`)
* **Scope:** `maxBudgetPerConversation` bounds **tool** cost only. LLM token spend is governed separately and per run by the model cascade's `maxCostPerRun`; the two are not summed
* **Pricing:** default per-call prices are keyed on the built-in slug (`webscraper` $0.002, `websearch` $0.001, `pdfreader` $0.001, `weather` $0.0005; `calculator`/`datetime`/`dataformatter`/`textsummarizer` free). Everything else — http, mcp, a2a, dynamic — is $0.00 until priced via `toolPricing`, which accepts a slug or a dispatch name (dispatch name wins). Operator-supplied prices are clamped at 0.0, so a negative value cannot credit a conversation and make a ceiling unreachable
* **Eviction:** To prevent unbounded memory growth, the tracker caps per-conversation entries at 10 000 and evicts the oldest \~10% when the limit is reached
* **Behaviour:** Enforcement is **opt-in**. A configured `maxBudgetPerConversation` records cost but refuses nothing until `enforceBudget: true` (per task) or `eddi.tools.budget.enforce-by-default=true` (per deployment). Once enforced, the budget is checked *before* each call using `<=` — the call that crosses the ceiling completes and the next one returns `Error: Budget exceeded for conversation <id>` to the LLM. Enforcing by default was rejected because built-ins priced at $0.00 until the canonical-slug fix, so it would make those ceilings bind for the first time and abort tool calls on upgrade. The converse cost is real — http/MCP/A2A/dynamic tools dispatch under their configured name, so an agent with a tool called `websearch`/`webscraper`/`pdfreader` *was* being refused before this release — so every task carrying a ceiling without the flag is named once in a startup WARN rather than lapsing silently

### Configuration Example

```json
{
  "tasks": [
    {
      "actions": ["help"],
      "type": "openai",
      "enableBuiltInTools": true,
      "enableRateLimiting": true,
      "defaultRateLimit": 100,
      "toolRateLimits": { "websearch": 30, "weather": 50 },
      "enableToolCaching": true,
      "enableCostTracking": true,
      "toolPricing": { "websearch": 0.005 },
      "maxBudgetPerConversation": 5.0,
      "enforceBudget": true,
      "parameters": {
        "apiKey": "...",
        "modelName": "gpt-4o",
        "systemMessage": "You are a helpful assistant."
      }
    }
  ]
}
```

***

## Conversation Coordinator — Sequential Processing

The `ConversationCoordinator` ensures that messages for the same conversation are processed **sequentially**, preventing race conditions in conversation state. The `isEmpty()` → `offer()` → `submit()` sequence is wrapped in a `synchronized` block to prevent two concurrent requests from both being submitted to the thread pool simultaneously.

Different conversations are processed **concurrently** — only same-conversation messages are serialised.

***

## HTTP Call Content-Type Handling

The `HttpCallExecutor` uses strict equality (`equals`) rather than prefix matching (`startsWith`) when checking the `Content-Type` header against `application/json`. This prevents content types like `application/json-patch+json` from being incorrectly deserialised as standard JSON.

***

## Recommendations for New Tools

When adding a new tool to EDDI:

1. **Validate all URLs** with `UrlValidationUtils.validateUrl()` before making any outbound request
2. **Never use `ScriptEngine`** or any form of dynamic code evaluation
3. **Add `@Tool` annotations** with clear descriptions so the LLM understands the tool's purpose and constraints
4. **Write unit tests** that specifically verify rejection of malicious inputs (SSRF URLs, injection strings)
5. **Route execution through `ToolExecutionService`** to inherit rate limiting, caching, and cost tracking

***

## TLS Requirements

EDDI does not enforce TLS directly — it is designed to run behind a reverse proxy (nginx, Traefik, Caddy, cloud load balancer) that handles TLS termination.

**For regulated deployments (HIPAA, EU AI Act)**, all traffic to and from EDDI must be encrypted in transit. A compliance startup warning is logged if no TLS certificate is detected.

### Option 1: TLS at Reverse Proxy (Recommended)

Configure your reverse proxy to terminate TLS and forward traffic to EDDI on `localhost:7070`. This is the standard production pattern.

### Option 2: TLS Directly in Quarkus

```properties
quarkus.http.ssl.certificate.file=/path/to/cert.pem
quarkus.http.ssl.certificate.key-file=/path/to/key.pem
quarkus.http.ssl-port=8443
```

### Internal Traffic

If EDDI and its database run on the same host or within a private network, internal traffic may be unencrypted. However, HIPAA deployments should evaluate whether this meets their security requirements.

***

## Supply Chain & CI/CD Security

EDDI's CI/CD pipeline enforces multiple automated security gates before any code reaches production. All GitHub Actions are **SHA-pinned** to immutable commit hashes to prevent supply-chain attacks via tag hijacking.

### Security Scanning Pipeline

| Tool          | Type            | Scope                            | Mode                             | Override                  |
| ------------- | --------------- | -------------------------------- | -------------------------------- | ------------------------- |
| **CodeQL**    | SAST            | Java source code                 | Blocking (PR) + weekly deep scan | N/A                       |
| **Trivy**     | CVE scanning    | Filesystem deps + Docker image   | Blocking (CRITICAL/HIGH)         | `.trivyignore`            |
| **Gitleaks**  | Secret scanning | Full git history                 | Blocking                         | `.gitleaksignore`         |
| **ZAP**       | DAST            | Live API (OpenAPI spec)          | Report-only                      | `fail_action` in workflow |
| **CycloneDX** | SBOM            | Maven dependency tree            | Artifact generation              | N/A                       |
| **Jazzer**    | Fuzz testing    | PathNavigator, MatchingUtilities | JUnit integration                | N/A                       |

### Override Files

For audited false positives, EDDI provides override files at the repository root:

* **`.trivyignore`** — Suppress specific CVEs with mandatory justification comments
* **`.gitleaksignore`** — Suppress specific Gitleaks fingerprints with justification

Both files should be reviewed periodically to ensure suppressions remain valid.

### Fuzz Testing

Security-critical input parsers are tested with [Jazzer](https://github.com/CodeIntelligenceTesting/jazzer) coverage-guided fuzzing:

* **`PathNavigator`** — Safe path navigation (replaced OGNL). Fuzz targets: `getValue`, `setValue`, arithmetic paths
* **`MatchingUtilities`** — Condition evaluation for DynamicValueMatcher

In CI, fuzz tests run as standard JUnit regression tests. For deep coverage-guided fuzzing locally:

```bash
./mvnw test -Dtest=PathNavigatorFuzzTest \
  -Djazzer.instrument=ai.labs.eddi.utils.PathNavigator
```

### Docker Image Security

* Trivy scans the built Docker image for CRITICAL/HIGH CVEs **before** pushing to Docker Hub
* Red Hat Preflight checks verify container certification compliance (labels, licenses)
* Security headers are validated against the running container in the smoke test

***

## See Also

* [LangChain Integration](/agent-configuration/langchain) — Full agent configuration reference
* [Agent Father LangChain Tools Guide](/advanced-concepts/agent-father-langchain-tools-guide) — Guided tool setup
* [Architecture](/architecture-and-concepts/architecture) — EDDI's lifecycle pipeline and concurrency model
* [Metrics](/deployment-and-infrastructure/metrics) — Monitoring tool execution performance
* [HIPAA Compliance](/security-and-compliance/hipaa-compliance) — HIPAA deployment guide
* [EU AI Act Compliance](/security-and-compliance/eu-ai-act-compliance) — EU AI Act compliance
* [Compliance Data Flow](/security-and-compliance/compliance-data-flow) — Data flow diagram for auditors


# Secrets Vault

EDDI includes a built-in secrets vault for managing sensitive values like API keys, tokens, and passwords. Secrets are encrypted at rest, referenced via URI syntax, and automatically scrubbed from logs and API exports.

## Architecture

```
┌─────────────────┐     ┌──────────────┐     ┌───────────────────┐
│  Configuration   │────>│ SecretResolver│────>│  VaultSecretProv.  │
│  (JSON configs)  │     │  (resolves    │     │  (envelope crypto  │
│  ${vault:..} │     │   at runtime) │     │   + persistence)   │
└─────────────────┘     └──────────────┘     └───────────────────┘
                                                       │
                                              ┌────────▼────────┐
                                              │  EnvelopeCrypto  │
                                              │  (AES-256-GCM +  │
                                              │   PBKDF2 KEK)    │
                                              └─────────────────┘
```

### Core Components

| Component                              | Package            | Purpose                                                      |
| -------------------------------------- | ------------------ | ------------------------------------------------------------ |
| `SecretReference`                      | `secrets.model`    | Value object: `tenantId/keyName` URI parsing                 |
| `EnvelopeCrypto`                       | `secrets.crypto`   | AES-256-GCM encryption with envelope key wrapping            |
| `ISecretProvider`                      | `secrets`          | SPI for reading/writing encrypted secrets                    |
| `VaultSecretProvider`                  | `secrets.impl`     | Production implementation with envelope crypto + persistence |
| `SecretResolver`                       | `secrets`          | Resolves `${vault:...}` references to plaintext at runtime   |
| `IRestSecretStore` / `RestSecretStore` | `secrets.rest`     | JAX-RS endpoints for secret CRUD and key rotation            |
| `SecretScrubber`                       | `secrets.sanitize` | Removes `${vault:...}` references from export payloads       |
| `SecretRedactionFilter`                | `secrets.sanitize` | Regex-based log redaction for API keys, tokens, vault refs   |
| `ISecretPersistence`                   | `secrets.persist.` | DB abstraction (MongoDB default, PostgreSQL via profile)     |

## Secret References

Secrets are referenced in configuration JSON using the vault URI syntax:

**Short form** (uses `default` tenant):

```
${vault:keyName}
```

**Full form** (explicit tenant):

```
${vault:tenantId/keyName}
```

* **tenantId** — tenant namespace (e.g., `default`, `acme-corp`)
* **keyName** — the secret name (e.g., `openai-api-key`)

### Where Vault References Work

| Configuration Type                    | Fields Resolved                        |
| ------------------------------------- | -------------------------------------- |
| **HTTP Calls** (`httpcalls.json`)     | URL, headers, body, query parameters   |
| **LangChain** (`langchain.json`)      | `apiKey` and other model configuration |
| **Property Setter** (`property.json`) | Values with `scope: secret` auto-vault |

### Resolution Behavior

Vault references are resolved **at runtime** when the task executes, never stored as plaintext in conversation memory. The resolution flow:

1. Task reads configuration containing `${vault:...}` reference
2. `SecretResolver.resolveValue()` finds and replaces vault URIs
3. `VaultSecretProvider.resolve()` decrypts and returns the plaintext
4. Plaintext is used for the operation (e.g., HTTP call header)
5. **Plaintext is NOT stored in memory** — only the vault reference persists

**Caching:** Successfully resolved secrets are cached in a Caffeine cache (configurable TTL). Failed resolutions are **never cached**, ensuring newly created secrets resolve immediately without waiting for cache expiry.

## Encryption

### Envelope Encryption

EDDI uses **envelope encryption** — each tenant gets its own random Data Encryption Key (DEK), which is itself encrypted by a Key Encryption Key (KEK) derived from the master password.

```
Master Password → PBKDF2 (600,000 iterations) → KEK
                                                  │
Secret → tenant DEK → AES-256-GCM encrypt → ciphertext
                │
                └→ KEK wraps DEK → encrypted DEK
                        │
                        └→ stored: { encryptedDek, iv, ciphertext }
```

### Configuration

The vault requires a master key (KEK) to encrypt/decrypt secrets. If not set, the vault is **disabled** — all `${vault:...}` references pass through unresolved and a prominent warning is logged at startup.

#### Installer (Recommended)

The `install.sh` / `install.ps1` installer automatically generates a unique, cryptographically random vault master key during setup and stores it in `~/.eddi/.env`. No manual configuration is needed — the vault is **secure by default** for all installer-based deployments.

The installer offers two options during the "Security" wizard step:

1. **Auto-generate** (recommended) — creates a strong 32-character base64 key via `openssl rand`
2. **Custom passphrase** — enter your own passphrase (minimum 16 characters)

You can also provide a key non-interactively:

```bash
# Bash
bash install.sh --vault-key=your-strong-passphrase-here

# PowerShell
.\install.ps1 -VaultKey "your-strong-passphrase-here"
```

Re-running the installer preserves your existing key — it reads from `~/.eddi/.env` and never overwrites it.

#### Manual Configuration

For manual Docker Compose deployments or local development, set the master key using **one** of these methods (in priority order):

```bash
# 1. System property (highest priority)
./mvnw compile quarkus:dev -Deddi.vault.master-key=your-strong-passphrase

# 2. Environment variable (recommended for production)
export EDDI_VAULT_MASTER_KEY=your-strong-passphrase

# 3. .env file in project root (recommended for local dev — add to .gitignore!)
echo "EDDI_VAULT_MASTER_KEY=your-strong-passphrase" > .env

# 4. application.properties (dev profile only — safe to commit)
%dev.eddi.vault.master-key=dev-passphrase
```

Additional vault settings in `application.properties`:

```properties
# Cache for resolved secrets (avoids repeated decryption)
eddi.vault.cache-ttl-minutes=5
eddi.vault.cache-max-size=1000
```

> **⚠️ Important:** The vault master key encrypts all stored API keys. If the master key is lost, all encrypted secrets become **permanently unrecoverable**. Back up your `~/.eddi/.env` file.

## Secret Input (Agent Conversations)

Agents can request secret input from users (e.g., API keys during setup). The flow works end-to-end across backend, chat UI, and Manager.

### Backend: PropertySetterTask + Conversation Scrubbing

When a property has `scope: secret`:

1. **PropertySetterTask** detects `scope == secret` on the property instruction
2. The raw value is immediately stored in the vault via `ISecretProvider.store()`
3. A vault reference (`${vault:...}`) replaces the plaintext in memory
4. The raw `input:initial` entry is scrubbed from the conversation step

When the **client flags input as secret** (via the `secretInput` context key):

1. `Conversation.isSecretInputFlagged()` checks for `{"secretInput": {"type": "string", "value": "true"}}` in the context map
2. `storeUserInputInMemory()` replaces the display value with `<secret input>` in conversation output
3. The actual plaintext still flows through lifecycle data so `PropertySetterTask` can vault it
4. The conversation log and API responses show `<secret input>` — **plaintext is never persisted**

### Output InputField Directive

To signal the chat UI to show a password field, use the `inputField` output type in your output configuration:

```json
{
  "type": "inputField",
  "subType": "password",
  "text": "Please enter your API key:"
}
```

### Chat UI: Password Fields + Secret Mode

Both **eddi-chat-ui** and the **EDDI-Manager chat panel** support secret input:

**Backend-driven password fields:**

* When the backend response contains an `inputField` output item with `subType: "password"`, the chat UI replaces the normal text input with a masked `<input type="password">` field
* An **eye toggle** button allows the user to reveal/hide the value
* After submission, the input reverts to the normal text field

**Proactive secret mode (client-initiated):**

* A 🔒/🔓 toggle button on the chat input lets users mark any input as secret
* When toggled ON, the input becomes a password field with eye toggle
* The `secretInput` context flag is sent to the backend, triggering output scrubbing in `Conversation.java`

**Security measures:**

* Chat UI state for secret values is **ephemeral** — cleared on submit or dialog close
* No secret values are stored in browser `localStorage` or `sessionStorage`
* `autoComplete="new-password"` prevents browser caching

### Agent Father Example

The default Agent Father agent demonstrates vault integration during API key setup:

```json
// Output configuration — prompts with a password field
{
  "type": "inputField",
  "subType": "password",
  "text": "Please enter your API key:"
}

// Property setter — auto-vaults the input
{
  "name": "apiKey",
  "valueString": "{memory.current.input}",
  "scope": "secret"
}
```

The `scope: secret` instruction causes `PropertySetterTask` to store the API key in the vault and replace the memory value with a `${vault:...}` reference.

## Auto-Vaulting (Agent Setup)

When creating agents through the **Agent Father** wizard or the Setup API, API keys are **automatically stored in the vault**. You don't need to manually create vault entries.

### How It Works

1. User provides an API key during agent setup
2. `AgentSetupService.vaultApiKey()` stores the key in the vault
3. A vault reference (`${vault:setup.<agent-name>.<timestamp>.apiKey}`) is written to the LLM configuration
4. When the vault is enabled, the plaintext key is never persisted in MongoDB — only the vault reference is stored

### Collision Prevention

Each vault key includes an epoch-millisecond timestamp suffix. This prevents key collisions when two agents share the same name — each gets a unique vault entry.

### Graceful Degradation

When the vault is disabled (no `EDDI_VAULT_MASTER_KEY`), the setup service logs a warning and falls back to plaintext storage. This ensures the Agent Father wizard works in local development without requiring vault configuration.

> **Production recommendation:** Always set `EDDI_VAULT_MASTER_KEY` in production. The installer does this automatically.

***

## REST API

### Endpoints

All endpoints are under the base path `/secretstore/secrets`. All endpoints require the `eddi-admin` role.

| Method   | Path                     | Description                                            |
| -------- | ------------------------ | ------------------------------------------------------ |
| `PUT`    | `/{tenantId}/{keyName}`  | Store a secret (body = plaintext value)                |
| `DELETE` | `/{tenantId}/{keyName}`  | Delete a secret                                        |
| `GET`    | `/{tenantId}/{keyName}`  | Get secret **metadata only** (never returns plaintext) |
| `GET`    | `/{tenantId}`            | List all secrets for a tenant (metadata only)          |
| `GET`    | `/health`                | Vault health check (provider status)                   |
| `POST`   | `/{tenantId}/rotate-dek` | Rotate the tenant's Data Encryption Key                |
| `POST`   | `/admin/rotate-kek`      | Rotate the Master Key (KEK) — **TLS required**         |

> **⚠️ Important:** The `GET` endpoints return **metadata only** (`keyName`, `createdAt`, `lastAccessedAt`, `checksum`). Secret values are **write-only** — they can be stored and used by the engine but never retrieved via API.

### Response Examples

**`PUT /{tenantId}/{keyName}`** — returns the vault reference:

```json
{
  "reference": "${vault:apiKey}",
  "tenantId": "default",
  "keyName": "apiKey"
}
```

**`GET /{tenantId}`** — returns metadata list:

```json
[
  {
    "tenantId": "default",
    "keyName": "apiKey",
    "createdAt": "2026-03-15T10:30:00Z",
    "lastAccessedAt": "2026-03-16T14:00:00Z",
    "checksum": "a1b2c3d4..."
  }
]
```

**`GET /health`** — returns vault provider status:

```json
{
  "status": "UP",
  "provider": "VaultSecretProvider",
  "available": true
}
```

**`POST /{tenantId}/rotate-dek`** — rotates the tenant's DEK:

```json
{
  "tenantId": "default",
  "secretsReEncrypted": 5,
  "message": "DEK rotated successfully. 5 secrets re-encrypted."
}
```

**`POST /admin/rotate-kek`** — rotates the master key:

Request body:

```json
{
  "oldMasterKey": "current-master-key",
  "newMasterKey": "new-master-key-at-least-8-chars"
}
```

Response:

```json
{
  "deksReEncrypted": 3,
  "message": "KEK rotated successfully. 3 DEKs re-encrypted. IMPORTANT: Update the EDDI_VAULT_MASTER_KEY environment variable to the new key and restart."
}
```

> **⚠️ Warning:** The `rotate-kek` endpoint transmits master keys in the request body. Ensure TLS is enabled. After rotation, update `EDDI_VAULT_MASTER_KEY` and restart.

### Key Rotation

EDDI supports two levels of key rotation:

**DEK Rotation** (`POST /{tenantId}/rotate-dek`):

* Generates a new Data Encryption Key for the tenant
* Re-encrypts all secrets with the new DEK
* Does NOT require a restart
* Recommended: rotate periodically or after personnel changes

**KEK Rotation** (`POST /admin/rotate-kek`):

* Re-encrypts all tenant DEKs with a new master key
* Secret ciphertexts are NOT modified — only DEK wrappers change
* Requires an application restart with the new `EDDI_VAULT_MASTER_KEY` after rotation
* Both operations use a verify-then-commit pattern: all decryption is validated before any writes occur

### Input Validation

All path parameters (`tenantId`, `keyName`) are validated against `[a-zA-Z0-9._-]{1,128}` to prevent path traversal attacks.

## Observability

### Micrometer Metrics

The vault emits metrics under the `eddi.vault.*` namespace for Grafana/Prometheus monitoring:

#### SecretResolver Metrics

| Metric                      | Type    | Description                             |
| --------------------------- | ------- | --------------------------------------- |
| `eddi.vault.cache.hits`     | Counter | Number of cache hits                    |
| `eddi.vault.cache.misses`   | Counter | Number of cache misses                  |
| `eddi.vault.resolve.errors` | Counter | Resolution failures (not-found, errors) |
| `eddi.vault.resolve.time`   | Timer   | Duration of provider resolution calls   |

#### VaultSecretProvider Metrics

| Metric                        | Type    | Description                              |
| ----------------------------- | ------- | ---------------------------------------- |
| `eddi.vault.resolve.count`    | Counter | Total resolve operations                 |
| `eddi.vault.store.count`      | Counter | Total store operations                   |
| `eddi.vault.delete.count`     | Counter | Total delete operations                  |
| `eddi.vault.rotate.count`     | Counter | Total rotation operations (DEK + KEK)    |
| `eddi.vault.errors.count`     | Counter | Total error count (persistence + crypto) |
| `eddi.vault.resolve.duration` | Timer   | Duration of resolve operations           |
| `eddi.vault.store.duration`   | Timer   | Duration of store operations             |

## Manager — Secrets Admin Page

The EDDI Manager includes a dedicated **Secrets Admin** page at `/manage/secrets` for managing vault entries through the UI.

### Features

* **Namespace filtering** — select tenant ID to scope the view
* **Secrets table** — displays `keyName`, `createdAt`, `lastAccessedAt`, and `checksum` (truncated)
* **Add Secret** — dialog with masked password input (eye toggle, `autoComplete="new-password"`)
* **Delete Secret** — confirmation dialog before permanent deletion
* **Vault Health** — live status badge showing vault online/offline state

### Security

* `autoComplete="off"` on key name input prevents browser caching
* `autoComplete="new-password"` on value input prevents browser caching
* React state is cleared immediately on dialog close or submission
* Secret values are **never displayed** — the API only returns metadata

## Security Measures

### Log Redaction

`SecretRedactionFilter` applies pre-compiled regex patterns to all log messages:

| Pattern                       | Replacement           | Example                                  |
| ----------------------------- | --------------------- | ---------------------------------------- |
| OpenAI keys (`sk-...`)        | `sk-<REDACTED>`       | `sk-abc123...` → `sk-<REDACTED>`         |
| Anthropic keys (`sk-ant-...`) | `sk-ant-<REDACTED>`   | `sk-ant-api03-...` → `sk-ant-<REDACTED>` |
| Bearer tokens                 | `Bearer <REDACTED>`   | `Bearer eyJhb...` → `Bearer <REDACTED>`  |
| API key params                | `apikey=<REDACTED>`   | `apikey=secret123` → `apikey=<REDACTED>` |
| Vault references              | `${vault:<REDACTED>}` | `${vault:t/key}` → `${vault:<REDACTED>}` |

### Export Sanitization

`SecretScrubber` removes vault references from agent export (backup) payloads, replacing them with `<SECRET_REMOVED>`. This prevents secrets from leaking when agents are shared or exported.

### Memory Protection

* **HTTP headers**: Sensitive headers (`Authorization`, `X-Api-Key`, etc.) are scrubbed before storing HTTP request details in conversation memory
* **Property values**: Secret-scoped properties store only vault references, never plaintext
* **User input**: When `scope == secret`, the raw `input:initial` is removed from the conversation step

### Persistence Error Handling

Both MongoDB and PostgreSQL persistence implementations wrap all database exceptions in `PersistenceException` (unchecked). This ensures:

* Consistent error handling across database backends
* No silent failures — all persistence errors surface to the caller
* Clear error messages with context (tenant ID, key name, operation)

## Testing

\~100 tests across backend and frontend:

### Backend (\~80 tests)

| Test Class                    | Tests | Coverage                                                                                           |
| ----------------------------- | ----- | -------------------------------------------------------------------------------------------------- |
| `SecretVaultIntegrationTest`  | 22    | Full round-trip, negative caching, DEK/KEK rotation, metrics, exceptions                           |
| `VaultSecretProviderTest`     | 11    | Store, resolve, delete, metadata, list, unavailable states                                         |
| `SecretResolverTest`          | 10    | Single/multiple/nested resolution, caching, passthrough, auto-vault keys                           |
| `RestSecretStoreTest`         | 22    | All endpoints, validation, error codes, vault unavailable, rotation                                |
| `EnvelopeCryptoTest`          | 9     | Encrypt/decrypt, key rotation, wrong key, tampering, large payloads                                |
| `SecretRedactionFilterTest`   | 6     | All 5 regex patterns, null/empty, safe messages                                                    |
| `SecretScrubberTest`          | 4     | Nested object scrubbing, preservation of non-secret fields                                         |
| `SecretReferenceTest`         | 6+    | Parsing, equality, hash, invalid references                                                        |
| `ConversationSecretInputTest` | 5     | Secret context scrubbing, normal passthrough, false flag, empty context, output vs. lifecycle data |

### Frontend (17 tests)

| Test File                       | Tests | Coverage                                                                                                          |
| ------------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------- |
| `secrets.test.tsx` (Manager)    | 12    | Page render, tenant inputs, vault health, create dialog (password, autocomplete, eye toggle), delete confirmation |
| `chat-store.test.tsx` (Chat UI) | 5     | `SET_INPUT_FIELD`, `CLEAR_INPUT_FIELD`, `TOGGLE_SECRET_MODE`, `CLEAR_MESSAGES` reset, initial defaults            |


# Global Variables

EDDI includes a built-in global variable store for managing configuration values like default LLM model names, API base URLs, temperature settings, and feature flags. Variables are **scoped per tenant** — single-tenant deployments use `"default"` implicitly, while multi-tenant deployments can maintain separate variable sets per tenant. Unlike secrets, global variables are **not encrypted** and are **fully visible** in the UI and logs.

## Architecture

```
┌─────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│  Configuration   │────>│ GlobalVariable   │────>│  GlobalVariable  │
│  (JSON configs)  │     │ Resolver         │     │  Store           │
│  ${vars:..}   │     │ (regex + cache)  │     │  (MongoDB/PG)    │
└─────────────────┘     └──────────────────┘     └──────────────────┘
        │
        ▼
┌─────────────────┐
│  Template Layer  │
│  {vars.<key>}  │
│      (Qute)      │
└─────────────────┘
```

### Core Components

| Component                     | Package                   | Purpose                                                                              |
| ----------------------------- | ------------------------- | ------------------------------------------------------------------------------------ |
| `GlobalVariable`              | `configs.variables.model` | Record: `tenantId`, `key`, `value`, `description`, `exportable`                      |
| `IGlobalVariableStore`        | `configs.variables`       | Persistence interface (non-versioned, tenant-scoped key-value)                       |
| `GlobalVariableStore`         | `configs.variables.mongo` | MongoDB implementation (`globalvariables` collection, composite `_id: tenantId/key`) |
| `PostgresGlobalVariableStore` | `datastore.postgres`      | PostgreSQL implementation (`global_variables` table, PK: `(tenant_id, key)`)         |
| `GlobalVariableResolver`      | `configs.variables`       | Resolves `${vars:...}` references with Caffeine cache                                |
| `IRestGlobalVariableStore`    | `configs.variables.rest`  | JAX-RS REST interface                                                                |
| `RestGlobalVariableStore`     | `configs.variables.rest`  | REST implementation with key validation and cache invalidation                       |

## Two Access Syntaxes

Global variables are available through two complementary syntaxes:

### 1. Template Syntax: `{vars.<key>}`

Available in LLM task system prompts and other template contexts. Resolved by the Qute template engine at template processing time.

```
You are an AI assistant powered by {vars.default-model}.
Always respond at temperature {vars.default-temperature}.
```

### 2. Late-Binding Syntax: `${vars:<key>}` / `${vars:tenantId/<key>}`

Available **everywhere** — in LLM task parameters, HTTP call configurations, MCP/A2A settings, embedding configs, Slack configs, and even the `type` field that selects the LLM provider. Resolved by `GlobalVariableResolver` at runtime, after template processing but before vault secret resolution.

Supports two forms, mirroring the vault's pattern:

| Form           | Syntax                 | Behavior                                          |
| -------------- | ---------------------- | ------------------------------------------------- |
| **Short form** | `${vars:key}`          | Uses the context tenant (defaults to `"default"`) |
| **Full form**  | `${vars:tenantId/key}` | Uses the explicit tenant                          |

```json
{
  "type": "${vars:default-provider}",
  "parameters": {
    "model": "${vars:default-model}",
    "apiKey": "${vault:openai-api-key}",
    "baseUrl": "${vars:api-base-url}"
  }
}
```

Multi-tenant example:

```json
{
  "parameters": {
    "model": "${vars:tenant-a/default-model}"
  }
}
```

### When to Use Which

| Syntax          | Where It Works                             | When to Use                                        |
| --------------- | ------------------------------------------ | -------------------------------------------------- |
| `{vars.<key>}`  | System prompts, template-processed strings | Dynamic prompt content that changes per-deployment |
| `${vars:<key>}` | Everywhere (params, URLs, headers, type)   | Operational config that affects infrastructure     |

### Resolution Order

EDDI resolves configuration values in a strict three-step order:

```
1. Qute templates          →  {vars.x}, {snippets.x}, {properties.x}, etc.
2. Global variables         →  ${vars:x}   ← this feature
3. Vault secrets           →  ${vault:x}
```

This ordering is important: vault secrets can contain global variable references, and global variables can be composed with template expressions.

> **⚠️ Nesting is not supported.** `${vars:${vault:x}}` and `${vault:${vars:x}}` will NOT work. Each resolution layer operates independently on the fully-resolved output of the previous layer.

## Where References Work

`${vars:...}` references are resolved in these pipeline callsites:

| Configuration Type                | Fields Resolved                                      |
| --------------------------------- | ---------------------------------------------------- |
| **LLM Task** (`langchain.json`)   | `type` (provider selection), all template parameters |
| **Chat Model Registry**           | All model parameters before model creation           |
| **HTTP Calls** (`httpcalls.json`) | URL, request body, headers, query parameters         |
| **MCP Tool Providers**            | API keys, server URLs                                |
| **A2A Tool Providers**            | API keys, agent URLs                                 |
| **Embedding Model Factory**       | All embedding model parameters                       |
| **Embedding Store Factory**       | All embedding store parameters                       |
| **Slack Channel Router**          | Channel configuration values (bot tokens, etc.)      |

## REST API

All endpoints require the `eddi-admin` or `eddi-editor` role.

### Endpoints

| Method   | Path                                        | Description                             |
| -------- | ------------------------------------------- | --------------------------------------- |
| `GET`    | `/variablestore/variables/{tenantId}`       | List all variables for a tenant         |
| `GET`    | `/variablestore/variables/{tenantId}/{key}` | Get a single variable by tenant and key |
| `PUT`    | `/variablestore/variables/{tenantId}/{key}` | Create or update a variable             |
| `DELETE` | `/variablestore/variables/{tenantId}/{key}` | Delete a variable                       |

> **Single-tenant shortcut:** Use `default` as the tenantId for single-tenant deployments.

### ID Validation

Both `tenantId` and `key` must match the pattern `[a-zA-Z0-9_.\-]+`:

| Valid ✅           | Invalid ❌      |
| ----------------- | -------------- |
| `default`         | `has space`    |
| `tenant-a`        | `key=value`    |
| `api.base-url`    | `key/path`     |
| `feature_flag_v2` | `special!char` |

### Response Examples

**`GET /variablestore/variables/default`**:

```json
[
  {
    "tenantId": "default",
    "key": "default-model",
    "value": "gpt-4.1",
    "description": "The default LLM model used by all agents",
    "exportable": true
  },
  {
    "tenantId": "default",
    "key": "api.base-url",
    "value": "https://api.openai.com",
    "description": "OpenAI API base URL",
    "exportable": false
  }
]
```

**`PUT /variablestore/variables/default/default-model`** — request body:

```json
{
  "key": "default-model",
  "value": "gpt-4.1-mini",
  "description": "The default LLM model used by all agents",
  "exportable": true
}
```

**`GET /variablestore/variables/default/missing-key`** — returns `404 Not Found`.

## Caching

Global variables are cached in a Caffeine cache with a **2-minute TTL** (configurable). This means:

* Variables load once from the database, then serve from cache
* After creating/updating/deleting a variable via the REST API, changes take effect **immediately** (the REST endpoint invalidates the cache)
* If the database is updated directly (outside the REST API), changes appear within 2 minutes

### Configuration

```properties
# Cache TTL for global variables (default: 2 minutes)
eddi.variables.cache-ttl-minutes=2
```

### Downstream Cache Invalidation

When global variables change, downstream caches that were built with old variable values need to be evicted. The `GlobalVariableResolver` supports an **invalidation listener** pattern:

* **ChatModelRegistry** registers a listener that clears all cached model instances when variables change
* This ensures that if you change `${vars:default-model}` from `gpt-4.1` to `gpt-4.1-mini`, all agents pick up the new model on their next request

## Use Cases

### Fleet-Wide Model Switching

Set a default model for all agents:

```bash
# Set the variable (default tenant)
curl -X PUT http://localhost:7070/variablestore/variables/default/default-model \
  -H "Content-Type: application/json" \
  -d '{"key": "default-model", "value": "gpt-4.1", "description": "Fleet default"}'
```

Reference it in all agents' `langchain.json`:

```json
{
  "type": "${vars:default-provider}",
  "parameters": {
    "model": "${vars:default-model}"
  }
}
```

Switch all agents at once:

```bash
curl -X PUT http://localhost:7070/variablestore/variables/default/default-model \
  -H "Content-Type: application/json" \
  -d '{"key": "default-model", "value": "gpt-4.1-mini"}'
```

### Multi-Tenant Model Switching

Set a different default model per tenant:

```bash
# Tenant A uses GPT
curl -X PUT http://localhost:7070/variablestore/variables/tenant-a/default-model \
  -H "Content-Type: application/json" \
  -d '{"key": "default-model", "value": "gpt-4.1"}'

# Tenant B uses Claude
curl -X PUT http://localhost:7070/variablestore/variables/tenant-b/default-model \
  -H "Content-Type: application/json" \
  -d '{"key": "default-model", "value": "claude-sonnet-4-20250514"}'
```

Reference in agent config:

```json
{
  "parameters": {
    "model": "${vars:default-model}"
  }
}
```

### Environment-Specific API Endpoints

```json
{
  "key": "api-gateway-url",
  "value": "https://staging.api.example.com",
  "description": "API gateway URL (changes between staging/production)",
  "exportable": false
}
```

### Feature Flags

```json
{
  "key": "enable-rag",
  "value": "true",
  "description": "Toggle RAG context injection"
}
```

### System Prompt Injection

```
You are an AI assistant for {vars.company-name}.
Your default language is {vars.default-language}.
Current API version: {vars.api-version}.
```

## Comparison: Global Variables vs Secrets vs Properties vs Snippets

| Aspect              | Global Variables                            | Secrets Vault                                 | Properties                   | Snippets                 |
| ------------------- | ------------------------------------------- | --------------------------------------------- | ---------------------------- | ------------------------ |
| **Purpose**         | Operational config                          | Sensitive credentials                         | Per-user/conversation state  | Reusable prompt text     |
| **Scope**           | Per-tenant (all agents)                     | Per-tenant                                    | Per-user or per-conversation | Deployment-wide          |
| **Encryption**      | None                                        | AES-256-GCM                                   | None                         | None                     |
| **Visibility**      | Fully visible                               | Write-only                                    | Fully visible                | Fully visible            |
| **Template syntax** | `{vars.<key>}`                              | —                                             | `{properties.<key>}`         | `{snippets.<name>}`      |
| **Late-binding**    | `${vars:<key>}` or `${vars:tenantId/<key>}` | `${vault:<key>}` or `${vault:tenantId/<key>}` | —                            | —                        |
| **Versioned**       | No                                          | No                                            | No                           | Yes                      |
| **REST path**       | `/variablestore/variables/{tenantId}`       | `/secretstore/secrets/{tenantId}`             | via PropertySetter           | `/snippetstore/snippets` |
| **Caching**         | 2 min (per-tenant)                          | 5 min                                         | No cache                     | 5 min                    |
| **Export**          | Configurable (`exportable`)                 | Always scrubbed                               | Per-user                     | Included                 |

### Decision Guide

* **Need to store an API key?** → Use the **Secrets Vault** (`${vault:...}`)
* **Need to change the LLM model for all agents?** → Use a **Global Variable** (`${vars:...}`)
* **Need to remember a user's name across conversations?** → Use **Properties** with `scope: longTerm`
* **Need reusable system prompt instructions?** → Use **Prompt Snippets** (`{snippets.<name>}`)

## Testing

### Backend Tests

| Test Class                        | Tests | Coverage                                                                |
| --------------------------------- | ----- | ----------------------------------------------------------------------- |
| `GlobalVariableTest`              | 9     | Model record, tenant defaults, JSON serialization, equality             |
| `GlobalVariableResolverTest`      | 16    | Short/full form resolution, tenant caching, invalidation, edge cases    |
| `RestGlobalVariableStoreTest`     | 11    | CRUD, tenant+key validation, cache invalidation, patterns               |
| `GlobalVariableStoreTest`         | 9     | MongoDB adapter CRUD with mocked MongoCollection, tenant scoping        |
| `PostgresGlobalVariableStoreTest` | 14    | PostgreSQL adapter CRUD with mocked JDBC, tenant isolation, error paths |
| `GlobalVariableCrudIT`            | 8     | Full CRUD lifecycle against MongoDB (Testcontainers)                    |
| `PostgresGlobalVariableCrudIT`    | 8     | Full CRUD lifecycle against PostgreSQL (Testcontainers)                 |

### Integration Tests

The integration tests verify the complete REST API lifecycle:

1. List variables (initially empty)
2. Create a variable via PUT
3. Read the variable by key
4. Update the variable value
5. Verify the variable appears in the list
6. Verify 404 for non-existent keys
7. Verify validation rejects invalid key patterns
8. Delete the variable and verify removal

Tests run against both MongoDB and PostgreSQL via Quarkus DevServices (Testcontainers).


# Audit Ledger

> **Status:** Available since v6.0.0 **EU AI Act:** Articles 17/19 — Immutable Decision Traceability

The Audit Ledger provides a **write-once, append-only** trail of every lifecycle task execution. It captures what data each task read, what it produced, LLM-specific details (compiled prompts, model responses, token usage), tool calls, actions, costs, and timing — signed with HMAC-SHA256 for tamper detection.

## Overview

Every time a conversation turn is processed, each lifecycle task (parser, behavior rules, HTTP calls, LangChain, output, etc.) generates an audit entry. These entries are:

1. **Scrubbed** — secrets are redacted (API keys, bearer tokens, vault references)
2. **Signed** — HMAC-SHA256 computed over all fields for tamper detection
3. **Batched** — queued in-memory and flushed to the database every few seconds
4. **Immutable** — stored in a write-once collection with no update or delete operations

## Configuration

| Property                            | Default | Description                                                 |
| ----------------------------------- | ------- | ----------------------------------------------------------- |
| `eddi.audit.enabled`                | `true`  | Enable/disable the audit ledger                             |
| `eddi.audit.flush-interval-seconds` | `3`     | How often to flush queued entries to the database           |
| `EDDI_VAULT_MASTER_KEY`             | (none)  | Vault master key — also used to derive the HMAC signing key |

> **Note:** If `EDDI_VAULT_MASTER_KEY` is not set, audit entries are stored without HMAC integrity hashes. A warning is logged at startup.

## Audit Entry Structure

Each audit entry captures:

| Field            | Type    | Description                                                |
| ---------------- | ------- | ---------------------------------------------------------- |
| `id`             | UUID    | Auto-generated unique identifier                           |
| `conversationId` | String  | Conversation this entry belongs to                         |
| `agentId`        | String  | Agent identifier                                           |
| `agentVersion`   | Integer | Agent version                                              |
| `userId`         | String  | User identifier                                            |
| `environment`    | String  | Deployment environment (e.g., `production`)                |
| `stepIndex`      | int     | 0-based step position in the conversation                  |
| `taskId`         | String  | Lifecycle task ID (e.g., `ai.labs.parser`)                 |
| `taskType`       | String  | Task type (e.g., `expressions`, `langchain`)               |
| `taskIndex`      | int     | 0-based task position in the pipeline                      |
| `durationMs`     | long    | Task execution time in milliseconds                        |
| `input`          | Map     | Data read by the task (user input, actions)                |
| `output`         | Map     | Data written by the task (output text, tool results)       |
| `llmDetail`      | Map     | LLM-specific: compiled prompt, model response, token usage |
| `toolCalls`      | Map     | Tool execution: name, args, result, cost                   |
| `actions`        | List    | Actions emitted by this task                               |
| `cost`           | double  | Monetary cost of this step                                 |
| `timestamp`      | Instant | When the task completed                                    |
| `hmac`           | String  | HMAC-SHA256 integrity hash                                 |

## REST API

The audit ledger exposes a **read-only** REST API. No create, update, or delete endpoints exist.

### Get Audit Trail by Conversation

```
GET /auditstore/{conversationId}?skip=0&limit=100
```

Returns audit entries for a conversation, newest first.

### Get Audit Trail by Agent

```
GET /auditstore/agent/{agentId}?agentVersion=1&skip=0&limit=100
```

Returns audit entries for an agent. The `agentVersion` parameter is optional.

### Get Entry Count

```
GET /auditstore/{conversationId}/count
```

Returns the total number of audit entries for a conversation.

## HMAC Integrity

When the vault master key is configured, each audit entry is signed with HMAC-SHA256:

1. A **signing key** is derived from the vault master key using PBKDF2 with a distinct salt (`eddi-audit-hmac-v1`, 600K iterations). This makes the audit signing key cryptographically independent from the vault's KEK.
2. A **canonical string** is built from all entry fields (excluding the HMAC itself), with map keys sorted alphabetically for deterministic output. Nested maps and lists are canonicalized recursively.
3. The HMAC is computed and stored as `v2:<64 hex chars>`.

To verify an entry has not been tampered with, recompute the HMAC and compare it to the stored value.

### Canonical form versioning

The stored value carries the version of the canonical form it was computed over, and verification picks the canonicalizer from that tag:

| Stored value     | Canonical form | Written by                |
| ---------------- | -------------- | ------------------------- |
| `v2:<hex>`       | v2             | current                   |
| `<hex>` (no tag) | v1             | before delimiter escaping |

**v1** joined keys and values with `=`, `,`, `{}`, `[]` and `|` without escaping them, so the map-to-string mapping was not injective: `{"a": "x", "b": "y"}` and `{"a": "x,b=y"}` canonicalize to the same bytes and therefore share one valid HMAC — a tampered entry could verify as intact. That became reachable once `toolCalls` started carrying tool-trace `arguments`/`result` strings, which the model and the user write.

**v2** escapes every delimiter inside keys and scalars and type-tags every value (`s:` scalar, `m` map, `l` list, `n` null), so a string can never render like a nested structure.

Verification never falls back from v2 to v1 — that would hand the collision straight back — and pre-existing untagged rows keep verifying under v1, so an upgrade does not turn the historical ledger into a wall of "tampered".

## Secret Redaction

All string values in audit entries pass through the `SecretRedactionFilter` before storage. The following patterns are redacted:

* OpenAI API keys (`sk-...`)
* Anthropic API keys (`sk-ant-...`)
* Bearer tokens (JWTs and opaque tokens)
* Generic API key patterns (`apikey=...`, `token=...`, etc.)
* Vault references (`${vault:...}`)

Redaction is applied recursively to nested maps and lists.

## Failure Handling

If a database write fails, entries are **re-queued** for the next flush cycle. After 3 consecutive failures, entries are dropped and an error is logged. This prevents unbounded memory growth while maximizing data retention.

## Storage

### MongoDB (default)

* Collection: `audit_ledger`
* Indexes: `conversationId`, `(agentId, agentVersion)`, `timestamp` (descending)
* Operations: `insertOne`, `insertMany` only — no update or delete

### PostgreSQL

* Table: `audit_ledger` (auto-created on first use)
* Hybrid storage: indexed columns (conversation\_id, agent\_id, agent\_version, timestamp) + JSONB for variable data
* Activated with `@IfBuildProfile("postgres")`
* Same insert-only contract as MongoDB

## Architecture

```
LifecycleManager                 ConversationService
  |                                |
  | buildAuditEntry()              | setAuditCollector()
  | (per task completion)          | (enriches with environment)
  |                                |
  v                                v
IAuditEntryCollector ---------> AuditLedgerService
                                   |
                                   | 1. scrubSecrets()
                                   | 2. computeHmac()
                                   | 3. queue.offer()
                                   |
                                   v  (every N seconds)
                                IAuditStore.appendBatch()
                                   |
                          +--------+--------+
                          |                 |
                     AuditStore     PostgresAuditStore
                     (MongoDB)        (PostgreSQL)
```


# GDPR / CCPA Compliance

This guide helps EDDI operators handle data subject requests and meet GDPR/CCPA requirements. For an overview of data processing, see [PRIVACY.md](/security-and-compliance/privacy).

## Handling Data Subject Requests

### 1. Right to Erasure (GDPR Art. 17 / CCPA §1798.105)

When you receive an erasure request:

```bash
# Via REST API
curl -X DELETE https://your-eddi-instance/admin/gdpr/{userId} \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
```

The response includes per-store counts:

```json
{
  "userId": "user-123",
  "memoriesDeleted": 15,
  "conversationsDeleted": 8,
  "conversationMappingsDeleted": 3,
  "logsPseudonymized": 42,
  "auditEntriesPseudonymized": 156,
  "completedAt": "2026-04-02T15:30:00Z"
}
```

**Via MCP:** Use the `delete_user_data` tool with `confirmation="CONFIRM"`.

**What happens:**

1. User memories — **permanently deleted**
2. Conversation snapshots — **permanently deleted**
3. Managed conversation mappings — **permanently deleted**
4. Database logs — userId **pseudonymized** (SHA-256 hash)
5. Audit ledger — userId **pseudonymized** (SHA-256 hash)

### 2. Right of Access (GDPR Art. 15) / Data Portability (Art. 20) / Right to Know (CCPA §1798.100)

```bash
curl https://your-eddi-instance/admin/gdpr/{userId}/export \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
  -o user-data.json
```

The export includes all user data in a structured, machine-readable JSON format:

* All persistent user memories
* All conversation transcripts (with full chat history)
* All managed conversation mappings (intent→conversation bindings)
* All audit processing records (capped at 10,000 entries)

**Via MCP:** Use the `export_user_data` tool.

### 3. Right to Restriction of Processing (GDPR Art. 18 / LGPD Art. 18)

When a user disputes data accuracy or objects to processing, you can freeze their processing without deleting data:

```bash
# Restrict processing
curl -X POST https://your-eddi-instance/admin/gdpr/{userId}/restrict \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN"

# Check restriction status
curl https://your-eddi-instance/admin/gdpr/{userId}/restrict \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
# Returns: true/false

# Remove restriction
curl -X DELETE https://your-eddi-instance/admin/gdpr/{userId}/restrict \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
```

**What happens when restricted:**

* New conversation creation is blocked → returns **403 Forbidden**
* Message processing (`say`) is blocked → returns **403 Forbidden**
* Existing data is preserved (not deleted)
* Restriction status is stored as a user memory entry
* All restriction/unrestriction events are logged in the audit ledger

**Use cases:**

* User disputes accuracy of stored data (Art. 18(1)(a))
* Processing is unlawful but user requests restriction instead of erasure (Art. 18(1)(b))
* User objects to processing pending verification (Art. 21)

### 4. Response Timeline

| Regulation | Initial Deadline | Extension                          |
| ---------- | ---------------- | ---------------------------------- |
| **GDPR**   | 30 days          | Up to 90 days for complex requests |
| **CCPA**   | 45 days          | Up to 90 days with notification    |

EDDI's erasure and export operations complete in seconds — the timeline constraint is your internal DSAR process, not the technical execution.

## Retention Configuration

```properties
# Auto-delete ended conversations after N days (default: 365, -1 to disable)
eddi.conversations.deleteEndedConversationsOnceOlderThanDays=365

# Close idle conversations after N days (default: 90)
eddi.conversations.maximumLifeTimeOfIdleConversationsInDays=90

# User memories — delete entries older than N days (default: -1, disabled)
eddi.usermemories.deleteOlderThanDays=-1
```

**Per-category retention** allows different retention periods for:

* **Conversations** — 365 days (default)
* **User memories** — disabled by default (configure per-deployment)

**The audit ledger has no retention property — by design.** `IAuditStore` is an append-only contract that deliberately exposes no update or delete operation, so EDDI never time-expires audit entries. Erasure requests are satisfied by **pseudonymizing** the `userId` (`IAuditStore.pseudonymizeByUserId`), which the cascading-erasure path invokes — not by deleting entries. See [Audit Ledger Legal Basis](#audit-ledger-legal-basis) below for the legal basis. If your jurisdiction requires time-limited audit retention, implement it at the operational or database layer (archival job, partition drop, storage-level TTL); EDDI provides no application-level audit purge.

> **Operator note:** earlier releases shipped an `eddi.audit.retentionDays` property in `application.properties`. No code ever read it, so setting it was a silent no-op — no audit entry was ever deleted by it. The property has been removed; if your deployment sets `eddi.audit.retentionDays` (or `EDDI_AUDIT_RETENTIONDAYS`), drop it and use database-level archival instead.

**Data minimization (Art. 5(1)(e)):** Review the default retention periods and reduce them to the minimum necessary for your use case.

## Audit Ledger Legal Basis

The EDDI audit ledger is retained indefinitely under two legal bases:

1. **GDPR Art. 17(3)(e)** — Compliance with a legal obligation
2. **EU AI Act Articles 17/19** — Immutable decision traceability for AI systems

Upon erasure requests, userId fields in audit entries are pseudonymized (replaced with a SHA-256 hash). The audit data structure, timestamps, and decision records remain intact for regulatory compliance.

## Controller Checklist

As the data controller, you must:

* [ ] **Privacy Policy**: Document your use of EDDI and AI processing
* [ ] **Legal Basis**: Determine and document the legal basis for each processing activity (see PRIVACY.md for suggestions)
* [ ] **Consent**: Obtain consent before enabling conversational AI (if consent is your legal basis)
* [ ] **DPAs**: Establish Data Processing Agreements with:
  * Your EDDI hosting provider (if not self-hosted)
  * Each cloud LLM provider configured in your agents
* [ ] **User Notice**: Inform users that conversations are processed by AI
* [ ] **Retention**: Review default 365-day retention — adjust if needed
* [ ] **Memory Disclosure**: If using `enableMemoryTools`, inform users their interactions are remembered across sessions
* [ ] **Provider Selection**: Choose LLM providers that meet your data residency requirements (see provider table in PRIVACY.md)
* [ ] **Article 30 Register**: Document EDDI as a processing system in your records of processing activities
* [ ] **Erasure Process**: Document your internal process for handling DSARs using the GDPR API
* [ ] **Breach Response**: Prepare a breach notification plan (see [incident-response.md](/security-and-compliance/incident-response))
* [ ] **CCPA Disclosure**: If serving California consumers, document that EDDI does not sell personal information

## LLM Provider Data Flow

EDDI sends conversation content to the LLM provider configured per agent. You choose the provider via the Manager UI or configuration files.

| Provider      | Data Location        | Self-Hosted? |
| ------------- | -------------------- | ------------ |
| Ollama        | Your infrastructure  | ✅ Yes        |
| jlama         | Your infrastructure  | ✅ Yes        |
| Anthropic     | US/EU (varies)       | ❌ No         |
| OpenAI        | US                   | ❌ No         |
| Google Gemini | US/EU (varies)       | ❌ No         |
| Mistral       | EU (France)          | ❌ No         |
| Azure OpenAI  | Your Azure region    | Partially    |
| AWS Bedrock   | Your AWS region      | Partially    |
| Oracle GenAI  | Your OCI region      | Partially    |
| Hugging Face  | Varies by model host | Partially    |

**For maximum data sovereignty**, use Ollama or jlama with self-hosted models.

**What is sent to LLM providers:**

* The current user message
* Recent conversation history (windowed)
* Agent system prompt

**What is NOT sent:**

* User IDs or account metadata
* Data from other conversations or agents
* API keys (except the provider's own authentication key)

## CCPA-Specific Requirements

### Do Not Sell (§1798.120)

EDDI does **not sell personal information** and has no mechanism to do so. Document this in your CCPA privacy notice.

### Right to Know (§1798.100)

Use the export endpoint (`GET /admin/gdpr/{userId}/export`) to fulfill "right to know" requests. The response includes all categories of personal information collected.

### Right to Delete (§1798.105)

Use the erasure endpoint (`DELETE /admin/gdpr/{userId}`) to fulfill "right to delete" requests. The cascade covers all data stores.

## International Privacy Regulations

EDDI's GDPR/CCPA infrastructure covers the technical requirements of all major international privacy frameworks. Each regulation has a detailed feature mapping and deployer checklist in PRIVACY.md:

* [**PIPEDA**](/security-and-compliance/privacy#pipeda--canada) — Canada (10 Fair Information Principles)
* [**LGPD**](/security-and-compliance/privacy#lgpd--brazil) — Brazil (Art. 18 data subject rights)
* [**APPI**](/security-and-compliance/privacy#appi--japan) — Japan (2022 amendments, EU adequacy)
* [**POPIA**](/security-and-compliance/privacy#popia--south-africa) — South Africa (8 processing conditions)
* [**PDPA**](/security-and-compliance/privacy#pdpa--southeast-asia) — Singapore, Thailand & Malaysia
* [**PIPL**](/security-and-compliance/privacy#pipl--china) — China (data localization, cross-border transfer)

The same erasure and export endpoints (`DELETE /admin/gdpr/{userId}`, `GET /admin/gdpr/{userId}/export`) work for all jurisdictions. EDDI provides the technical layer; deployer checklists cover organizational measures (consent, officer appointments, breach notification).

## See Also

* [PRIVACY.md](/security-and-compliance/privacy) — Data processing overview and international regulations
* [hipaa-compliance.md](/security-and-compliance/hipaa-compliance) — HIPAA deployment guide
* [eu-ai-act-compliance.md](/security-and-compliance/eu-ai-act-compliance) — EU AI Act compliance
* [compliance-data-flow.md](/security-and-compliance/compliance-data-flow) — Data flow diagram for auditors
* [incident-response.md](/security-and-compliance/incident-response) — Breach response runbook
* [templates/baa-template.md](https://github.com/labsai/EDDI/tree/main/docs/templates/baa-template.md) — Business Associate Agreement template


# HIPAA Compliance

> **HIPAA applies when EDDI processes Protected Health Information (PHI)** — for example, when used by healthcare organizations, telehealth platforms, or health-related chatbots. Under HIPAA, EDDI acts as infrastructure used by a **Business Associate**.

This guide helps deployers configure EDDI for HIPAA-compliant operation. For general data processing documentation, see [PRIVACY.md](/security-and-compliance/privacy). For GDPR/CCPA operations, see [gdpr-compliance.md](/security-and-compliance/gdpr-compliance).

***

## HIPAA Readiness Overview

| HIPAA Safeguard                             | EDDI Feature                                                                            | Status                     |
| ------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------- |
| **Access Control** (§164.312(a))            | Keycloak OIDC + RBAC roles (`admin`, `editor`, `viewer`)                                | ✅ Built-in                 |
| **Audit Controls** (§164.312(b))            | HMAC-signed immutable audit ledger                                                      | ✅ Built-in                 |
| **Integrity Controls** (§164.312(c))        | HMAC tamper detection on all audit entries                                              | ✅ Built-in                 |
| **Person Authentication** (§164.312(d))     | Keycloak with JWT/OIDC, MFA-capable                                                     | ✅ Built-in                 |
| **Transmission Security** (§164.312(e))     | TLS — deployer configures                                                               | ⚠️ Deployer responsibility |
| **Encryption at Rest** (§164.312(a)(2)(iv)) | Database-level TDE — deployer configures                                                | ⚠️ Deployer responsibility |
| **Data Disposal** (§164.310(d)(2)(i))       | GDPR cascade delete (`DELETE /admin/gdpr/{userId}`)                                     | ✅ Built-in                 |
| **Incident Response** (§164.308(a)(6))      | Documented runbook ([incident-response.md](/security-and-compliance/incident-response)) | ✅ Built-in                 |
| **Secret Management**                       | AES-256-GCM Secrets Vault with envelope encryption                                      | ✅ Built-in                 |

***

## PHI Data Flow

```
End User (patient / healthcare worker)
    │
    │  HTTPS (TLS required)
    │
    ▼
┌─────────────────────────────────────────┐
│              EDDI Backend               │
│                                         │
│  ┌───────────┐    ┌──────────────────┐  │
│  │ Keycloak  │    │  Audit Ledger    │  │
│  │ (AuthN)   │    │  (HMAC-signed,   │  │
│  └───────────┘    │   write-once)    │  │
│                   └──────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │  Conversation Pipeline            │  │
│  │  Input → Parse → Rules → LLM →   │  │
│  │  Output                           │  │
│  └───────────────────────────────────┘  │
│         │                    │          │
│    ┌────▼────┐         ┌────▼────┐     │
│    │MongoDB/ │         │ Secrets │     │
│    │Postgres │         │ Vault   │     │
│    │(TDE req)│         │(AES-256)│     │
│    └─────────┘         └─────────┘     │
└──────────────┬──────────────────────────┘
               │
               │  HTTPS (BAA required with provider)
               ▼
       ┌───────────────┐
       │  LLM Provider  │
       │  (see matrix)  │
       └───────────────┘
```

**What is sent to LLM providers:**

* Current user message (may contain PHI)
* Recent conversation history (windowed — may contain PHI)
* Agent system prompt (configured by deployer, should NOT contain PHI)

**What is NOT sent:**

* User IDs or account metadata
* Data from other conversations or agents
* API keys (except the provider's own authentication key)

***

## Encryption at Rest

HIPAA requires all ePHI to be encrypted at rest. EDDI stores conversation data and user memories in MongoDB or PostgreSQL. **The deployer must enable database-level encryption.**

EDDI will log a startup warning if encryption has not been acknowledged:

```
COMPLIANCE: Database encryption status unknown
```

### MongoDB

Use [WiredTiger Encryption at Rest](https://www.mongodb.com/docs/manual/core/security-encryption-at-rest/) (MongoDB Enterprise) or encrypt the underlying volume.

### PostgreSQL

Options (choose one):

* **Cloud-managed encryption**: AWS RDS encryption, Azure Database encryption, GCP Cloud SQL encryption — all encrypt at the storage layer
* **Full-disk encryption**: LUKS (Linux), BitLocker (Windows)
* **Tablespace encryption**: Available in PostgreSQL 16+ with third-party extensions

### Acknowledging Encryption

Once database encryption is configured, suppress the startup warning:

```properties
eddi.compliance.database-encryption-acknowledged=true
```

***

## Encryption in Transit

HIPAA requires encryption of ePHI during transmission. Configure TLS using one of these approaches:

### Option 1: TLS at Reverse Proxy (Recommended)

Terminate TLS at nginx, Traefik, Caddy, or your cloud load balancer. EDDI communicates with the proxy over localhost.

### Option 2: TLS Directly in EDDI

```properties
quarkus.http.ssl.certificate.file=/path/to/cert.pem
quarkus.http.ssl.certificate.key-file=/path/to/key.pem
quarkus.http.ssl-port=8443
```

***

## LLM Provider BAA Requirements

When conversation content containing PHI is sent to a cloud LLM provider, that provider becomes a **sub-Business Associate**. You must have a BAA in place.

| Provider                 | BAA Available? | Notes                                                     |
| ------------------------ | -------------- | --------------------------------------------------------- |
| **Ollama** (self-hosted) | N/A            | No external transfer — recommended for HIPAA              |
| **jlama** (self-hosted)  | N/A            | No external transfer — recommended for HIPAA              |
| **Azure OpenAI**         | ✅ Yes          | Via Azure Enterprise Agreement; data stays in your region |
| **AWS Bedrock**          | ✅ Yes          | Via AWS BAA; HIPAA-eligible service                       |
| **Google Vertex AI**     | ✅ Yes          | Via Google Cloud BAA                                      |
| **OpenAI API**           | ✅ Yes          | OpenAI offers BAAs for API customers (not ChatGPT)        |
| **Anthropic**            | ⚠️ Contact     | Contact sales for BAA availability                        |
| **Mistral**              | ⚠️ Contact     | Contact sales for BAA availability                        |
| **Hugging Face**         | ❌ Varies       | Depends on model hosting — evaluate per deployment        |

> **Recommendation:** For maximum HIPAA safety, use **Ollama or jlama** with self-hosted models. This eliminates external PHI transfer entirely.

***

## Authentication & Session Management

### Enable Keycloak

HIPAA requires person authentication (§164.312(d)). Enable Keycloak:

```bash
docker run -e QUARKUS_OIDC_TENANT_ENABLED=true \
           -e QUARKUS_OIDC_AUTH_SERVER_URL=http://keycloak:8080/realms/eddi \
           labsai/eddi:latest
```

### Session Timeout

HIPAA requires automatic logoff after inactivity (§164.312(a)(2)(iii)). Configure Keycloak session timeouts:

| Setting               | Recommended Value | Keycloak Path              |
| --------------------- | ----------------- | -------------------------- |
| SSO Session Idle      | 15 minutes        | Realm Settings → Sessions  |
| SSO Session Max       | 8 hours           | Realm Settings → Sessions  |
| Client Session Idle   | 15 minutes        | Client → Advanced Settings |
| Access Token Lifespan | 5 minutes         | Realm Settings → Tokens    |

***

## Minimum Necessary Standard

HIPAA (§164.502(b)) requires that only the minimum necessary PHI is used for each operation. EDDI provides several mechanisms:

1. **Conversation History Windowing**: `ConversationHistoryBuilder` limits the number of past turns sent to the LLM (configurable per agent)
2. **Context Selection Rules** (planned): Conditional context loading based on current action — sends only task-relevant data to the LLM
3. **Self-hosted models**: Eliminates external PHI exposure entirely

***

## Emergency Access Procedure

HIPAA requires emergency access procedures (§164.312(a)(2)(ii)). Document a "break glass" process for your deployment:

1. **Emergency admin account**: Create a dedicated Keycloak account with `eddi-admin` role, stored in a sealed envelope or hardware security module
2. **Activation**: Two-person authorization to unseal the emergency credentials
3. **Audit**: All emergency access is logged in the immutable audit ledger
4. **Deactivation**: Rotate emergency credentials after each use

***

## Breach Notification

HIPAA breach notification timelines differ from GDPR:

| Regulation | Notify Authority                          | Notify Individuals                                    |
| ---------- | ----------------------------------------- | ----------------------------------------------------- |
| **HIPAA**  | HHS within **60 days**                    | Without unreasonable delay, no later than **60 days** |
| **GDPR**   | Supervisory authority within **72 hours** | Without undue delay if high risk                      |

For small breaches (< 500 individuals), HIPAA allows annual batch notification to HHS. For large breaches (≥ 500), immediate notification plus media notice in affected states.

See [incident-response.md](/security-and-compliance/incident-response) for the full response runbook.

***

## Deployer Checklist

As the HIPAA-covered entity or business associate deploying EDDI:

* [ ] **BAA**: Execute a Business Associate Agreement with any managed EDDI hosting provider
* [ ] **LLM Provider BAAs**: Execute BAAs with each cloud LLM provider used in your agents (or use self-hosted Ollama/jlama)
* [ ] **TLS**: Enable TLS for all EDDI endpoints (direct or via reverse proxy)
* [ ] **Database Encryption**: Enable encryption at rest on MongoDB/PostgreSQL
* [ ] **Vault Master Key**: Set `EDDI_VAULT_MASTER_KEY` (enables AES-256-GCM encryption for API keys and HMAC audit signing)
* [ ] **Keycloak**: Enable authentication (`QUARKUS_OIDC_TENANT_ENABLED=true`)
* [ ] **Session Timeouts**: Configure 15-minute idle timeout in Keycloak
* [ ] **RBAC**: Assign minimum necessary roles to each operator
* [ ] **Data Retention**: Review `eddi.conversations.deleteEndedConversationsOnceOlderThanDays` — reduce from 365 to minimum necessary
* [ ] **User Memory Purge**: Configure `eddi.usermemory.auto-purge-days` if PHI is stored in user memories
* [ ] **Emergency Access**: Document emergency access procedure with two-person authorization
* [ ] **Risk Assessment**: Complete HIPAA Security Risk Assessment for your deployment
* [ ] **Workforce Training**: Train all operators on PHI handling procedures
* [ ] **Breach Response**: Prepare breach notification plan per [incident-response.md](/security-and-compliance/incident-response)

***

## See Also

* [PRIVACY.md](/security-and-compliance/privacy) — Data processing overview
* [gdpr-compliance.md](/security-and-compliance/gdpr-compliance) — GDPR/CCPA operations
* [security.md](/security-and-compliance/security) — Security architecture
* [secrets-vault.md](/security-and-compliance/secrets-vault) — Encryption and key management
* [audit-ledger.md](/security-and-compliance/audit-ledger) — Immutable decision trail
* [incident-response.md](/security-and-compliance/incident-response) — Breach response runbook


# EU AI Act Compliance

> **The EU AI Act** is the world's first comprehensive AI regulation. It applies to any AI system used within the EU, regardless of where the provider is based. EDDI deployers must classify their agents by risk level and meet corresponding obligations.

This guide maps EDDI's built-in features to EU AI Act requirements and helps deployers determine their compliance obligations.

***

## Risk Classification

The EU AI Act classifies AI systems into four risk tiers. The deployer (not EDDI as infrastructure) determines the risk level based on the **use case**, not the technology.

### High-Risk AI Systems (Annex III)

Your EDDI agents are **high-risk** if used for:

| Domain                      | Examples                                                       |
| --------------------------- | -------------------------------------------------------------- |
| **Healthcare**              | Patient triage, symptom assessment, treatment recommendations  |
| **Employment**              | Resume screening, interview assessment, performance evaluation |
| **Credit & Finance**        | Credit scoring, loan eligibility, fraud detection              |
| **Education**               | Student assessment, admission decisions                        |
| **Law Enforcement**         | Suspect profiling, crime prediction                            |
| **Critical Infrastructure** | Energy management, water treatment decisions                   |

**Obligations**: Full compliance with Articles 9–15 (risk management, data governance, technical documentation, transparency, human oversight, accuracy/robustness).

### Limited-Risk AI Systems

Your EDDI agents are **limited-risk** if they interact with humans but don't fall into high-risk categories:

| Examples                      |
| ----------------------------- |
| Customer service chatbots     |
| Product recommendation agents |
| FAQ / help desk bots          |

**Obligations**: Transparency only — users must be informed they are interacting with an AI system (Article 52).

### Minimal-Risk AI Systems

| Examples                |
| ----------------------- |
| Entertainment chatbots  |
| Internal testing agents |

**Obligations**: None specific, but general principles apply.

***

## EDDI Feature Mapping

### Article 9 — Risk Management System

| Requirement                                  | EDDI Feature                             | Status      |
| -------------------------------------------- | ---------------------------------------- | ----------- |
| Identify and analyze known/foreseeable risks | Behavior rules with guardrails           | ✅ Available |
| Estimate and evaluate risks                  | Cost tracking, token budgets             | ✅ Available |
| Adopt risk management measures               | Rate limiting, tool caching, budget caps | ✅ Available |
| Test risk management measures                | Integration test suite, Testcontainers   | ✅ Available |

**Deployer action**: Document your risk assessment per agent in your internal risk management system.

### Articles 11–12 — Technical Documentation & Record-Keeping

| Requirement                             | EDDI Feature                                               | Status                     |
| --------------------------------------- | ---------------------------------------------------------- | -------------------------- |
| General description of the AI system    | [architecture.md](/architecture-and-concepts/architecture) | ✅ Available                |
| Detailed description of system elements | Agent configuration (JSON)                                 | ✅ Available                |
| Information about training data         | N/A — EDDI uses pre-trained models                         | ℹ️ Provider responsibility |
| Capabilities and limitations            | Agent config + system prompt                               | ✅ Available                |
| Automatic logging / record-keeping      | Immutable audit ledger                                     | ✅ Available                |

**Deployer action**: Maintain technical documentation that references EDDI's architecture docs and your agent configuration.

### Article 13 — Transparency & Information

| Requirement                                 | EDDI Feature                      | Status      |
| ------------------------------------------- | --------------------------------- | ----------- |
| Inform users they are interacting with AI   | Deployer responsibility           | ⚠️ Deployer |
| Explain system capabilities and limitations | System prompt + agent description | ✅ Available |
| Provide contact information for deployer    | Deployer responsibility           | ⚠️ Deployer |

**Deployer action**: Display a clear notice that users are interacting with an AI system. Include this in your application's UI or terms of service.

### Article 14 — Human Oversight

| Requirement                               | EDDI Feature                                | Status      |
| ----------------------------------------- | ------------------------------------------- | ----------- |
| Human ability to understand AI outputs    | Audit ledger (full prompt + response trail) | ✅ Available |
| Human ability to override AI decisions    | Behavior rules (action routing)             | ✅ Available |
| Human ability to stop the AI system       | Agent undeploy, conversation end            | ✅ Available |
| Human-in-the-loop for high-risk decisions | HITL framework (planned Phase 9b)           | ⚠️ Planned  |

**Deployer action for high-risk agents**: Configure behavior rules that require human approval for consequential decisions. Use the `managed_agent` pattern to route high-stakes outputs through a human review queue.

### Articles 17/19 — Quality Management & Logging

| Requirement                     | EDDI Feature                              | Status      |
| ------------------------------- | ----------------------------------------- | ----------- |
| Immutable decision traceability | HMAC-signed audit ledger                  | ✅ Available |
| What data was read by each task | Audit entry `input` field                 | ✅ Available |
| What data was produced          | Audit entry `output` field                | ✅ Available |
| LLM prompts and responses       | Audit entry `llmDetail` field             | ✅ Available |
| Tool invocations and results    | Audit entry `toolCalls` field             | ✅ Available |
| Timing and cost                 | Audit entry `durationMs` + `cost` fields  | ✅ Available |
| Tamper detection                | HMAC-SHA256 integrity hash on every entry | ✅ Available |

This is EDDI's strongest compliance area. The audit ledger was specifically designed for EU AI Act compliance.

***

## Deployer Checklist

### All Deployments

* [ ] **Risk classification**: Determine the risk level of each agent
* [ ] **Transparency notice**: Inform users they are interacting with AI
* [ ] **Audit ledger**: Ensure `eddi.audit.enabled=true` (default)
* [ ] **Vault master key**: Set `EDDI_VAULT_MASTER_KEY` for HMAC audit signing

### High-Risk Deployments

All of the above, plus:

* [ ] **Technical documentation**: Maintain documentation per Art. 11
* [ ] **Risk assessment**: Document per-agent risk analysis
* [ ] **Human oversight**: Configure behavior rules for human review of high-stakes decisions
* [ ] **Data governance**: Document training data provenance (this is the LLM provider's responsibility — ensure your provider complies)
* [ ] **Accuracy testing**: Regularly evaluate agent output quality
* [ ] **Incident reporting**: Report serious incidents to the relevant national authority (Art. 62)

***

## See Also

* [audit-ledger.md](/security-and-compliance/audit-ledger) — Immutable decision trail (Art. 17/19)
* [security.md](/security-and-compliance/security) — Security architecture
* [behavior-rules.md](/agent-configuration/behavior-rules) — Agent guardrails configuration
* [hipaa-compliance.md](/security-and-compliance/hipaa-compliance) — HIPAA compliance guide
* [gdpr-compliance.md](/security-and-compliance/gdpr-compliance) — GDPR/CCPA compliance


# Compliance Data Flow

> **Audience**: Compliance auditors, DPOs, and deployers performing risk assessments. This document provides a single-page overview of how data flows through EDDI, where it's stored, and where encryption is applied.

***

## System Data Flow

```
┌──────────────────────────────────────────────────────────────────────────────┐
│                                EDDI Platform                                │
│                                                                             │
│  ┌──────────┐    ┌────────────────┐    ┌──────────────────────────────────┐ │
│  │ Keycloak │───▶│  REST API /    │───▶│     Conversation Pipeline        │ │
│  │  (OIDC)  │    │  SSE / MCP     │    │                                  │ │
│  │          │    │                │    │  Input → Parser → Behavior Rules │ │
│  │ JWT auth │    │  TLS required  │    │  → LLM Task → Output Generation │ │
│  └──────────┘    └────────────────┘    └──────────┬───────────────────────┘ │
│                                                   │                         │
│                    ┌──────────────────────────────┼──────────────────┐      │
│                    │              │               │                  │      │
│              ┌─────▼─────┐ ┌─────▼────┐  ┌──────▼──────┐  ┌───────▼────┐ │
│              │ Conversa- │ │  User    │  │   Audit     │  │  Secrets   │ │
│              │ tion      │ │ Memory   │  │   Ledger    │  │  Vault     │ │
│              │ Memory    │ │ Store    │  │             │  │            │ │
│              │           │ │          │  │  HMAC-signed│  │ AES-256-GCM│ │
│              │ PII: Yes  │ │ PII: Yes │  │  Write-once │  │ Envelope   │ │
│              │ Encrypted:│ │ Encrypted│  │  PII: Yes** │  │ encryption │ │
│              │ TDE*      │ │ TDE*     │  │  Encrypted: │  │            │ │
│              │           │ │          │  │  TDE*       │  │ PII: No    │ │
│              └─────┬─────┘ └─────┬───┘  └──────┬──────┘  └────────────┘ │
│                    │             │              │                         │
│                    └─────────────┼──────────────┘                         │
│                                 │                                         │
│                          ┌──────▼──────┐                                  │
│                          │  MongoDB /  │                                  │
│                          │ PostgreSQL  │                                  │
│                          │             │                                  │
│                          │ TDE* = DB-  │                                  │
│                          │ level       │                                  │
│                          │ encryption  │                                  │
│                          └─────────────┘                                  │
│                                                                           │
│              ** Audit userId is pseudonymized on GDPR erasure             │
└──────────────────────────────┬────────────────────────────────────────────┘
                               │
                               │ HTTPS (conversation content)
                               │ Only when LLM Task executes
                               ▼
                    ┌──────────────────────┐
                    │    LLM Provider      │
                    │                      │
                    │  Receives:           │
                    │  • User message      │
                    │  • Chat history      │
                    │  • System prompt     │
                    │                      │
                    │  Does NOT receive:   │
                    │  • User IDs          │
                    │  • API keys          │
                    │  • Other sessions    │
                    └──────────────────────┘
```

***

## Data Store Inventory

| Data Store                | Contains PII                        | Encryption                      | Retention                       | Deletable            | Regulatory Notes         |
| ------------------------- | ----------------------------------- | ------------------------------- | ------------------------------- | -------------------- | ------------------------ |
| **Conversation Memory**   | ✅ userId, chat content              | TDE (deployer)                  | 365 days default (configurable) | ✅ GDPR cascade       | Primary PII store        |
| **User Memory**           | ✅ userId, structured facts          | TDE (deployer)                  | Until deleted                   | ✅ GDPR cascade       | Cross-conversation state |
| **Managed Conversations** | ✅ userId, intent mappings           | TDE (deployer)                  | Until deleted                   | ✅ GDPR cascade       | Routing metadata         |
| **Audit Ledger**          | ✅ userId (pseudonymized on erasure) | TDE (deployer) + HMAC           | Indefinite                      | ❌ Pseudonymized only | EU AI Act Art. 17/19     |
| **Database Logs**         | ✅ userId (pseudonymized on erasure) | TDE (deployer)                  | Configurable                    | ❌ Pseudonymized only | Operational data         |
| **Secrets Vault**         | ❌ API keys only                     | AES-256-GCM (application-level) | Until rotated/deleted           | ✅ Via REST API       | Credentials only         |

***

## PII Lifecycle

```
User Input (may contain PII)
    │
    ├──▶ Stored in Conversation Memory (MongoDB/PostgreSQL)
    │        └─ Retention: configurable (default 365 days)
    │        └─ Auto-deleted after retention period
    │        └─ Or: GDPR cascade delete (immediate)
    │
    ├──▶ Extracted to User Memory (if PropertySetter configured)
    │        └─ Retention: until deleted
    │        └─ Or: GDPR cascade delete (immediate)
    │
    ├──▶ Sent to LLM Provider (if LLM task triggers)
    │        └─ Transient: not stored by EDDI after response
    │        └─ Provider retention: per provider's data policy
    │
    ├──▶ Logged in Audit Ledger (userId + task data)
    │        └─ Retention: indefinite (EU AI Act)
    │        └─ userId pseudonymized on GDPR erasure (SHA-256)
    │        └─ HMAC integrity hash prevents tampering
    │
    └──▶ Secret-scoped values → Secrets Vault
             └─ Vault reference replaces plaintext in memory
             └─ Raw input scrubbed from conversation step
```

***

## Encryption Summary

| Layer                     | Mechanism                         | Managed By                           | Covers                               |
| ------------------------- | --------------------------------- | ------------------------------------ | ------------------------------------ |
| **In Transit**            | TLS 1.2+                          | Deployer (reverse proxy or direct)   | All HTTP/SSE/MCP traffic             |
| **At Rest (credentials)** | AES-256-GCM envelope encryption   | EDDI Secrets Vault                   | API keys, tokens, passwords          |
| **At Rest (data)**        | Transparent Data Encryption (TDE) | Deployer (database config)           | Conversations, memories, audit, logs |
| **Audit Integrity**       | HMAC-SHA256                       | EDDI (derived from vault master key) | Tamper detection on audit entries    |

***

## GDPR Erasure Cascade

When `DELETE /admin/gdpr/{userId}` is called:

```
1. User Memories ──────────────── PERMANENTLY DELETED
2. Conversation Snapshots ─────── PERMANENTLY DELETED
3. Managed Conversation Maps ──── PERMANENTLY DELETED
4. Database Logs ──────────────── userId → SHA-256 PSEUDONYMIZED
5. Audit Ledger ───────────────── userId → SHA-256 PSEUDONYMIZED
6. Audit Ledger Event ─────────── GDPR_ERASURE entry written (immutable)
```

Steps 4–5 retain operational and compliance data but make re-identification impossible without the original userId.

***

## See Also

* [PRIVACY.md](/security-and-compliance/privacy) — Data processing overview
* [hipaa-compliance.md](/security-and-compliance/hipaa-compliance) — HIPAA deployment guide
* [eu-ai-act-compliance.md](/security-and-compliance/eu-ai-act-compliance) — EU AI Act compliance
* [gdpr-compliance.md](/security-and-compliance/gdpr-compliance) — GDPR/CCPA operations
* [secrets-vault.md](/security-and-compliance/secrets-vault) — Encryption architecture
* [audit-ledger.md](/security-and-compliance/audit-ledger) — Audit trail details


# Incident Response Plan

This runbook outlines the steps for detecting, assessing, and responding to a data breach involving the EDDI platform.

## 1. Detection

### Indicators

* Unexpected audit ledger HMAC validation failures (tamper detection)
* Unusual API access patterns in `/admin/` endpoints
* Failed authentication spikes in Keycloak logs
* Anomalous conversation volume or data export requests
* Alerts from infrastructure monitoring (Grafana/Prometheus)

### Monitoring

EDDI exposes metrics at `/q/metrics` (Prometheus format):

* `eddi.conversations.active` — active conversation count
* `eddi.tool.execution.count` — tool execution volume
* `eddi.audit.entries.count` — audit ledger write rate

## 2. Assessment (First 4 Hours)

### Scope Determination

1. **Identify affected data**: Which stores were compromised?
   * Conversation content (chat history)
   * User memories (persistent facts)
   * API keys/credentials (vault)
   * Audit trail integrity
2. **Identify affected users**: Query the GDPR export endpoint to enumerate affected user data:

   ```bash
   GET /admin/gdpr/{userId}/export
   ```
3. **Determine attack vector**: Check database logs for unauthorized access:

   ```bash
   GET /admin/logs?level=ERROR&limit=100
   ```

### Risk Classification

| Risk Level | Criteria                         | Response             |
| ---------- | -------------------------------- | -------------------- |
| **High**   | PII exposure, credentials leaked | Full breach protocol |
| **Medium** | System data exposed, no PII      | Containment + review |
| **Low**    | Failed attempt, no data access   | Log + monitor        |

## 3. Containment (First 24 Hours)

1. **Rotate compromised credentials**:
   * Rotate all LLM API keys in the Secrets Vault
   * Invalidate affected Keycloak sessions
   * Update any exposed database credentials
2. **Isolate affected systems**:
   * Undeploy compromised agents
   * Revoke affected user tokens
3. **Preserve evidence**:
   * Export audit trail for affected conversations
   * Snapshot database logs
   * Do NOT delete audit entries (immutable by design)

## 4. Notification

### GDPR (Art. 33-34)

* **Supervisory authority**: Within **72 hours** of becoming aware
* **Data subjects**: Without undue delay if high risk to rights/freedoms
* **Processor → Controller**: If you use EDDI as a managed service, ensure your hosting provider notifies you immediately upon discovering a breach. If you self-host, you are both controller and processor.

### CCPA

* **Affected consumers**: Notification required for certain categories of personal information
* **California Attorney General**: If breach affects 500+ residents

### HIPAA (§164.408)

* **HHS (Secretary)**: Within **60 days** of discovery
* **Affected individuals**: Without unreasonable delay, no later than **60 days** after discovery
* **Media**: If breach affects ≥ 500 residents of a state, notify prominent media outlets in that state
* **Small breaches** (< 500 individuals): May be reported annually to HHS in a batch submission

### Template

```
Subject: Data Breach Notification

Date of discovery: [DATE]
Nature of breach: [DESCRIPTION]
Categories of data: [conversation content / user memories / credentials]
Approximate number of affected users: [COUNT]
Measures taken: [CONTAINMENT STEPS]
Contact: [DPO / PRIVACY CONTACT]
```

## 5. Recovery

1. Deploy patched version of EDDI with vulnerability remediated
2. Re-validate audit ledger integrity (HMAC chain verification)
3. Conduct post-incident review
4. Update this runbook with lessons learned

## 6. Prevention

* Enable Keycloak authentication in production
* Use RBAC (`eddi-admin`, `eddi-viewer`) for all administrative operations
* Review audit trail regularly for anomalies
* Keep EDDI updated to the latest version
* Use self-hosted LLM providers for sensitive deployments
* Enable TLS for all EDDI endpoints
* Regularly rotate API keys and vault credentials

## 7. Emergency Access Procedure (HIPAA §164.312(a)(2)(ii))

For healthcare deployments, maintain a documented "break glass" procedure:

1. **Emergency admin account**: Create a dedicated Keycloak account with `eddi-admin` role, stored in a sealed envelope or hardware security module (HSM)
2. **Activation**: Require two-person authorization to unseal the emergency credentials
3. **Audit**: All emergency access is logged in the immutable audit ledger — EDDI records every API call, tool invocation, and data access
4. **Deactivation**: Rotate emergency credentials immediately after each use via Keycloak admin console
5. **Documentation**: Log the reason for emergency access, duration, and actions taken

## See Also

* [hipaa-compliance.md](https://github.com/labsai/EDDI/blob/main/docs/hipaa-compliance.md) — HIPAA deployment guide
* [gdpr-compliance.md](/security-and-compliance/gdpr-compliance) — GDPR/CCPA compliance
* [security.md](/security-and-compliance/security) — Security architecture
* [audit-ledger.md](/security-and-compliance/audit-ledger) — Immutable audit trail


# Privacy & Data Processing

> **EDDI is a data processor.** Organizations deploying EDDI act as the data controller and are responsible for obtaining user consent, maintaining data processing agreements (DPAs), and ensuring lawful basis for processing.

## What Data EDDI Stores

| Data Category             | Storage              | Contains PII?                  | Retention                       |
| ------------------------- | -------------------- | ------------------------------ | ------------------------------- |
| **Conversation Memory**   | MongoDB / PostgreSQL | Yes (userId, chat content)     | 365 days default (configurable) |
| **User Memory**           | MongoDB / PostgreSQL | Yes (userId, structured facts) | Until explicitly deleted        |
| **Audit Ledger**          | MongoDB / PostgreSQL | Yes (userId)                   | Indefinite (EU AI Act)          |
| **Database Logs**         | MongoDB / PostgreSQL | Yes (userId)                   | Configurable                    |
| **Managed Conversations** | MongoDB / PostgreSQL | Yes (userId, intent)           | Until explicitly deleted        |

## Data Subject Rights

### Right to Erasure (Art. 17)

EDDI provides a unified erasure endpoint:

```
DELETE /admin/gdpr/{userId}
```

This cascades across all stores:

1. **User memories** — permanently deleted
2. **Conversation snapshots** — permanently deleted
3. **Managed conversation mappings** — permanently deleted
4. **Database logs** — userId pseudonymized (SHA-256 hash)
5. **Audit ledger** — userId pseudonymized (SHA-256 hash)

**Why pseudonymize instead of delete?** The audit ledger and logs are retained under GDPR Art. 17(3)(e) — compliance with EU AI Act Articles 17/19 which require immutable decision traceability for AI systems. User identifiers are replaced with irreversible hashes, making re-identification impossible without the original identifier.

### Right of Access / Portability (Art. 15/20)

```
GET /admin/gdpr/{userId}/export
```

Returns all user data as JSON: memories, conversations (with chat history), and managed conversation mappings.

### MCP Tools

For AI-orchestrated compliance workflows:

* `delete_user_data` — full cascade erasure (requires `confirmation="CONFIRM"`)
* `export_user_data` — complete user data bundle

## Security Measures

* **Encryption at rest**: AES-256-GCM via Secrets Vault for API keys and credentials
* **Immutable audit trail**: HMAC-signed ledger entries for tamper detection
* **Secret redaction**: `SecretRedactionFilter` scrubs API keys, tokens, and vault references from audit entries before persistence
* **RBAC**: All GDPR endpoints require `eddi-admin` role
* **Input validation**: URL validation, regex injection prevention, path traversal protection
* **PII-safe logging**: GDPR operations log SHA-256 pseudonyms, never raw user IDs

## Third-Party Data Transfers (GDPR Art. 44-49)

EDDI sends conversation content to configured LLM providers during AI processing. **Every conversation turn constitutes a data transfer to the selected provider.** The specific provider is configured per agent by the deployer.

### Supported LLM Providers and Data Locations

| Provider          | Data Location        | Self-Hosted? | DPA Available                                                   |
| ----------------- | -------------------- | ------------ | --------------------------------------------------------------- |
| **Ollama**        | Your infrastructure  | ✅ Yes        | N/A (local)                                                     |
| **jlama**         | Your infrastructure  | ✅ Yes        | N/A (local)                                                     |
| **Anthropic**     | US/EU (varies)       | ❌ No         | [anthropic.com/policies](https://www.anthropic.com/policies)    |
| **OpenAI**        | US                   | ❌ No         | [openai.com/policies](https://openai.com/policies)              |
| **Google Gemini** | US/EU (varies)       | ❌ No         | [cloud.google.com/terms](https://cloud.google.com/terms)        |
| **Mistral**       | EU (France)          | ❌ No         | [mistral.ai/terms](https://mistral.ai/terms/)                   |
| **Azure OpenAI**  | Your Azure region    | Partially    | Via Azure Enterprise Agreement                                  |
| **AWS Bedrock**   | Your AWS region      | Partially    | Via AWS Enterprise Agreement                                    |
| **Oracle GenAI**  | Your OCI region      | Partially    | Via Oracle Cloud Agreement                                      |
| **Hugging Face**  | Varies by model host | Partially    | [huggingface.co/terms](https://huggingface.co/terms-of-service) |

### Deployer Responsibilities for LLM Data Transfers

As the data controller, you **must**:

1. **Select providers**: Choose LLM providers that meet your data residency and compliance requirements
2. **Establish DPAs**: Sign Data Processing Agreements with each cloud LLM provider you configure in EDDI
3. **Document transfers**: Record all LLM providers in your Article 30 processing register
4. **Inform users**: Disclose in your privacy policy that conversations are processed by third-party AI providers
5. **Assess adequacy**: For non-EU providers, ensure adequate safeguards (Standard Contractual Clauses, adequacy decisions, or binding corporate rules) per GDPR Art. 46
6. **Consider self-hosting**: For maximum data sovereignty, use Ollama or jlama with self-hosted models — zero external data transfers

### What Data is Sent to LLM Providers

| Data Type            | Sent? | Notes                                          |
| -------------------- | ----- | ---------------------------------------------- |
| User messages        | ✅ Yes | The current turn's input                       |
| Conversation history | ✅ Yes | Recent conversation context (windowed)         |
| System prompt        | ✅ Yes | Agent instructions (configured by deployer)    |
| User ID              | ❌ No  | Not included in LLM requests                   |
| API keys             | ❌ No  | Only the provider's own key for authentication |

EDDI does **not** send user IDs, metadata, or data from other conversations to LLM providers. Only the conversation context relevant to the current agent interaction is transmitted.

## Consent (GDPR Art. 6/7)

EDDI does **not** manage user consent. As a data processor, consent is the controller's responsibility.

### Legal Basis

EDDI's data processing activities and their typical legal bases:

| Processing Activity     | Suggested Legal Basis                         | Notes                    |
| ----------------------- | --------------------------------------------- | ------------------------ |
| Conversation processing | Art. 6(1)(a) Consent or Art. 6(1)(b) Contract | Controller determines    |
| Persistent user memory  | Art. 6(1)(a) Consent                          | Users should be informed |
| Audit ledger retention  | Art. 6(1)(c) Legal obligation                 | EU AI Act Art. 17/19     |
| System logging          | Art. 6(1)(f) Legitimate interest              | Operational necessity    |

### Controller Obligations

Deployers should:

1. Obtain appropriate consent before enabling conversational AI
2. Inform users about data processing via their privacy policy
3. Provide clear opt-out mechanisms in their application
4. Consider disabling `enableMemoryTools` unless users are explicitly informed that their interactions are remembered across sessions
5. Document the legal basis for each processing activity in your Article 30 register

## CCPA Compliance

### Do Not Sell (§1798.120)

EDDI does **not sell personal information** and has no mechanism to do so. EDDI is middleware infrastructure — it processes data on behalf of the deployer (controller) and does not share, sell, or monetize user data with any third party for commercial purposes.

If your deployment scenario involves sharing user data with third parties (e.g., analytics providers), this is the controller's responsibility to manage and disclose.

### Right to Know (§1798.100)

The GDPR export endpoint (`GET /admin/gdpr/{userId}/export`) satisfies the CCPA "right to know" requirement by providing all personal information collected about a consumer in a structured, machine-readable format.

### Right to Delete (§1798.105)

The GDPR erasure endpoint (`DELETE /admin/gdpr/{userId}`) satisfies the CCPA "right to delete" requirement.

## Data Retention

| Category            | Default        | Configuration                                                  |
| ------------------- | -------------- | -------------------------------------------------------------- |
| Ended conversations | 365 days       | `eddi.conversations.deleteEndedConversationsOnceOlderThanDays` |
| Idle conversations  | 90 days        | `eddi.conversations.maximumLifeTimeOfIdleConversationsInDays`  |
| User memories       | No auto-delete | Use GDPR API or MCP tools                                      |
| Audit ledger        | No auto-delete | Retained for EU AI Act compliance                              |

Set retention to `-1` to disable automatic cleanup.

## International Privacy Regulations

EDDI is open-source middleware deployed by organizations worldwide in regulated environments. This section maps each major privacy framework to EDDI's technical capabilities and identifies the organizational measures deployers add on top.

> **How to read this section**: Privacy regulations have two layers — **technical safeguards** (encryption, access control, audit, erasure) and **organizational measures** (consent processes, officer appointments, breach notification procedures). EDDI implements the technical layer. Your organization implements the organizational layer. Together, they form a complete compliance posture.
>
> * ✅ **Built-in** — EDDI handles this out of the box
> * 🏢 **Your org** — Organizational measure, outside the scope of middleware
> * ✅ + 🏢 — EDDI provides the technical foundation; your org completes it

***

### PIPEDA — Canada

Canada's **Personal Information Protection and Electronic Documents Act** (2000, amended 2023) governs the collection, use, and disclosure of personal information in the course of commercial activity. It follows the 10 Fair Information Principles.

| PIPEDA Principle            | EDDI Technical Capability                                                                                              | Status      |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------- |
| **Accountability**          | Immutable HMAC-signed audit ledger traces all operations                                                               | ✅ + 🏢      |
| **Identifying Purposes**    | Agent configuration documents AI processing purpose                                                                    | ✅ + 🏢      |
| **Consent**                 | Deployer integrates consent capture in their application layer                                                         | 🏢 Your org |
| **Limiting Collection**     | Token-aware windowing limits data sent to LLMs; configurable retention auto-deletes old conversations                  | ✅ Built-in  |
| **Limiting Use/Disclosure** | Data used only for configured agent interactions; audit trail logs every LLM invocation (model name, prompt, response) | ✅ Built-in  |
| **Accuracy**                | Conversation state is timestamped and versioned; user memories updatable via `PUT /usermemorystore/memories`           | ✅ Built-in  |
| **Safeguards**              | AES-256-GCM envelope encryption (Secrets Vault), HMAC-SHA256 audit integrity, Keycloak OIDC, role-based access control | ✅ Built-in  |
| **Openness**                | Full source code is open (Apache 2.0); PRIVACY.md and documentation are public                                         | ✅ Built-in  |
| **Individual Access**       | `GET /admin/gdpr/{userId}/export` — returns all memories, conversations, and managed conversation mappings as JSON     | ✅ Built-in  |
| **Challenging Compliance**  | `DELETE /admin/gdpr/{userId}` — cascade deletion across all 5 data stores; audit trail pseudonymized (not deleted)     | ✅ Built-in  |

**Deployer checklist**:

| Responsibility    | Details                                                                                                                                                                                                                                   |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Consent capture   | Integrate consent flow in your frontend before enabling EDDI chat. PIPEDA: implied consent for non-sensitive data, express consent for sensitive data (health, financial)                                                                 |
| Bilingual notices | Provide English/French privacy notices for Canadian consumers                                                                                                                                                                             |
| Breach reporting  | Report breaches with "real risk of significant harm" to the **Office of the Privacy Commissioner** (OPC) and affected individuals. Maintain breach records for 24 months. Use EDDI's `docs/incident-response.md` as your runbook template |

***

### LGPD — Brazil

Brazil's **Lei Geral de Proteção de Dados** (2018, effective 2020) closely mirrors GDPR and grants data subjects (titulares) extensive rights over their personal data.

| LGPD Right                                    | EDDI Technical Capability                                                                                                                                  | Status     |
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| Confirmation of processing (Art. 18, I)       | Documented in PRIVACY.md; audit trail records all operations                                                                                               | ✅ Built-in |
| Access to data (Art. 18, II)                  | `GET /admin/gdpr/{userId}/export` — full JSON bundle                                                                                                       | ✅ Built-in |
| Correction of inaccurate data (Art. 18, III)  | `PUT /usermemorystore/memories` — upserts individual memory entries                                                                                        | ✅ Built-in |
| Anonymization/blocking/deletion (Art. 18, IV) | `DELETE /admin/gdpr/{userId}` — cascade deletion + SHA-256 pseudonymization of audit trail                                                                 | ✅ Built-in |
| Data portability (Art. 18, V)                 | JSON export includes all data; machine-readable format                                                                                                     | ✅ Built-in |
| Deletion of unnecessary data (Art. 18, VI)    | Configurable retention (`eddi.conversations.deleteEndedConversationsOnceOlderThanDays`) + idle conversation auto-end                                       | ✅ Built-in |
| Information about sharing (Art. 18, VII)      | LLM provider data flows documented; audit trail records model name per invocation                                                                          | ✅ Built-in |
| Consent revocation (Art. 18, IX)              | `POST /{conversationId}/endConversation` + `DELETE /admin/gdpr/{userId}` provide the technical mechanism; consent state tracking is your application layer | ✅ + 🏢     |

**Deployer checklist**:

| Responsibility         | Details                                                                                                                               |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Legal basis            | Establish a legal basis for processing (consent, legitimate interest, contract performance) before deploying EDDI agents              |
| DPO appointment        | Appoint a **Data Protection Officer** (Encarregado) — mandatory for all controllers                                                   |
| DPIA                   | Conduct and document Data Protection Impact Assessments for AI processing. EDDI's audit ledger provides the data inputs for your DPIA |
| Breach reporting       | Report security incidents to the **ANPD** within a "reasonable time" (ANPD recommends 2 business days)                                |
| Cross-border transfers | Ensure LLM providers meet LGPD transfer requirements (adequacy decisions, standard contractual clauses, or binding corporate rules)   |
| Portuguese notices     | Provide privacy notices in **Portuguese**                                                                                             |

***

### APPI — Japan

Japan's **Act on the Protection of Personal Information** (2003, significantly amended 2022) is one of Asia's most mature data protection laws. Japan holds an EU adequacy decision, facilitating cross-border data flows between the two regions.

| APPI Obligation                            | EDDI Technical Capability                                                                                              | Status     |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ---------- |
| Purpose of use specification (Art. 17)     | Agent configuration documents processing purpose via system prompts and behavior rules                                 | ✅ + 🏢     |
| Accurate and up-to-date data (Art. 19)     | Conversation state is timestamped and versioned; user memories are updatable via REST API                              | ✅ Built-in |
| Security control measures (Art. 23)        | AES-256-GCM vault encryption, HMAC-SHA256 audit integrity, Keycloak OIDC, RBAC, SSRF protection, sandboxed evaluation  | ✅ Built-in |
| Supervision of employees (Art. 24)         | Keycloak OIDC + role-based access (eddi-admin, eddi-editor, eddi-viewer)                                               | ✅ Built-in |
| Supervision of contractors (Art. 25)       | LLM provider data flows documented in PRIVACY.md; audit trail records which model/provider processed each turn         | ✅ Built-in |
| Disclosure to data subjects (Art. 33)      | `GET /admin/gdpr/{userId}/export` — full data bundle                                                                   | ✅ Built-in |
| Correction and deletion (Art. 34-35)       | `PUT /usermemorystore/memories` for correction; `DELETE /admin/gdpr/{userId}` for deletion                             | ✅ Built-in |
| Breach notification (Art. 26)              | Incident response runbook (`docs/incident-response.md`)                                                                | ✅ Built-in |
| Cross-border transfer (Art. 28)            | EDDI documents provider data flows; deployer verifies recipient country protections and obtains consent where required | ✅ + 🏢     |
| Pseudonymized information (2022 amendment) | GDPR erasure uses SHA-256 pseudonymization — satisfies APPI's pseudonymized information category                       | ✅ Built-in |

**Deployer checklist**:

| Responsibility            | Details                                                                                                                                                                   |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Purpose documentation     | Document AI processing purposes in your privacy notice; EDDI's agent config captures intent, but your privacy policy communicates it to users                             |
| Cross-border verification | Verify LLM provider countries meet APPI equivalence or obtain individual consent. Japan→EU: covered by adequacy decision. Other countries: require contractual safeguards |
| PPC registration          | Register with the **Personal Information Protection Commission** (PPC) if processing data at scale                                                                        |
| Breach reporting          | Notify PPC and affected individuals **promptly** (report within 3-5 days in practice, full report within 30 days)                                                         |

***

### POPIA — South Africa

South Africa's **Protection of Personal Information Act** (2013, effective 2021) establishes 8 data processing conditions closely aligned with EU standards. Enforced by the Information Regulator.

| POPIA Condition                             | EDDI Technical Capability                                                             | Status     |
| ------------------------------------------- | ------------------------------------------------------------------------------------- | ---------- |
| Accountability (Condition 1)                | HMAC-signed audit ledger, documented data flows, open-source code                     | ✅ Built-in |
| Processing limitation (Condition 2)         | Token-aware windowing, configurable retention, idle conversation auto-end             | ✅ Built-in |
| Purpose specification (Condition 3)         | Agent configuration documents purpose; deployer communicates to users                 | ✅ + 🏢     |
| Further processing limitation (Condition 4) | Data used only for configured agent interactions; audit trail provides accountability | ✅ Built-in |
| Information quality (Condition 5)           | Timestamped, versioned state; user memories updatable                                 | ✅ Built-in |
| Openness (Condition 6)                      | Public PRIVACY.md + Apache 2.0 open source                                            | ✅ Built-in |
| Security safeguards (Condition 7)           | AES-256-GCM, HMAC, Keycloak OIDC, RBAC, SSRF protection                               | ✅ Built-in |
| Data subject participation (Condition 8)    | Export endpoint + cascade deletion endpoint                                           | ✅ Built-in |

**Deployer checklist**:

| Responsibility                     | Details                                                                                                                                                                                               |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Information Regulator registration | Register with the **Information Regulator** before processing personal information                                                                                                                    |
| Information Officer                | Appoint an **Information Officer** — mandatory for all responsible parties                                                                                                                            |
| Special personal information       | POPIA requires prior authorization from the Information Regulator for processing special personal information (health, biometric, children's data). Assess whether your EDDI agents process such data |
| Breach notification                | Notify the Information Regulator and data subjects "as soon as reasonably possible"                                                                                                                   |
| Language requirements              | Provide privacy notices in at least one of South Africa's **11 official languages** relevant to your user base                                                                                        |
| Cross-border transfers             | Only to countries with adequate protection or with appropriate safeguards (binding corporate rules, consent)                                                                                          |

***

### PDPA — Southeast Asia

The **Personal Data Protection Act** applies in multiple Southeast Asian jurisdictions. Singapore's PDPA (2012, major amendments 2021), Thailand's PDPA (2019, effective 2022), and Malaysia's PDPA (2010, amended 2024) are the most mature.

#### Singapore PDPA

| Singapore PDPA Obligation          | EDDI Technical Capability                                             | Status      |
| ---------------------------------- | --------------------------------------------------------------------- | ----------- |
| Consent (Part 4)                   | Deployer integrates consent capture in their application layer        | 🏢 Your org |
| Purpose limitation (Part 4)        | Agent configuration documents purpose; deployer communicates to users | ✅ + 🏢      |
| Access obligation (Part 5)         | `GET /admin/gdpr/{userId}/export` — full data bundle                  | ✅ Built-in  |
| Correction obligation (Part 5)     | `PUT /usermemorystore/memories` — upserts individual entries          | ✅ Built-in  |
| Accuracy obligation (Part 4)       | Timestamped, versioned conversation state                             | ✅ Built-in  |
| Protection obligation (Part 5)     | AES-256-GCM, HMAC, Keycloak OIDC, RBAC                                | ✅ Built-in  |
| Retention limitation (Part 5)      | Configurable auto-cleanup + configurable idle conversation timeout    | ✅ Built-in  |
| Transfer limitation (Part 5)       | Provider data flows documented; deployer verifies transfer safeguards | ✅ + 🏢      |
| Data breach notification (Part 6A) | Incident response runbook template                                    | ✅ Built-in  |

**Deployer checklist (Singapore)**:

| Responsibility         | Details                                                                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Consent capture        | Singapore PDPA has a deemed consent framework for business improvement, but explicit consent is required for most AI processing            |
| DPO appointment        | Appoint a **Data Protection Officer** (DPO) — mandatory                                                                                    |
| Breach notification    | Notify the **PDPC** within **3 calendar days** of assessing a notifiable breach; notify affected individuals as soon as practicable        |
| DPIA for AI processing | Mandatory Data Protection Impact Assessment for high-risk AI processing. EDDI's audit ledger provides the technical evidence for your DPIA |

#### Thailand PDPA

Thailand's PDPA is structurally modeled on GDPR. EDDI's GDPR infrastructure covers all technical requirements. Key deployer responsibilities:

**Deployer checklist (Thailand)**:

| Responsibility             | Details                                                                                   |
| -------------------------- | ----------------------------------------------------------------------------------------- |
| DPO appointment            | Appoint a DPO if processing sensitive data or performing large-scale monitoring           |
| Breach notification        | Notify the **PDPC** (Personal Data Protection Committee) within **72 hours** of discovery |
| Cross-border transfers     | Require adequacy, appropriate safeguards, or consent                                      |
| Thai-language notices      | Provide privacy notices in **Thai**                                                       |
| Consent for sensitive data | Explicit consent required for sensitive personal data (health, biometrics, etc.)          |

#### Malaysia PDPA

Malaysia's **Personal Data Protection Act** (2010, significant amendments 2024) regulates the processing of personal data in commercial transactions. The 2024 amendments introduced mandatory breach notification, appointment of data protection officers, and cross-border data transfer mechanisms — aligning Malaysia more closely with GDPR.

| Malaysia PDPA Principle                   | EDDI Technical Capability                                                                                | Status      |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------- | ----------- |
| General Principle (lawful processing)     | Agent configuration documents processing purpose; deployer establishes lawful basis                      | ✅ + 🏢      |
| Notice and Choice Principle               | Deployer provides notice and obtains consent in their application layer                                  | 🏢 Your org |
| Disclosure Principle                      | LLM provider data flows documented in PRIVACY.md; audit trail records which provider processed each turn | ✅ Built-in  |
| Security Principle                        | AES-256-GCM vault encryption, HMAC-SHA256 audit integrity, Keycloak OIDC, RBAC, SSRF protection          | ✅ Built-in  |
| Retention Principle                       | Configurable auto-cleanup (`deleteEndedConversationsOnceOlderThanDays`) + idle conversation timeout      | ✅ Built-in  |
| Data Integrity Principle                  | Timestamped, versioned conversation state; user memories updatable via REST API                          | ✅ Built-in  |
| Access Principle                          | `GET /admin/gdpr/{userId}/export` — full data bundle in machine-readable JSON                            | ✅ Built-in  |
| Correction (2024 amendment)               | `PUT /usermemorystore/memories` — upserts individual entries                                             | ✅ Built-in  |
| Deletion (2024 amendment)                 | `DELETE /admin/gdpr/{userId}` — cascade deletion across all stores                                       | ✅ Built-in  |
| Data breach notification (2024 amendment) | Incident response runbook template (`docs/incident-response.md`)                                         | ✅ Built-in  |
| Cross-border transfer (2024 amendment)    | Provider data flows documented; deployer applies for approval or uses whitelisted countries              | ✅ + 🏢      |

**Deployer checklist (Malaysia)**:

| Responsibility          | Details                                                                                                                                                              |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Registration            | Register with the **Department of Personal Data Protection** (JPDP) if processing personal data                                                                      |
| DPO appointment         | Appoint a **Data Protection Officer** — mandatory under the 2024 amendments                                                                                          |
| Consent capture         | Obtain consent before processing; separate consent required for each purpose; consent must be in both **Bahasa Malaysia and English**                                |
| Breach notification     | Notify the **JPDP Commissioner** as soon as practicable (within **72 hours** under 2024 amendments); notify affected data subjects without undue delay               |
| Cross-border transfers  | The 2024 amendments introduced a whitelist model — data may only be transferred to countries specified by the Minister, or with explicit consent of the data subject |
| Sensitive personal data | Processing of sensitive data (health, political opinions, religion, criminal offences) requires **explicit consent** and additional safeguards                       |

***

### PIPL — China

China's **Personal Information Protection Law** (2021) is one of the world's strictest data privacy frameworks. It has strong extraterritorial reach, strict data localization requirements, and heavy penalties (up to 5% of annual revenue). PIPL is particularly relevant for EDDI deployments serving users in mainland China, as it imposes specific requirements on cross-border data transfers and AI-powered automated decision-making.

| PIPL Obligation                                     | EDDI Technical Capability                                                                                                       | Status      |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| Lawful basis for processing (Art. 13)               | Agent configuration documents purpose; deployer establishes legal basis (consent, contract, public interest, etc.)              | ✅ + 🏢      |
| Informed consent / separate consent (Art. 14, 29)   | Deployer integrates consent capture and separate consent UI for sensitive data, cross-border transfer, and public disclosure    | 🏢 Your org |
| Right to know (Art. 44)                             | Documented data processing in PRIVACY.md; agent configs describe purpose                                                        | ✅ + 🏢      |
| Right to access and copy (Art. 45)                  | `GET /admin/gdpr/{userId}/export` — full data bundle in structured JSON                                                         | ✅ Built-in  |
| Right to correction (Art. 46)                       | `PUT /usermemorystore/memories` — upserts individual entries                                                                    | ✅ Built-in  |
| Right to deletion (Art. 47)                         | `DELETE /admin/gdpr/{userId}` — cascade deletion across all stores + pseudonymization of audit trail                            | ✅ Built-in  |
| Right to refuse automated decision-making (Art. 24) | Deployer provides opt-out mechanism; EDDI's audit ledger records all AI decisions for transparency                              | ✅ + 🏢      |
| Data minimization (Art. 6)                          | Token-aware windowing limits data sent to LLMs; configurable retention auto-deletes old conversations                           | ✅ Built-in  |
| Security measures (Art. 51)                         | AES-256-GCM vault encryption, HMAC-SHA256 audit integrity, Keycloak OIDC, RBAC, SSRF protection, sandboxed evaluation           | ✅ Built-in  |
| Data breach notification (Art. 57)                  | Incident response runbook template (`docs/incident-response.md`)                                                                | ✅ Built-in  |
| Impact assessment for sensitive data (Art. 55)      | EDDI's audit ledger and data flow documentation provide evidence for Personal Information Protection Impact Assessments (PIIAs) | ✅ + 🏢      |
| Audit and record-keeping (Art. 54)                  | Immutable HMAC-signed audit ledger with full agent decision traceability                                                        | ✅ Built-in  |

**Deployer checklist (China)**:

| Responsibility                                    | Details                                                                                                                                                                                                                                                                                            |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Data localization                                 | PIPL Art. 40 requires **Critical Information Infrastructure Operators** (CIIOs) and processors exceeding volume thresholds to store personal information **within mainland China**. Use self-hosted LLM providers (Ollama, jlama) or Chinese cloud providers to avoid cross-border transfer issues |
| Cross-border data transfer                        | If personal information must leave China, complete one of: (1) **CAC security assessment** (mandatory for CIIOs or large volumes), (2) **PIPL standard contract** filed with provincial CAC, or (3) **Personal information protection certification** from an accredited body (Art. 38)            |
| Separate consent                                  | Obtain **separate, informed consent** for: cross-border transfers (Art. 39), sensitive personal information processing (Art. 29), public disclosure of personal information, and image/identity processing in public spaces                                                                        |
| Personal Information Protection Impact Assessment | Conduct PIIAs before: processing sensitive data, using personal data for automated decision-making, cross-border transfers, or any processing that significantly impacts individuals (Art. 55). EDDI's audit ledger and data flow docs provide the technical input                                 |
| DPO / representative appointment                  | Appoint a **Personal Information Protection Officer** if processing volume exceeds CAC thresholds. Foreign organizations processing Chinese residents' data must designate a **domestic representative** (Art. 53)                                                                                 |
| Breach notification                               | Notify the **Cyberspace Administration of China** (CAC) and affected individuals **immediately** upon discovering a breach. Notification must include: categories of data, cause, potential harm, and remediation measures (Art. 57)                                                               |
| Automated decision-making transparency            | PIPL Art. 24 requires transparency and fairness in automated decision-making. Provide users with an explanation of decision logic and an opt-out mechanism. Do **not** use automated decisions to impose unreasonable differential treatment on individuals                                        |
| LLM provider selection                            | Prefer providers with data centers in mainland China (e.g., Azure China, Alibaba Cloud, Baidu, local Ollama deployment) to avoid triggering cross-border transfer obligations                                                                                                                      |

> **⚠️ Important**: PIPL's cross-border data transfer regime is significantly stricter than GDPR's. Using cloud-hosted LLM providers (OpenAI, Anthropic, Google, etc.) with Chinese user data likely triggers cross-border transfer obligations. For China-targeted deployments, **self-hosted models via Ollama or jlama are strongly recommended** to keep all data within Chinese jurisdiction.

***

### Other Jurisdictions

For jurisdictions not listed above, EDDI's data protection infrastructure generally meets international standards. Key regions and their primary regulations:

| Region          | Regulation                                         | Key Notes                                                                                                           |
| --------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| **Australia**   | Privacy Act 1988 + APPs                            | 13 Australian Privacy Principles; notifiable data breach scheme; OAIC oversight                                     |
| **South Korea** | PIPA (Personal Information Protection Act)         | Strict consent requirements; mandatory DPO; 72-hour breach notification                                             |
| **India**       | DPDPA (Digital Personal Data Protection Act, 2023) | Consent-based framework; "significant data fiduciary" category; cross-border restrictions to blocked countries only |
| **EU/EEA**      | GDPR                                               | See [gdpr-compliance.md](/security-and-compliance/gdpr-compliance)                                                  |
| **USA (state)** | CCPA/CPRA (CA), VCDPA (VA), CPA (CO), etc.         | See CCPA section above                                                                                              |
| **UK**          | UK GDPR + Data Protection Act 2018                 | Substantially mirrors EU GDPR; ICO oversight                                                                        |

For all jurisdictions: deployers should consult local counsel to confirm organizational obligations specific to their region.

## Contact

For privacy-related inquiries about the EDDI platform, contact the project maintainers at [github.com/labsai/EDDI](https://github.com/labsai/EDDI).


# Agent Father: A Deep Dive

**Version: 6.2.0**

## Overview

The **Agent Father** is EDDI's meta-agent—an agent that creates other agents. It's the perfect example of EDDI's architecture in action, demonstrating how conversation flow, behavior rules, property extraction, and HTTP calls work together to build sophisticated workflows.

More importantly, it shows EDDI's unique capability: **the same architecture that powers simple agents can orchestrate complex, multi-step processes**, even self-modifying the system itself.

## What Makes Agent Father Special?

### It's Not Special Code

Agent Father is **not** a special feature or custom module. It's a **regular EDDI agent** built using the standard components:

* Behavior Rules (to control conversation flow)
* Property Extraction (to gather user input)
* HTTP Calls (to invoke EDDI's own API)
* Output Templates (to guide users)

### It Demonstrates Self-Modification

Agent Father uses EDDI's REST API to create new agents, packages, dictionaries, and configurations. This is possible because EDDI's API is designed to be **programmable**—you can automate agent creation just like any other API integration.

### It's a Conversational Wizard

Instead of requiring users to understand JSON configurations or API calls, Agent Father provides a **conversational interface** that:

1. Asks questions in natural language
2. Validates and stores answers
3. Builds complete agent configurations
4. Creates the agent via API
5. Returns the agent ID for deployment

## Architecture of Agent Father

### Agent Composition

Agent Father is composed of multiple packages:

```
Agent Father (.agent.json)
  ├─ Workflow 1: Core Conversation Flow
  │   ├─ Behavior Rules: Question sequencing
  │   ├─ Output Templates: Questions and responses
  │   └─ Properties: Store user answers
  │
  ├─ Workflow 2: Agent Creation Logic
  │   ├─ Behavior Rules: Trigger API calls when data is ready
  │   ├─ HTTP Calls: POST to /agentstore/agents
  │   └─ Properties: Extract agent ID from response
  │
  ├─ Workflow 3: Workflow Creation Logic
  │   ├─ HTTP Calls: POST to /packagestore/packages
  │   └─ Properties: Store package references
  │
  ├─ Workflow 4: Dictionary Creation Logic
  │   └─ HTTP Calls: POST to /regulardictionarystore/regulardictionaries
  │
  └─ Workflow 5: LangChain Configuration
      └─ HTTP Calls: POST to /langchainstore/langchains
```

## Step-by-Step Flow

Let's walk through how Agent Father creates a new agent:

### Step 1: Conversation Start

**User**: Starts conversation with Agent Father

**Agent Father**: (via Output Template)

```
"Welcome! I'll help you create a new agent. What would you like to call your agent?"
```

**Behavior Rule**:

```json
{
  "name": "Greeting",
  "conditions": [
    {
      "type": "occurrence",
      "configs": {
        "maxTimesOccurred": "0",
        "behaviorRuleName": "Greeting"
      }
    }
  ],
  "actions": ["greet_user"]
}
```

*(Triggers only on first step)*

### Step 2: Capture Agent Name

**User**: "My Weather Agent"

**Property Setter**: (from property extension)

```json
{
  "name": "agentName",
  "valueExtraction": "input",
  "scope": "conversation"
}
```

**Result**: Stores "My Weather Agent" in conversation memory:

```java
memory.getConversationProperties().put("context.agentName", "My Weather Agent");
```

**Agent Father**: "Great! What should your agent do? Describe its purpose."

### Step 3: Capture Agent Description

**User**: "It should tell users the current weather"

**Property Setter**:

```json
{
  "name": "agentDescription",
  "valueExtraction": "input",
  "scope": "conversation"
}
```

**Agent Father**: "Which AI provider would you like to use? (OpenAI, Claude, Gemini, or None)"

### Step 4: Capture LLM Choice

**User**: "OpenAI"

**Property Setter**:

```json
{
  "name": "llmProvider",
  "valueExtraction": "input",
  "scope": "conversation"
}
```

**Agent Father**: "Please provide your OpenAI API key."

### Step 5: Capture API Key

**User**: "sk-..."

**Property Setter**:

```json
{
  "name": "apiKey",
  "valueExtraction": "input",
  "scope": "conversation"
}
```

### Step 6: Trigger Agent Creation

Now all required data is collected. A Behavior Rule monitors the memory:

```json
{
  "name": "Create Agent When Ready",
  "conditions": [
    {
      "type": "contextmatcher",
      "configs": {
        "contextKey": "agentName",
        "contextType": "string"
      }
    },
    {
      "type": "contextmatcher",
      "configs": {
        "contextKey": "agentDescription",
        "contextType": "string"
      }
    },
    {
      "type": "contextmatcher",
      "configs": {
        "contextKey": "llmProvider",
        "contextType": "string"
      }
    },
    {
      "type": "contextmatcher",
      "configs": {
        "contextKey": "apiKey",
        "contextType": "string"
      }
    }
  ],
  "actions": ["httpcall(create-agent)"]
}
```

**Explanation**:

* This rule checks if all required data exists in memory
* When all conditions are met, it triggers the `httpcall(create-agent)` action
* This demonstrates **conditional API execution** based on conversation state

### Step 7: Execute HTTP Call to Create Agent

The `create-agent` HTTP call is defined in an HTTP Calls extension:

```json
{
  "targetServerUrl": "http://localhost:7070",
  "httpCalls": [
    {
      "name": "create-agent",
      "saveResponse": true,
      "responseObjectName": "newAgentResponse",
      "actions": ["httpcall(create-agent)"],
      "request": {
        "method": "POST",
        "path": "/agentstore/agents",
        "headers": {
          "Content-Type": "application/json"
        },
        "body": "{\"packages\": []}"
      },
      "postResponse": {
        "propertyInstructions": [
          {
            "name": "newAgentId",
            "fromObjectPath": "newAgentResponse.id",
            "scope": "conversation"
          }
        ]
      }
    }
  ]
}
```

**What Happens**:

1. **Request**: POST to `http://localhost:7070/agentstore/agents`
2. **Body**: Empty agent configuration (packages added later)
3. **Response**:

   ```json
   {
     "id": "673f1a2b4c5d6e7f8a9b0c1d",
     "version": 1,
     "packages": []
   }
   ```
4. **Property Extraction**: Saves agent ID to `context.newAgentId`

### Step 8: Create Workflow with LangChain Configuration

Another HTTP call creates a package:

```json
{
  "name": "create-package",
  "actions": ["httpcall(create-package)"],
  "request": {
    "method": "POST",
    "path": "/packagestore/packages",
    "body": "{\"packageExtensions\": [{\"type\": \"eddi://ai.labs.llm\", \"extensions\": {\"uri\": \"{memory.current.httpCalls.langchainConfigUri}\"}}]}"
  },
  "postResponse": {
    "propertyInstructions": [
      {
        "name": "workflowId",
        "fromObjectPath": "packageResponse.id",
        "scope": "conversation"
      }
    ]
  }
}
```

### Step 9: Create LangChain Configuration

```json
{
  "name": "create-langchain-config",
  "actions": ["httpcall(create-langchain-config)"],
  "request": {
    "method": "POST",
    "path": "/langchainstore/langchains",
    "body": "{\"tasks\": [{\"actions\": [\"send_message\"], \"type\": \"{context.llmProvider.toLowerCase()}\", \"parameters\": {\"apiKey\": \"{context.apiKey}\", \"modelName\": \"gpt-4o\", \"systemMessage\": \"{context.agentDescription}\", \"addToOutput\": \"true\"}}]}"
  }
}
```

**Note**: The body uses **Qute templating** to inject conversation memory values:

* `${context.llmProvider}` → "openai"
* `${context.apiKey}` → "sk-..."
* `${context.agentDescription}` → "It should tell users the current weather"

### Step 10: Link Workflow to Agent

```json
{
  "name": "update-agent-with-package",
  "actions": ["httpcall(update-agent)"],
  "request": {
    "method": "PUT",
    "path": "/agentstore/agents/{context.newAgentId}",
    "body": "{\"packages\": [\"eddi://ai.labs.package/packagestore/packages/{context.workflowId}?version=1\"]}"
  }
}
```

### Step 11: Deploy Agent

```json
{
  "name": "deploy-agent",
  "actions": ["httpcall(deploy-agent)"],
  "request": {
    "method": "POST",
    "path": "/administration/production/deploy/{context.newAgentId}",
    "queryParams": {
      "version": "1"
    }
  }
}
```

### Step 12: Confirmation

**Agent Father**: (via Output Template)

```
"Your agent has been created successfully!
Agent ID: {context.newAgentId}
You can start chatting with it at:
http://localhost:7070/chat/production/{context.newAgentId}"
```

## Key Architectural Insights

### 1. Conversation-Driven Workflows

Agent Father demonstrates that EDDI can orchestrate **any** multi-step process, not just conversations:

* Data collection (via conversation)
* Validation (via behavior rules)
* API orchestration (via HTTP calls)
* Response formatting (via output templates)

### 2. Conditional Execution

The behavior rule that triggers agent creation shows **conditional API execution**:

```
IF (all required data collected) THEN (create agent)
```

This is more sophisticated than simple API proxies—it's **business logic orchestration**.

### 3. Memory as State Machine

Conversation memory acts as a **state machine**:

* Initial state: No data collected
* Transition: User provides information → Property setters update state
* Trigger: All data present → Behavior rule fires
* Action: HTTP call executes

### 4. Template-Based Configuration

HTTP call bodies use Qute templates, allowing **dynamic configuration**:

```json
{
  "apiKey": "{context.apiKey}",
  "systemMessage": "{context.agentDescription}"
}
```

This means the same HTTP call definition can create different configurations based on conversation data.

### 5. Self-Modification

Agent Father calls EDDI's own API, demonstrating:

* **Programmable infrastructure**: Agents can modify the system
* **API-first design**: Everything is accessible via REST
* **Composability**: Agents are data, not code—they can be created programmatically

## Real-World Applications

The Agent Father pattern can be applied to many scenarios:

### 1. Customer Onboarding Wizard

```
Agent collects: Name, email, company, preferences
→ Creates CRM record via API
→ Sends welcome email via SendGrid API
→ Creates Slack channel via Slack API
```

### 2. Order Processing System

```
Agent collects: Product, quantity, shipping address
→ Validates inventory via ERP API
→ Processes payment via Stripe API
→ Creates shipping label via FedEx API
→ Sends confirmation via Twilio SMS API
```

### 3. Support Ticket Creation

```
Agent collects: Issue description, severity, attachments
→ Creates Jira ticket via Jira API
→ Notifies team via Slack API
→ Sends confirmation email via SendGrid API
```

### 4. Dynamic Agent Configuration

```
Agent collects: Customer requirements, industry, use case
→ Selects appropriate LLM (OpenAI for creative, Claude for analytical)
→ Configures behavior rules based on industry
→ Sets up integrations based on use case
→ Deploys customized agent
```

## Code Deep Dive

Let's look at the actual Java components that make Agent Father work:

### Behavior Rules Task (executes rules)

```java
public class BehaviorRulesTask implements ILifecycleTask {
    @Override
    public void execute(IConversationMemory memory, Object component) {
        // Load behavior rules from component
        BehaviorConfiguration config = (BehaviorConfiguration) component;

        // Evaluate each rule
        for (BehaviorRule rule : config.getBehaviorRules()) {
            boolean allConditionsMet = evaluateConditions(rule.getConditions(), memory);

            if (allConditionsMet) {
                // Store actions in memory for next task
                memory.getCurrentStep().storeData(
                    dataFactory.createData("actions", rule.getActions())
                );
                break;  // First match wins
            }
        }
    }
}
```

### API Calls Task (executes API calls)

```java
public class ApiCallsTask implements ILifecycleTask {
    @Override
    public void execute(IConversationMemory memory, Object component) {
        ApiCallsConfiguration config = (ApiCallsConfiguration) component;

        // Get actions from previous task (behavior rules)
        List<String> actions = memory.getCurrentStep()
            .getLatestData("actions").getResult();

        for (ApiCall apiCall : config.getHttpCalls()) {
            if (actions.contains("httpcall(" + apiCall.getName() + ")")) {
                // Execute HTTP call
                String url = config.getTargetServerUrl() + apiCall.getRequest().getPath();
                String body = applyTemplate(apiCall.getRequest().getBody(), memory);

                Response response = httpClient.post(url, body);

                // Store response in memory
                if (apiCall.isSaveResponse()) {
                    memory.getCurrentStep().storeData(
                        dataFactory.createData(
                            "httpCalls." + apiCall.getResponseObjectName(),
                            response.getBody()
                        )
                    );
                }

                // Extract properties from response
                for (PropertyInstruction instruction : apiCall.getPostResponse().getPropertyInstructions()) {
                    Object value = extractFromJsonPath(response.getBody(), instruction.getFromObjectPath());
                    memory.getConversationProperties().put(
                        "context." + instruction.getName(),
                        value
                    );
                }
            }
        }
    }
}
```

### Property Extraction Task

```java
public class PropertyExtractorTask implements ILifecycleTask {
    @Override
    public void execute(IConversationMemory memory, Object component) {
        PropertyConfiguration config = (PropertyConfiguration) component;

        for (PropertyInstruction instruction : config.getInstructions()) {
            if (instruction.getValueExtraction().equals("input")) {
                // Extract from user input
                String input = memory.getCurrentStep()
                    .getLatestData("input").getResult();

                // Store in appropriate scope
                if (instruction.getScope().equals("conversation")) {
                    memory.getConversationProperties().put(
                        "context." + instruction.getName(),
                        input
                    );
                }
            }
        }
    }
}
```

## Configuration Files

### Agent Father Agent Configuration

**File**: `agentfather.agent.json`

```json
{
  "packages": [
    "eddi://ai.labs.package/packagestore/packages/6740832b2b0f614abcaee7c8?version=1",
    "eddi://ai.labs.package/packagestore/packages/6740832a2b0f614abcaee79e?version=1",
    "eddi://ai.labs.package/packagestore/packages/6740832a2b0f614abcaee7a3?version=1",
    "eddi://ai.labs.package/packagestore/packages/6740832a2b0f614abcaee7a8?version=1",
    "eddi://ai.labs.package/packagestore/packages/6740832a2b0f614abcaee7ad?version=1"
  ]
}
```

### Workflow Configuration Example

**File**: `package-conversation-flow.package.json`

```json
{
  "packageExtensions": [
    {
      "type": "eddi://ai.labs.behavior",
      "extensions": {
        "uri": "eddi://ai.labs.behavior/behaviorstore/behaviorsets/6740832a2b0f614abcaee79f?version=1"
      },
      "config": {
        "appendActions": true
      }
    },
    {
      "type": "eddi://ai.labs.output",
      "extensions": {
        "uri": "eddi://ai.labs.output/outputstore/outputsets/6740832a2b0f614abcaee7a1?version=1"
      }
    },
    {
      "type": "eddi://ai.labs.property",
      "extensions": {
        "uri": "eddi://ai.labs.property/propertysetterstore/propertysetters/6740832a2b0f614abcaee7a2?version=1"
      }
    }
  ]
}
```

### Behavior Rules Example

**File**: `behavior-agent-creation.behavior.json`

```json
{
  "behaviorGroups": [
    {
      "name": "Agent Creation",
      "behaviorRules": [
        {
          "name": "Create Agent When Ready",
          "conditions": [
            {
              "type": "contextmatcher",
              "configs": {
                "contextKey": "agentName",
                "contextType": "string"
              }
            },
            {
              "type": "contextmatcher",
              "configs": {
                "contextKey": "agentDescription",
                "contextType": "string"
              }
            },
            {
              "type": "contextmatcher",
              "configs": {
                "contextKey": "llmProvider",
                "contextType": "string"
              }
            },
            {
              "type": "contextmatcher",
              "configs": {
                "contextKey": "apiKey",
                "contextType": "string"
              }
            }
          ],
          "actions": ["httpcall(create-agent)", "show_success_message"]
        }
      ]
    }
  ]
}
```

## Testing Agent Father

### Using the REST API

```bash
# 1. Start conversation with Agent Father
curl -X POST "http://localhost:7070/agents/agentfather/start" \
  -H "Content-Type: application/json" \
  -d '{"input": "I want to create an agent"}'

# Response includes conversationId
# {
#   "conversationId": "conv-123",
#   "conversationState": "READY",
#   "conversationOutputs": [
#     {"output": ["Welcome! What would you like to call your agent?"]}
#   ]
# }

# 2. Provide agent name
curl -X POST "http://localhost:7070/agents/conv-123" \
  -H "Content-Type: application/json" \
  -d '{"input": "Weather Agent"}'

# 3. Provide description
curl -X POST "http://localhost:7070/agents/conv-123" \
  -H "Content-Type: application/json" \
  -d '{"input": "Tells users the current weather"}'

# 4. Provide LLM choice
curl -X POST "http://localhost:7070/agents/conv-123" \
  -H "Content-Type: application/json" \
  -d '{"input": "OpenAI"}'

# 5. Provide API key
curl -X POST "http://localhost:7070/agents/conv-123" \
  -H "Content-Type: application/json" \
  -d '{"input": "sk-..."}'

# Agent Father will create the agent and return the agent ID
```

## Lessons from Agent Father

### 1. Configuration Over Code

Agent Father proves that complex workflows can be **configured**, not coded. No Java needed—just JSON.

### 2. Composability is Powerful

By combining simple components (rules, HTTP calls, templates), you can build sophisticated systems.

### 3. Conversations Are Workflows

Any multi-step process can be modeled as a conversation, making it user-friendly and intuitive.

### 4. EDDI is Infrastructure

EDDI isn't just for agents—it's infrastructure for **orchestrating any API-driven workflow** with conversational interfaces.

### 5. Self-Modification is Safe

Because agents are data (JSON), creating/modifying them via API is safe and version-controlled.

## Summary

The Agent Father demonstrates EDDI's core philosophy:

> **Sophisticated AI orchestration should be configuration, not code.**

By combining:

* **Behavior Rules** (decision logic)
* **Property Extraction** (state management)
* **HTTP Calls** (API orchestration)
* **Output Templates** (user interaction)

You can build systems that:

* Guide users through complex processes
* Collect and validate data conversationally
* Orchestrate multiple API calls conditionally
* Generate dynamic configurations
* Self-modify and adapt

This is the power of EDDI's architecture—and Agent Father is the proof.

## Related Documentation

* [Architecture Overview](/architecture-and-concepts/architecture) - Understanding EDDI's design
* [Conversation Memory](/architecture-and-concepts/conversation-memory) - How state is managed
* [Behavior Rules](/agent-configuration/behavior-rules) - Conditional logic
* [HTTP Calls](/agent-configuration/httpcalls) - API integration
* [Output Templating](/agent-configuration/output-templating) - Dynamic responses


# Agent Father: LangChain Tools Guide

## Quick Start

When creating a new connector agent via Agent Father, you'll now be asked three additional questions about LangChain task features:

### 1. Enable Built-in Tools

**Question:** "Would you like to enable built-in tools (calculator, websearch, datetime, weather, etc.) for this agent?"

**Options:**

* **Yes, enable tools** - Activates AI agent mode with access to built-in tools
* **No, just simple chat** - Keeps simple chat mode (default)

### 2. Tools Whitelist (only if tools enabled)

**Question:** "Which specific tools would you like to enable?"

**Quick Reply Options:**

* **Enable all tools** - Makes all 8 tools available
* **Calculator & Web Search** - Enables only `calculator` and `websearch`
* **Calculator, Web, DateTime** - Enables `calculator`, `websearch`, and `datetime`

**Manual Entry:** You can also type a custom JSON array:

```json
["calculator", "websearch", "datetime", "weather"]
```

### 3. Conversation History Limit

**Question:** "How many conversation turns would you like to include in the context?"

**Quick Reply Options:**

* **10 turns (recommended)** - Balances context and performance
* **20 turns** - More context for complex conversations
* **Unlimited (-1)** - All conversation history (may impact performance)

**Manual Entry:** Type any number:

* `-1` = unlimited history
* `0` = no history
* `10-20` = recommended range

***

## Available Built-in Tools

| Tool                | Identifier       | Description                                     | Example Use Case                     |
| ------------------- | ---------------- | ----------------------------------------------- | ------------------------------------ |
| **Calculator**      | `calculator`     | Safe math evaluation (sandboxed)                | "What's 15% tip on $84.50?"          |
| **Date/Time**       | `datetime`       | Get current date, time, timezone                | "What time is it in Tokyo?"          |
| **Web Search**      | `websearch`      | Search the web (Wikipedia, news)                | "What's the weather forecast today?" |
| **Data Formatter**  | `dataformatter`  | Format JSON, CSV, XML                           | "Convert this JSON to CSV"           |
| **Web Scraper**     | `webscraper`     | Extract content from web pages (SSRF-protected) | "Get the content from example.com"   |
| **Text Summarizer** | `textsummarizer` | Summarize long text                             | "Summarize this article"             |
| **PDF Reader**      | `pdfreader`      | Extract text from PDF URLs (SSRF-protected)     | "Read this PDF file"                 |
| **Weather**         | `weather`        | Get weather information                         | "What's the weather in Paris?"       |

***

## Tool Configuration (Server-Side)

Some tools require API keys or external configuration to function. These are configured via **Environment Variables** or `application.properties` on the EDDI server, not in the agent configuration.

### Web Search Tool

By default, the tool uses **DuckDuckGo** (HTML scraping), which requires no configuration.

To use **Google Custom Search** (more reliable/structured), configure these properties:

```properties
# In application.properties
eddi.tools.websearch.provider=google
eddi.tools.websearch.google.api-key=YOUR_GOOGLE_API_KEY
eddi.tools.websearch.google.cx=YOUR_CUSTOM_SEARCH_ENGINE_ID
```

**Docker Environment Variables:**

* `EDDI_TOOLS_WEBSEARCH_PROVIDER=google`
* `EDDI_TOOLS_WEBSEARCH_GOOGLE_API_KEY=...`
* `EDDI_TOOLS_WEBSEARCH_GOOGLE_CX=...`

### Weather Tool

The weather tool uses **OpenWeatherMap**. You must provide an API key:

```properties
# In application.properties
eddi.tools.weather.openweathermap.api-key=YOUR_OWM_API_KEY
```

**Docker Environment Variables:**

* `EDDI_TOOLS_WEATHER_OPENWEATHERMAP_API_KEY=...`

***

## Configuration Examples

### Example 1: Customer Support Agent with Tools

```json
{
  "enableBuiltInTools": true,
  "builtInToolsWhitelist": ["calculator", "websearch", "datetime"],
  "conversationHistoryLimit": 15
}
```

**Use Case:** Customer support agent that can calculate discounts, search for product info, and provide time-based responses.

### Example 2: Simple Chat Agent (No Tools)

```json
{
  "enableBuiltInTools": false,
  "builtInToolsWhitelist": [],
  "conversationHistoryLimit": 10
}
```

**Use Case:** Basic chat agent for general conversation without external tool access.

### Example 3: Research Assistant with All Tools

```json
{
  "enableBuiltInTools": true,
  "builtInToolsWhitelist": [],
  "conversationHistoryLimit": 20
}
```

**Use Case:** Research assistant with access to all tools and extended conversation memory.

### Example 4: Math Tutor (Calculator Only)

```json
{
  "enableBuiltInTools": true,
  "builtInToolsWhitelist": ["calculator"],
  "conversationHistoryLimit": 10
}
```

**Use Case:** Math tutor that can perform calculations but doesn't need web access.

***

## Tool Execution Pipeline

All tool invocations flow through a unified pipeline that provides enterprise-grade controls. These settings can be added to the `langchain.json` task configuration:

| Setting                    | Type    | Default   | Description                                                                                |
| -------------------------- | ------- | --------- | ------------------------------------------------------------------------------------------ |
| `enableRateLimiting`       | boolean | `true`    | Token-bucket rate limiting per tool                                                        |
| `defaultRateLimit`         | int     | `100`     | Default calls/minute for each tool                                                         |
| `toolRateLimits`           | map     | `{}`      | Per-tool overrides, e.g. `{"websearch": 30}`                                               |
| `enableToolCaching`        | boolean | `true`    | Cache identical tool calls                                                                 |
| `defaultToolCacheScope`    | string  | `user`    | Who may reuse a cached result: `user`, `conversation` or `global`                          |
| `toolCacheScopes`          | map     | `{}`      | Per-tool overrides, keyed on the dispatch name or the slug, e.g. `{"calculate": "global"}` |
| `enableCostTracking`       | boolean | `true`    | Track tool cost per conversation                                                           |
| `toolPricing`              | map     | built-ins | Per-call price in USD, e.g. `{"websearch": 0.005}`                                         |
| `maxBudgetPerConversation` | number  | unlimited | Ceiling on accumulated **tool** cost per conversation                                      |
| `enforceBudget`            | boolean | `false`   | Set `true` to actually refuse calls past the ceiling                                       |

### Example: Restrict web search rate and set a budget

```json
{
  "enableBuiltInTools": true,
  "builtInToolsWhitelist": ["calculator", "websearch"],
  "enableRateLimiting": true,
  "defaultRateLimit": 100,
  "toolRateLimits": { "websearch": 20 },
  "enableCostTracking": true,
  "maxBudgetPerConversation": 2.0,
  "enforceBudget": true
}
```

> A configured `maxBudgetPerConversation` is **report-only until you add `enforceBudget: true`.** Built-in tools priced at $0.00 before v6.1, so these ceilings have never refused a call; enforcing them automatically would start aborting tool calls on upgrade. If you relied on a ceiling that *was* binding (an http/MCP/A2A tool named `websearch`, `webscraper` or `pdfreader` was priced by name and refused), add the flag — the startup log names every task in that position. It covers **tool** cost only; LLM token spend is capped separately by the model cascade's `maxCostPerRun`.

### Which name does a setting expect?

Every built-in tool has a **slug** — the token you list in `builtInToolsWhitelist` (`websearch`) — and one or more **dispatch names**, the `@Tool` methods the model actually calls (`searchWeb`, `searchNews`, `searchWikipedia`). `toolRateLimits` and `toolPricing` accept either: the dispatch name is looked up first, then the slug, so a slug entry configures the whole tool while a dispatch-name entry pins one operation.

Rate-limit *buckets* stay per dispatch name. `{"websearch": 20}` therefore gives each of the three search operations its own 20 calls/minute, not 20 shared between them.

### Cache scoping

Tool results are cached **per user** by default: a cached result is only ever served back to the identity that produced it, so one user's tool output can never reach another. Widen a tool only when its result depends purely on its arguments and never on who is asking:

```json
{
  "enableToolCaching": true,
  "toolCacheScopes": { "calculate": "global" }
}
```

`conversation` narrows reuse further, to the single conversation that produced the entry. If no user id and no conversation id are available for a call, the cache is skipped entirely for it rather than falling back to a shared bucket.

***

## Security

Tools that accept URLs (PDF Reader, Web Scraper) are protected against **SSRF** (Server-Side Request Forgery):

* Only `http://` and `https://` URLs accepted
* Private / internal IP ranges blocked (127.x, 10.x, 192.168.x, 172.16-31.x)
* Cloud metadata endpoints blocked (169.254.169.254, metadata.google.internal)
* Internal hostnames rejected (localhost, \_.local, \_.internal)

The **Calculator** tool uses a sandboxed recursive-descent parser — no script engine, no code injection risk.

See the [Security documentation](/security-and-compliance/security) for full details.

***

## Best Practices

### When to Enable Tools

✅ **Enable tools when:**

* Agent needs to perform calculations
* Agent should search for current information
* Agent needs to access external data
* You want agentic behavior (autonomous tool use)

❌ **Keep tools disabled when:**

* Simple conversational agent
* Controlled, predictable responses needed
* Security/privacy concerns about external access
* Cost optimization (tools may increase API costs)

### Choosing History Limit

* **Short conversations (0-5 turns):** Use for FAQ agents or single-turn interactions
* **Medium conversations (10-15 turns):** Recommended for most use cases
* **Long conversations (20+ turns):** Use for complex problem-solving or tutoring
* **Unlimited (-1):** Only for special cases; may cause performance issues

### Tools Whitelist Strategy

1. **Start specific:** Begin with only the tools you need
2. **Add gradually:** Enable more tools as requirements grow
3. **Monitor usage:** Check which tools are actually being used
4. **Security first:** Only enable tools that match your security requirements

***

## Provider Support

All LLM providers in Agent Father now support these features:

| Provider         | Tools Support | History Limit | Notes                                      |
| ---------------- | ------------- | ------------- | ------------------------------------------ |
| OpenAI           | ✅             | ✅             | Best tool calling support                  |
| Anthropic/Claude | ✅             | ✅             | Requires `includeFirstAgentMessage: false` |
| Gemini           | ✅             | ✅             | Google AI Studio                           |
| Gemini Vertex    | ✅             | ✅             | Google Cloud Vertex AI                     |
| Hugging Face     | ✅             | ✅             | Model-dependent                            |
| Ollama           | ✅             | ✅             | Local models                               |
| Jlama            | ✅             | ✅             | Local Java-based models                    |

***

## Troubleshooting

### Tools Not Working

**Problem:** Agent doesn't use tools even when enabled **Solution:**

1. Verify `enableBuiltInTools` is set to `true`
2. Check the system message encourages tool use
3. Ensure the LLM model supports tool calling
4. Try with explicit tool-related prompts

### Too Many Tools Enabled

**Problem:** Agent is using unexpected tools **Solution:**

1. Use `builtInToolsWhitelist` to restrict tools
2. Adjust system message to guide tool usage
3. Review conversation logs to see tool invocations

### Performance Issues

**Problem:** Agent is slow or timing out **Solution:**

1. Reduce `conversationHistoryLimit` (try 5-10)
2. Reduce number of enabled tools
3. Increase API timeout in parameters
4. Use faster LLM model

### Context Length Errors

**Problem:** "Context length exceeded" errors **Solution:**

1. Lower `conversationHistoryLimit`
2. Use model with larger context window
3. Summarize conversation history periodically

***

## API Configuration Reference

When Agent Father creates the langchain configuration, it generates:

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "model-id",
      "type": "provider-type",
      "description": "Integration description",
      "parameters": {
        "systemMessage": "Your agent's system prompt",
        "addToOutput": "true",
        "apiKey": "your-api-key",
        "modelName": "model-name",
        "timeout": "15000",
        "temperature": "0.7",
        "logRequests": "true",
        "logResponses": "true"
      },
      "enableBuiltInTools": true,
      "builtInToolsWhitelist": ["calculator", "websearch"],
      "conversationHistoryLimit": 10
    }
  ]
}
```

***

## Next Steps

1. **Create a test agent** using Agent Father with tools enabled
2. **Experiment** with different tool combinations
3. **Monitor** agent behavior and tool usage
4. **Optimize** configuration based on actual usage
5. **Review** the [LangChain Integration Documentation](/agent-configuration/langchain) for advanced features

***

## Related Documentation

* [LangChain Integration](/agent-configuration/langchain) - Complete LangChain task documentation
* [Security](/security-and-compliance/security) - SSRF protection, sandboxed evaluation, tool hardening
* [Agent Father Deep Dive](/advanced-concepts/agent-father-deep-dive) - Agent Father architecture
* [Behavior Rules](/agent-configuration/behavior-rules) - Understanding behavior rules
* [Output Configuration](/agent-configuration/output-configuration) - Configuring agent outputs

***

**Last Updated:** March 2026\
**EDDI Version:** 6.2.0\
**Agent Father Version:** 3.0.1


# Agent Father: Conversation Flow

## Updated Conversation Flow (v3.0.1 with LangChain Tools)

```
┌─────────────────────────────────────────────────────────────────┐
│                    USER STARTS AGENT CREATION                     │
│              (e.g., "Create an OpenAI agent")                     │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  Step 1: Agent Name                                               │
│  ❓ "What would you like to name your agent?"                    │
│  💬 User: "My Assistant Agent"                                   │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  Step 2: Agent Intro Message                                      │
│  ❓ "What should be the intro message?"                        │
│  💬 User: "Hello! I'm your AI assistant."                     │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  Step 3: System Prompt                                          │
│  ❓ "What system prompt would you like to use?"                │
│  💬 User: "You are a helpful assistant"                       │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  Step 4: API Key                                                │
│  ❓ "Enter the API key you would like to use"                  │
│  💬 User: "sk-..."                                            │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  Step 5: Model Name                                             │
│  ❓ "What's the model name?"                                   │
│  💬 User: "gpt-4o"                                            │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  Step 6: Temperature                                            │
│  ❓ "What temperature would you like to set?"                  │
│  💬 User: "0.7"                                               │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  Step 7: Timeout                                                │
│  ❓ "What would you like to set for request timeout?"          │
│  💬 User: "15000"                                             │
└─────────────────────────────────────────────────────────────────┘
                              ↓
╔═════════════════════════════════════════════════════════════════╗
║                       🆕 NEW FEATURES                           ║
╚═════════════════════════════════════════════════════════════════╝
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  Step 8: Enable Built-in Tools                        🆕        │
│  ❓ "Would you like to enable built-in tools?"                 │
│                                                                 │
│  🔘 Yes, enable tools                                          │
│  🔘 No, just simple chat                                       │
└─────────────────────────────────────────────────────────────────┘
                              ↓
                    ┌─────────┴─────────┐
                    │                   │
            [if YES]│                   │[if NO]
                    ↓                   ↓
┌──────────────────────────────┐  ┌────────────────────────────┐
│  Step 9a: Tools Whitelist    │  │  Step 9b: Skip Tools       │
│                       🆕      │  │  (Set empty whitelist)     │
│  ❓ "Which tools to enable?" │  │                            │
│                              │  │  Properties set:           │
│  🔘 Enable all tools         │  │  • builtInToolsWhitelist:[]│
│  🔘 Calculator & Web Search  │  └────────────────────────────┘
│  🔘 Calculator, Web, DateTime│                 │
│  💬 ["calculator","datetime"]│                 │
└──────────────────────────────┘                 │
                    │                            │
                    └────────────┬───────────────┘
                                 ↓
┌─────────────────────────────────────────────────────────────────┐
│  Step 10: Conversation History Limit                    🆕      │
│  ❓ "How many conversation turns to include in context?"       │
│                                                                 │
│  🔘 10 turns (recommended)                                     │
│  🔘 20 turns                                                   │
│  🔘 Unlimited (-1)                                             │
│  💬 "15"                                                       │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  Step 11: Confirmation                                          │
│  ❓ "Continue with creating this connector agent?"               │
│                                                                 │
│  🔘 Create the agent!                                            │
│  🔘 Cancel this                                                │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                    AGENT CREATION PROCESS                         │
│  • Create behavior rules                                        │
│  • Create langchain config (with new params) 🆕                 │
│  • Create output set                                            │
│  • Create package                                               │
│  • Create agent                                                   │
│  • Deploy agent                                                   │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                     ✅ SUCCESS MESSAGE                          │
│  "It's all done! Your agent was successfully created!"            │
│  • Link to chat with agent                                        │
│  • Link to managed agent API                                      │
└─────────────────────────────────────────────────────────────────┘
```

***

## Quick Reply Options Summary

### Step 8: Enable Built-in Tools

| Button               | Value   | Description                      |
| -------------------- | ------- | -------------------------------- |
| Yes, enable tools    | `true`  | Enables AI agent mode with tools |
| No, just simple chat | `false` | Standard chat mode only          |

### Step 9a: Tools Whitelist (Conditional)

| Button                    | Value                                   | Result                |
| ------------------------- | --------------------------------------- | --------------------- |
| Enable all tools          | `[]`                                    | All 8 tools available |
| Calculator & Web Search   | `["calculator","websearch"]`            | Only 2 tools          |
| Calculator, Web, DateTime | `["calculator","websearch","datetime"]` | Only 3 tools          |

**Manual Entry Example:**

```json
["calculator", "websearch", "datetime", "weather"]
```

### Step 10: Conversation History Limit

| Button                 | Value | Context Size                   |
| ---------------------- | ----- | ------------------------------ |
| 10 turns (recommended) | `10`  | Last 10 conversation exchanges |
| 20 turns               | `20`  | Last 20 conversation exchanges |
| Unlimited              | `-1`  | All conversation history       |

***

## Conditional Flow Logic

### Tools Whitelist Decision

```
if (enableBuiltInTools === "true") {
    → Show "ask_for_tools_whitelist"
    → Set builtInToolsWhitelist from user input
} else {
    → Action: "skip_to_conversation_history"
    → Set builtInToolsWhitelist = []
}
```

### Implementation in Behavior Rules

```json
{
  "name": "Ask for tools whitelist",
  "actions": ["ask_for_tools_whitelist"],
  "conditions": [
    {
      "type": "dynamicvaluematcher",
      "configs": {
        "valuePath": "properties.enableBuiltInTools",
        "valueOperator": "equals",
        "value": "true"
      }
    }
  ]
}
```

***

## Generated Configuration Example

After completing all steps, Agent Father generates this langchain configuration:

```json
{
  "tasks": [
    {
      "actions": ["send_message"],
      "id": "openai",
      "type": "openai",
      "description": "Integration with OpenAI API",
      "parameters": {
        "systemMessage": "You are a helpful assistant",
        "addToOutput": "true",
        "apiKey": "sk-...",
        "modelName": "gpt-4o",
        "timeout": "15000",
        "temperature": "0.7",
        "logRequests": "true",
        "logResponses": "true"
      },
      "enableBuiltInTools": true, // 🆕 NEW
      "builtInToolsWhitelist": [
        // 🆕 NEW
        "calculator",
        "websearch",
        "datetime"
      ],
      "conversationHistoryLimit": 10 // 🆕 NEW
    }
  ]
}
```

***

## State Management

### Properties Set During Flow

| Step  | Property                   | Source                        | Scope        |
| ----- | -------------------------- | ----------------------------- | ------------ |
| 1     | `agentName`                | User input                    | conversation |
| 2     | `intro`                    | User input                    | conversation |
| 3     | `prompt`                   | User input                    | conversation |
| 4     | `apiKey`                   | User input                    | conversation |
| 5     | `modelName`                | User input                    | conversation |
| 6     | `temperature`              | User input                    | conversation |
| 7     | `timeout`                  | User input                    | conversation |
| 8 🆕  | `enableBuiltInTools`       | Quick reply / input           | conversation |
| 9 🆕  | `builtInToolsWhitelist`    | Quick reply / input / default | conversation |
| 10 🆕 | `conversationHistoryLimit` | Quick reply / input           | conversation |

***

## Error Handling & Validation

### User Input Validation

* **API Key:** No validation (passed as-is)
* **Model Name:** No validation (provider-specific)
* **Temperature:** Expected numeric string (0.0-1.0)
* **Timeout:** Expected numeric string (milliseconds)
* **enableBuiltInTools:** Must be "true" or "false"
* **builtInToolsWhitelist:** Must be valid JSON array or empty
* **conversationHistoryLimit:** Must be numeric (-1, 0, or positive)

### Quick Replies Ensure Valid Input

All critical fields have quick reply buttons to ensure valid values.

***

## User Experience Timeline

| Phase           | Steps        | Duration    | User Effort         |
| --------------- | ------------ | ----------- | ------------------- |
| Basic Config    | 1-7          | \~2 min     | Standard            |
| Tools Config 🆕 | 8-10         | +30 sec     | Low (quick replies) |
| Confirmation    | 11           | \~10 sec    | One click           |
| Creation        | Auto         | \~5 sec     | None (automated)    |
| **Total**       | **11 steps** | **\~3 min** | **Minimal**         |

***

## Comparison: Before vs After

### Before (v3.0.0)

* **Steps:** 8
* **Questions:** 7
* **Tools Support:** ❌
* **History Control:** ❌
* **Agent Mode:** ❌

### After (v3.0.1) 🆕

* **Steps:** 11
* **Questions:** 10
* **Tools Support:** ✅ (8 tools available)
* **History Control:** ✅ (flexible limits)
* **Agent Mode:** ✅ (conditional)

***

**Flow Version:** 3.0.1\
**Last Updated:** 2025\
**Applies to:** All 7 LLM providers


# Docker

## Quick Start

### Without Authentication (default)

```bash
docker-compose up
```

This starts EDDI on port `7070` and MongoDB. No login required.

### With Keycloak Authentication

The EDDI-Manager repo provides a full-stack docker-compose with Keycloak:

```bash
# From the EDDI-Manager repo
docker compose -f docker-compose.keycloak.yml up
```

This starts:

* **Keycloak 26** on port `8180` (admin console: `http://localhost:8180`, login `admin`/`admin`)
* **EDDI** on port `7070` with OIDC auth enabled
* **MongoDB** for data storage

Pre-configured test users:

| Username | Password | Role               |
| -------- | -------- | ------------------ |
| `eddi`   | `eddi`   | admin              |
| `viewer` | `viewer` | viewer (read-only) |

### Manual Docker Setup

Start MongoDB:

```bash
docker run --name mongodb -d mongo:6.0
```

Start EDDI (without auth):

```bash
docker run --name eddi --link mongodb:mongodb -p 7070:7070 -d labsai/eddi:latest
```

Start EDDI (with auth):

```bash
docker run --name eddi \
  --link mongodb:mongodb \
  -p 7070:7070 \
  -e QUARKUS_OIDC_TENANT_ENABLED=true \
  -e QUARKUS_OIDC_AUTH_SERVER_URL=http://your-keycloak:8080/realms/eddi \
  -e QUARKUS_OIDC_CLIENT_ID=eddi-backend \
  -d labsai/eddi:latest
```

## Environment Variables

### Authentication

| Variable                       | Default                             | Description                  |
| ------------------------------ | ----------------------------------- | ---------------------------- |
| `QUARKUS_OIDC_TENANT_ENABLED`  | `false`                             | Enable/disable Keycloak auth |
| `QUARKUS_OIDC_AUTH_SERVER_URL` | `http://localhost:8180/realms/eddi` | Keycloak realm URL           |
| `QUARKUS_OIDC_CLIENT_ID`       | `eddi-backend`                      | OIDC client ID               |
| `QUARKUS_HTTP_CORS_ORIGINS`    | `http://localhost:3000,...`         | Allowed CORS origins         |

> **Note:** `QUARKUS_OIDC_TENANT_ENABLED` is a **runtime** toggle. No rebuild needed to enable/disable auth.

### AI Tools

```bash
-e EDDI_TOOLS_WEBSEARCH_PROVIDER=google \
-e EDDI_TOOLS_WEBSEARCH_GOOGLE_API_KEY=your_key \
-e EDDI_TOOLS_WEBSEARCH_GOOGLE_CX=your_cx
```

```bash
-e EDDI_TOOLS_WEATHER_OPENWEATHERMAP_API_KEY=your_key
```

### Full Example

```bash
docker run --name eddi \
  --link mongodb:mongodb \
  -p 7070:7070 \
  -e QUARKUS_OIDC_TENANT_ENABLED=true \
  -e QUARKUS_OIDC_AUTH_SERVER_URL=http://keycloak:8080/realms/eddi \
  -e EDDI_TOOLS_WEBSEARCH_PROVIDER=google \
  -e EDDI_TOOLS_WEBSEARCH_GOOGLE_API_KEY=YOUR_KEY \
  -d labsai/eddi:latest
```


# Kubernetes

EDDI runs natively on Kubernetes. This guide covers deployment options from a simple quickstart to production-grade configurations.

## Prerequisites

* **Kubernetes cluster** (1.26+) — minikube, kind, GKE, EKS, AKS, or any conformant cluster
* **kubectl** configured to access your cluster
* **Helm 3** (optional, for Helm chart deployment)

## Quick Start (5 minutes)

### Option A: Single-file manifest

Deploy EDDI + MongoDB with one command:

```bash
kubectl apply -f https://raw.githubusercontent.com/labsai/EDDI/main/k8s/quickstart.yaml
```

Then generate and store a vault master key:

```bash
# Generate the secret
kubectl create secret generic eddi-secrets \
  --namespace=eddi \
  --from-literal=EDDI_VAULT_MASTER_KEY="$(openssl rand -base64 24)" \
  --dry-run=client -o yaml | kubectl apply -f -

# Restart EDDI to pick up the key
kubectl rollout restart deployment/eddi -n eddi

# Access EDDI
kubectl port-forward svc/eddi 7070:7070 -n eddi
```

Open <http://localhost:7070>.

### Option B: Using the helper script

```bash
# Clone the repo
git clone https://github.com/labsai/EDDI.git && cd EDDI

# Generate vault key + create K8s secret
bash k8s/create-secrets.sh

# Deploy with MongoDB
kubectl apply -k k8s/overlays/mongodb/
```

PowerShell:

```powershell
.\k8s\create-secrets.ps1
kubectl apply -k k8s\overlays\mongodb\
```

### Option C: Helm

```bash
helm install eddi ./helm/eddi \
  --set eddi.vaultMasterKey="$(openssl rand -base64 24)" \
  --namespace eddi --create-namespace
```

## Deployment Options

EDDI provides modular overlays (Kustomize) and Helm values for different deployment profiles:

### Database Backend

| Backend               | Kustomize                                 | Helm                                                                                        |
| --------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------- |
| **MongoDB** (default) | `kubectl apply -k k8s/overlays/mongodb/`  | `--set mongodb.enabled=true`                                                                |
| **PostgreSQL**        | `kubectl apply -k k8s/overlays/postgres/` | `--set postgres.enabled=true --set mongodb.enabled=false --set eddi.datastoreType=postgres` |

### Optional Components

The component overlays (auth, nats, monitoring, etc.) are designed to be **composed** with a database overlay. They do not include the base EDDI manifests on their own.

| Component          | Description                         | Helm Values                                                               |
| ------------------ | ----------------------------------- | ------------------------------------------------------------------------- |
| **Keycloak Auth**  | OIDC authentication                 | `--set keycloak.enabled=true --set eddi.oidc.enabled=true`                |
| **NATS JetStream** | Durable messaging for multi-replica | `--set nats.enabled=true --set eddi.messagingType=nats`                   |
| **Manager UI**     | Configuration dashboard             | `--set manager.enabled=true`                                              |
| **Monitoring**     | Prometheus + Grafana                | `--set monitoring.prometheus.enabled=true`                                |
| **Ingress**        | External HTTPS access               | `--set ingress.enabled=true --set ingress.hosts[0].host=eddi.example.com` |
| **Production**     | HPA, PDB, NetworkPolicy             | `--set autoscaling.enabled=true --set podDisruptionBudget.enabled=true`   |

### Composing Kustomize Overlays

Kustomize takes **one directory** as input. To combine components, create a `kustomization.yaml` that references multiple overlays:

```yaml
# my-deployment/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: eddi
resources:
  - ../k8s/overlays/mongodb                    # Base + MongoDB
  - ../k8s/overlays/auth/keycloak-deployment.yaml  # Keycloak
  - ../k8s/overlays/manager/manager-deployment.yaml # Manager UI
patches:
  - target: { kind: ConfigMap, name: eddi-config }
    patch: |
      - op: replace
        path: /data/QUARKUS_OIDC_TENANT_ENABLED
        value: "true"
```

Ready-made examples are provided in `k8s/examples/`:

```bash
# MongoDB + Auth + Monitoring + Manager
kubectl apply -k k8s/examples/mongodb-full/

# PostgreSQL + NATS + Production hardening
kubectl apply -k k8s/examples/postgres-ha/
```

## Architecture on Kubernetes

```
┌──────────────────────────────────────────────┐
│                  Ingress                      │
│            (nginx / traefik)                  │
└──────────────┬───────────────────────────────┘
               │
     ┌─────────▼──────────┐
     │    EDDI Service     │
     │   (ClusterIP:7070)  │
     └─────────┬──────────┘
               │
    ┌──────────▼──────────┐    ┌─────────────┐
    │  EDDI Deployment     │───▶│  MongoDB    │
    │  (labsai/eddi:latest) │    │ StatefulSet │
    │                      │    └─────────────┘
    │  replicas: 1-10      │    ┌─────────────┐
    │  (HPA auto-scales)   │───▶│ PostgreSQL  │
    └──────────────────────┘    │ StatefulSet │
               │                └─────────────┘
    ┌──────────▼──────────┐
    │   NATS JetStream     │  (optional, for multi-replica)
    │   StatefulSet        │
    └──────────────────────┘
```

## Security

### Vault Master Key

The vault master key encrypts all stored API keys and secrets. **If you lose this key, encrypted secrets are unrecoverable.**

Three ways to manage it:

1. **Helper script** (recommended for initial setup):

   ```bash
   bash k8s/create-secrets.sh
   ```
2. **Manual kubectl**:

   ```bash
   kubectl create secret generic eddi-secrets \
     --namespace=eddi \
     --from-literal=EDDI_VAULT_MASTER_KEY="$(openssl rand -base64 24)"
   ```
3. **External secrets** (production): Use [External Secrets Operator](https://external-secrets.io/) to sync from AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, etc.

### Pod Security

EDDI runs as non-root user (UID 185) and is compatible with `restricted` Pod Security Standards:

```yaml
securityContext:
  runAsNonRoot: true
  runAsUser: 185
  runAsGroup: 185
```

### Network Policy

The production overlay includes a `NetworkPolicy` that restricts EDDI to:

* **Ingress**: HTTP port 7070 from within the namespace + Ingress controllers
* **Egress**: Database (MongoDB/PG), NATS, Keycloak, DNS, and external HTTPS (port 443 for LLM APIs)

## Scaling

### Single Replica (default)

Default configuration uses in-memory messaging — suitable for development and low-traffic deployments.

### Multi-Replica (production)

For horizontal scaling, enable NATS JetStream for durable message ordering:

**Kustomize:**

```bash
# Use the ready-made HA example
kubectl apply -k k8s/examples/postgres-ha/
```

**Helm:**

```bash
helm install eddi ./helm/eddi \
  --set eddi.replicas=2 \
  --set nats.enabled=true \
  --set eddi.messagingType=nats \
  --set autoscaling.enabled=true \
  --namespace eddi --create-namespace
```

## Monitoring

EDDI exposes Prometheus metrics at `/q/metrics`. The EDDI Deployment includes Prometheus scrape annotations by default:

```yaml
annotations:
  prometheus.io/scrape: "true"
  prometheus.io/port: "7070"
  prometheus.io/path: "/q/metrics"
```

Deploy the monitoring stack using the full example or Helm:

```bash
# Kustomize (with MongoDB + Auth + Monitoring)
kubectl apply -k k8s/examples/mongodb-full/

# Helm
helm install eddi ./helm/eddi \
  --set monitoring.prometheus.enabled=true \
  --set monitoring.grafana.enabled=true

# Access Grafana
kubectl port-forward svc/grafana 3000:3000 -n eddi
# Open http://localhost:3000 (admin/admin)
```

## Health Checks

EDDI provides three probe endpoints:

| Endpoint          | Probe Type          | Purpose                         |
| ----------------- | ------------------- | ------------------------------- |
| `/q/health/live`  | Liveness            | Process is alive                |
| `/q/health/ready` | Readiness + Startup | DB connected, ready for traffic |
| `/q/metrics`      | —                   | Prometheus metrics              |

## File Structure

```
k8s/
├── base/                    # Core EDDI manifests
├── overlays/
│   ├── mongodb/             # MongoDB backend (standalone)
│   ├── postgres/            # PostgreSQL backend (standalone)
│   ├── nats/                # NATS JetStream (component)
│   ├── auth/                # Keycloak authentication (component)
│   ├── monitoring/          # Prometheus + Grafana (component)
│   ├── manager/             # Manager UI (component)
│   ├── ingress/             # Ingress resource (component)
│   └── production/          # HPA, PDB, NetworkPolicy (component)
├── examples/
│   ├── mongodb-full/        # MongoDB + Auth + Monitoring + Manager
│   └── postgres-ha/         # PostgreSQL + NATS + Production
├── create-secrets.sh        # Vault key generator (bash)
├── create-secrets.ps1       # Vault key generator (PowerShell)
└── quickstart.yaml          # All-in-one manifest

helm/
└── eddi/                    # Helm chart
    ├── Chart.yaml
    ├── values.yaml
    └── templates/
```

> **Note**: Overlays marked **(standalone)** include the base and can be applied directly with `kubectl apply -k`. Overlays marked **(component)** must be composed with a standalone overlay — see [Composing Kustomize Overlays](#composing-kustomize-overlays).

## Troubleshooting

### EDDI pod stuck in CrashLoopBackOff

Check if the database is reachable:

```bash
kubectl logs -n eddi deployment/eddi
kubectl get pods -n eddi
```

Common causes:

* MongoDB/PostgreSQL not yet ready (wait for StatefulSet pod)
* Incorrect connection string in ConfigMap
* Volume claims pending (check `kubectl get pvc -n eddi`)

### EDDI starts but readiness probe fails

Check the health endpoint:

```bash
kubectl exec -n eddi deployment/eddi -- curl -s localhost:7070/q/health/ready
```

### Vault key issues

If you see "vault master key not set" warnings, create the secret:

```bash
bash k8s/create-secrets.sh
kubectl rollout restart deployment/eddi -n eddi
```

### PVC stuck in Pending

If PVCs aren't provisioning, check your StorageClass:

```bash
kubectl get sc                    # List available StorageClasses
kubectl get pvc -n eddi           # Check PVC status
kubectl describe pvc -n eddi      # See events / errors
```

If your cluster doesn't have a default StorageClass, uncomment `storageClassName` in the StatefulSet manifests.


# RedHat OpenShift

## Platform Support

EDDI is built on and fully supports **Red Hat Enterprise Linux (RHEL)**. The production container image is based exclusively on Red Hat content:

* **Base OS**: [Red Hat Universal Base Image 9 (UBI 9)](https://catalog.redhat.com/software/base-images) — a freely redistributable subset of RHEL 9, binary-compatible with RHEL 9 and supported by Red Hat when run on RHEL or OpenShift.
* **Runtime**: OpenJDK 25 from the official Red Hat UBI 9 OpenJDK runtime image (`ubi9/openjdk-25-runtime`).
* **Architecture**: `linux/amd64` (x86\_64).
* **Non-root execution**: Runs as UID `185` (the default `jboss` user from the UBI base image) — containers never run as root.

EDDI is delivered as an OCI-compliant Docker container image and runs on any platform that supports OCI containers, including:

| Platform                               | Support Level                                                                        |
| -------------------------------------- | ------------------------------------------------------------------------------------ |
| **Red Hat Enterprise Linux 9**         | ✅ Primary — UBI 9 base image, Red Hat-certified                                      |
| **Red Hat OpenShift 4.12+**            | ✅ Certified — listed in the [Red Hat Ecosystem Catalog](https://catalog.redhat.com/) |
| **Docker** (any Linux, macOS, Windows) | ✅ Full support — standard OCI container                                              |
| **Kubernetes** (any distribution)      | ✅ Full support — standard OCI container                                              |
| **Podman**                             | ✅ Full support — OCI-compliant runtime                                               |

> **Note**: Because EDDI ships as a standard OCI container image built on Red Hat UBI 9, it is inherently compatible with RHEL 9 and any RHEL-based platform. No host-level OS dependencies are required beyond a container runtime.

All EDDI releases are continuously validated against Red Hat certification requirements via automated [preflight checks](https://github.com/redhat-openshift-ecosystem/openshift-preflight) in CI/CD.

***

## Red Hat Ecosystem Catalog

EDDI is listed in the [Red Hat Ecosystem Catalog](https://catalog.redhat.com/) as a certified container image, and is available on [Docker Hub](https://hub.docker.com/r/labsai/eddi):

🔗 [**hub.docker.com/r/labsai/eddi**](https://hub.docker.com/r/labsai/eddi)

***

## Container Certification

The EDDI container image is certified by Red Hat / IBM for use on OpenShift. Certification is automated via the [`redhat-certify.yml`](https://github.com/labsai/EDDI/tree/main/.github/workflows/redhat-certify.yml) GitHub Actions workflow.

### Certification Compliance

| Requirement            | Implementation                                                                                 |
| ---------------------- | ---------------------------------------------------------------------------------------------- |
| **Base image**         | `registry.access.redhat.com/ubi9/openjdk-25-runtime:1.24` (pinned by SHA256 digest)            |
| **Non-root execution** | Runs as UID `185` — the default `jboss` user                                                   |
| **Licenses**           | Auto-generated `/licenses` directory containing `THIRD-PARTY.txt` and downloaded license texts |
| **Required labels**    | `name`, `vendor`, `version`, `release`, `summary`, `description`                               |
| **OpenShift labels**   | `io.k8s.display-name`, `io.k8s.description`, `io.openshift.tags`                               |
| **Health check**       | Docker-native `HEALTHCHECK` on `/q/health/ready`                                               |
| **Security scanning**  | Trivy image scan in CI blocks push on OS-level CVEs                                            |

### Automated Certification Workflow

The certification release process is fully automated:

1. **Build** — `mvnw clean package -Plicense-gen` builds the application and auto-generates license files via the [MojoHaus license-maven-plugin](https://www.mojohaus.org/license-maven-plugin/)
2. **Docker build** — Builds the image with Red Hat certification labels (parameterized via `--build-arg`)
3. **Push** — Pushes to Docker Hub (or Quay.io when configured)
4. **Preflight** — Runs the [Red Hat preflight tool](https://github.com/redhat-openshift-ecosystem/openshift-preflight) to validate certification requirements
5. **Submit** — Optionally submits results to Red Hat Partner Connect for review

To trigger a certification release, go to **Actions → Red Hat Certification Release → Run workflow** and provide:

* `version` — EDDI version (e.g., `6.2.0`)
* `release` — Incremental release number (e.g., `1`, `2`, `3`)
* `submit` — Whether to submit results to Red Hat (`true`/`false`)
* `registry` — Target registry (`docker.io` or `quay.io`)

### Preflight Quality Gate

Every push to `main` or release tag that produces a Docker image is validated by a **preflight check** in CI. Pull requests also run a preflight dry-run. This catches certification regressions before they reach production (e.g., missing labels, license issues, prohibited packages).

### Required GitHub Secrets

| Secret                   | Purpose                                                  |
| ------------------------ | -------------------------------------------------------- |
| `REDHAT_API_TOKEN`       | Pyxis API token from Red Hat Partner Connect             |
| `REDHAT_CERT_PROJECT_ID` | Certification project ID                                 |
| `DOCKER_USERNAME`        | Docker Hub username                                      |
| `DOCKER_PASSWORD`        | Docker Hub password                                      |
| `QUAY_USERNAME`          | Quay.io robot account (optional, for Quay.io publishing) |
| `QUAY_PASSWORD`          | Quay.io password (optional)                              |

***

## License Automation

Third-party licenses are generated on-demand using the `license-gen` Maven profile:

```bash
./mvnw package -Plicense-gen -DskipTests
```

This generates:

| File                       | Contents                                          |
| -------------------------- | ------------------------------------------------- |
| `licenses/THIRD-PARTY.txt` | All runtime dependencies with their license names |
| `licenses/third-party/`    | Downloaded license text files for each dependency |
| `licenses/licenses.xml`    | Machine-readable license index                    |

The profile is **not activated during normal dev builds** to keep them fast. CI workflows (`redhat-certify.yml`, `ci.yml`) activate it automatically.

These files are **not committed to git** — they're generated fresh and accurate in every Docker image build.

***

## EDDI Operator for OpenShift

[![Docker Repository on Quay](https://quay.io/repository/labsai/eddi-operator/status)](https://quay.io/repository/labsai/eddi-operator)

### Prerequisites

* OpenShift 4.12+ deployment
* Block storage (preferably with a storage class)

### Installing from OperatorHub

1. Navigate to **Operators → OperatorHub** in the OpenShift Admin console
2. Search for "EDDI" and select the operator
3. Click **Install** — leave defaults (All Namespaces, Update Channel `alpha`, Approval Strategy `Automatic`)
4. Click **Subscribe**

### Creating an EDDI Instance

After installation, go to **Installed Operators → EDDI** and create a new instance:

```yaml
apiVersion: labs.ai/v1alpha1
kind: Eddi
metadata:
  name: eddi
spec:
  size: 1
  mongodb:
    environment: prod
    storageclass_name: managed-nfs-storage
    storage_size: 20G
```

The operator creates a route automatically. With the CR above, the route would be: `eddi-route-$NAMESPACE.apps.ocp.example.com`

> **Note**: The EDDI operator is being updated for v6 to support both MongoDB and PostgreSQL storage backends. Stay tuned for the updated operator release.

***

## Docker Image Details

| Property            | Value                                                     |
| ------------------- | --------------------------------------------------------- |
| **Image**           | `docker.io/labsai/eddi`                                   |
| **Base**            | `registry.access.redhat.com/ubi9/openjdk-25-runtime:1.24` |
| **Digest pinning**  | SHA256 digest for supply-chain integrity (OpenSSF Silver) |
| **User**            | `185` (non-root)                                          |
| **Port**            | `7070`                                                    |
| **Health endpoint** | `GET /q/health/ready`                                     |
| **Java**            | OpenJDK 25 (Red Hat build)                                |
| **Framework**       | Quarkus 3.34.x                                            |

### Quick Start

```bash
docker pull labsai/eddi:latest
docker run -i --rm -p 7070:7070 labsai/eddi
```

For production deployments with MongoDB:

```bash
docker run -d \
  -p 7070:7070 \
  -e QUARKUS_MONGODB_CONNECTION_STRING=mongodb://mongo:27017 \
  labsai/eddi:6.2.0
```


# Setting Up EDDI on AWS with MongoDB Atlas

This guide provides step-by-step instructions to set up EDDI on Amazon ECS and connect it to a MongoDB Atlas cluster.

## Prerequisites

1. **AWS Account**: Ensure you have an AWS account with the necessary permissions to create ECS clusters, task definitions, and IAM roles
2. **MongoDB Atlas Account**: Create an account on [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) if you don't have one

## Step 1: Set Up MongoDB Atlas

### 1. Create a MongoDB Atlas Cluster

1. **Sign Up / Log In**:
   * Go to [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) and log in
2. **Create a New Cluster**:
   * Click "Build a Cluster"
   * Choose AWS as the cloud provider and select a region
   * Choose the free tier (for development) or an appropriate plan for production purposes
   * Click "Create Cluster"
3. **Configure Cluster**:
   * After the cluster is created, click on "Connect"
   * Select "Connect Your Application"
   * Copy the connection string (e.g., `mongodb+srv://<user>:<password>@<host>/eddi?retryWrites=true&w=majority -Dmongodb.database=eddi`)

### 2. Create a Database User

1. **Add Database User**:
   * Navigate to "Database Access" under the "Security" tab
   * Click "Add New Database User"
   * Create a user with the required roles and note the username and password

### 3. Whitelist IP Addresses

1. **Network Access**:
   * Navigate to "Network Access" under the "Security" tab
   * Click "Add IP Address"
   * Add the IP addresses that need access, including your local machine and ECS IP range

## Step 2: Set Up Amazon ECS

### 1. Create a Task Definition

1. **Navigate to ECS**:
   * Go to the Amazon ECS console
   * Click "Task Definitions" and then "Create new Task Definition"
   * Select "FARGATE" as the launch type
2. **Configure Task Definition**:
   * Use the following JSON configuration:

```json
{
   "containerDefinitions": [
   {
   "name": "eddi",
   "image": "<image-id>.dkr.ecr.<region>.amazonaws.com/eddi:latest",
   "cpu": 1024,
   "memoryReservation": 2048,
   "portMappings": [
   {
   "containerPort": 7070,
   "hostPort": 7070,
   "protocol": "tcp"
   }
   ],
   "essential": true,
   "command": [
   "/bin/bash"
   ],
   "environment": [
   {
   "name": "JAVA_OPTS_APPEND",
   "value": "-Dmongodb.connectionString=mongodb+srv://<user>:<password>@<host>/eddi?retryWrites=true&w=majority -Dmongodb.database=eddi"
   }
   ],
   "mountPoints": [],
   "volumesFrom": [],
   "logConfiguration": {
   "logDriver": "awslogs",
   "options": {
   "awslogs-group": "eddi",
   "awslogs-region": "<region>",
   "awslogs-stream-prefix": "eddi"
   }
   },
   "healthCheck": {
   "command": [
   "CMD-SHELL",
   "curl -f http://localhost:7070/q/health || exit 1"
   ],
   "interval": 30,
   "timeout": 5,
   "retries": 3
   }
   }
   ],
   "networkMode": "awsvpc",
   "revision": 1,
   "volumes": [],
   "status": "ACTIVE",
   "requiresAttributes": [
   {
   "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
   },
   {
   "name": "com.amazonaws.ecs.capability.docker-remote-api.1.24"
   },
   {
   "name": "ecs.capability.execution-role-awslogs"
   },
   {
   "name": "com.amazonaws.ecs.capability.ecr-auth"
   },
   {
   "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
   },
   {
   "name": "com.amazonaws.ecs.capability.docker-remote-api.1.21"
   },
   {
   "name": "com.amazonaws.ecs.capability.task-iam-role"
   },
   {
   "name": "ecs.capability.container-health-check"
   },
   {
   "name": "ecs.capability.execution-role-ecr-pull"
   },
   {
   "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
   },
   {
   "name": "ecs.capability.task-eni"
   }
   ],
   "placementConstraints": [],
   "compatibilities": [
   "EC2",
   "FARGATE"
   ],
   "requiresCompatibilities": [
   "EC2",
   "FARGATE"
   ],
   "cpu": "1024",
   "memory": "2048"
}
```

### 2. Create an ECS Cluster

1. **Create Cluster**:
   * Navigate to "Clusters" and click "Create Cluster"
   * Choose "Networking only" (Fargate) and follow the prompts

### 3. Create a Service

1. **Create Service**:
   * Go to "Services" and click "Create"
   * Select your cluster and task definition
   * Configure the service with the desired number of tasks and networking settings

## Step 3: Connect EDDI to MongoDB Atlas

1. **Modify Application Configuration**:
   * Ensure that your EDDI application uses the MongoDB connection string from the environment variables
   * Update any necessary configuration files
2. **Deploy the Application**:
   * Deploy your EDDI application to ECS using the service created
3. **Test the Connection**:
   * Verify that the application connects to MongoDB Atlas by checking application logs and MongoDB Atlas metrics

## Security Considerations

1. **Encryption**:
   * Use TLS/SSL for encrypted connections (`ssl=true` in the connection string)
2. **IAM Roles**:
   * Assign IAM roles to ECS tasks to limit permissions
3. **Network Configuration**:
   * Place ECS tasks in private subnets and use a NAT gateway for internet access
   * Configure security groups for ECS tasks and MongoDB Atlas

***

By following these steps, you can set up EDDI on Amazon ECS and connect it to MongoDB Atlas securely and efficiently. If you encounter any issues or have further questions, please refer to the AWS and MongoDB Atlas documentation or contact support.


# Release & Versioning Strategy

> **Audience:** Maintainers, contributors, and CI/CD operators.

## Version Format

EDDI follows [Semantic Versioning](https://semver.org/):

```
MAJOR.MINOR.PATCH[-PRERELEASE]
```

| Component    | Meaning                           | Example                  |
| ------------ | --------------------------------- | ------------------------ |
| `MAJOR`      | Breaking API/config changes       | `6.0.0` → `7.0.0`        |
| `MINOR`      | New features, backward-compatible | `6.0.0` → `6.1.0`        |
| `PATCH`      | Bug fixes only                    | `6.0.0` → `6.0.1`        |
| `PRERELEASE` | Release candidate or beta         | `6.0.0-RC1`, `6.0.0-RC2` |

The canonical version lives in `pom.xml` (`<version>6.0.0</version>`) and is used for Maven artifacts and CI build tags.

***

## Branching Model

```
main ─────────────────────────────────────── production
  ↑
  │  merge when ready
  │
feature/version-6.0.0 ───────────────────── active development
```

| Branch                  | Purpose                    | Docker push?               |
| ----------------------- | -------------------------- | -------------------------- |
| `main`                  | Production-ready code      | ✅ Build tags on every push |
| `feature/version-X.Y.Z` | Active development branch  | ❌ No Docker push           |
| Pull requests → `main`  | Code review, CI validation | ❌ Tests + preflight only   |

***

## Docker Tag Strategy

All images are pushed to [Docker Hub: `labsai/eddi`](https://hub.docker.com/r/labsai/eddi).

| Trigger              | Docker Tags                                    | Purpose                                                               |
| -------------------- | ---------------------------------------------- | --------------------------------------------------------------------- |
| Push to `main`       | `labsai/eddi:6.0.0-b<N>`                       | Continuous integration build. `<N>` is the GitHub Actions run number. |
| Git tag `v6.0.0-RC1` | `labsai/eddi:6.0.0-RC1` + `labsai/eddi:latest` | Release candidate                                                     |
| Git tag `v6.0.0`     | `labsai/eddi:6.0.0` + `labsai/eddi:latest`     | General availability release                                          |

> **Key rule:** `latest` is **only** pushed on tag-based releases (RC or GA), never on regular main builds. This ensures `docker pull labsai/eddi` always gives users a deliberately released version.

### Build Tags

Every push to `main` produces a unique, immutable build tag:

```
labsai/eddi:6.0.0-b42
                  │  │
                  │  └── GitHub Actions run number (auto-incrementing)
                  └───── Version from pom.xml
```

These are useful for:

* Pinning deployments to a specific build
* Debugging issues ("which exact build is running?")
* Rolling back to a known-good build

***

## How to Release

### Release Candidate

```bash
# 1. Ensure feature branch is merged to main
git checkout main
git pull origin main

# 2. Tag the release candidate
git tag v6.0.0-RC1

# 3. Push the tag — CI pipeline triggers automatically
git push origin v6.0.0-RC1
```

This produces:

* `labsai/eddi:6.0.0-RC1` — the version-pinned tag
* `labsai/eddi:latest` — updated to point to this RC

### Subsequent Release Candidates

If RC1 needs fixes:

```bash
# 1. Fix on feature branch, merge to main
# 2. Tag the new main HEAD
git checkout main
git pull origin main
git tag v6.0.0-RC2
git push origin v6.0.0-RC2
```

### General Availability Release

```bash
git tag v6.0.0
git push origin v6.0.0
```

### Red Hat Certification Release

For Red Hat-certified images, use the separate workflow:

```
GitHub → Actions → "Red Hat Certification Release" → Run workflow
```

This builds, pushes, and submits the image to Red Hat's preflight certification system.

***

## Skipping Docker Builds

For documentation, config, or non-code commits, add `[skip docker]` to the commit message:

```bash
git commit -m "docs: update README [skip docker]"
```

This skips the Docker build and smoke test jobs, but **tests still run**.

| Commit message                         | Tests | Docker build | Smoke test |
| -------------------------------------- | ----- | ------------ | ---------- |
| `feat: add new API endpoint`           | ✅     | ✅            | ✅          |
| `docs: update changelog [skip docker]` | ✅     | ❌            | ❌          |
| Any tag push (`v6.0.0-RC1`)            | ✅     | ✅ (always)   | ✅          |

> `[skip docker]` is ignored on tag pushes — releases always build Docker images.

***

## CI/CD Pipeline

The entire pipeline lives in a single file: [`.github/workflows/ci.yml`](https://github.com/labsai/EDDI/blob/main/.github/workflows/ci.yml).

```
┌──────────────────┐
│  build-and-test  │  ← Always runs (push, PR, tag)
│  mvnw verify     │     Tests + JaCoCo coverage
└────────┬─────────┘
         │
    ┌────┴────┐
    │         │
    ▼         ▼
┌────────┐  ┌──────────────────┐
│ docker │  │ preflight-check  │  ← PRs only
│ build  │  │ Red Hat dry-run  │
│ + push │  └──────────────────┘
└────┬───┘
     │
     ▼
┌────────────┐
│ smoke-test │  ← Starts image + MongoDB, checks /q/health/ready
└────────────┘
```

### Job Details

| Job                 | Runs on                    | Condition               | Duration  |
| ------------------- | -------------------------- | ----------------------- | --------- |
| **build-and-test**  | Every push/PR/tag          | Always                  | \~3-5 min |
| **docker**          | Push to `main` or tag `v*` | `[skip docker]` to skip | \~3-4 min |
| **smoke-test**      | After `docker` succeeds    | Same as docker          | \~1-2 min |
| **preflight-check** | Pull requests only         | Always on PRs           | \~5-7 min |

### Secrets Required

Configure these in GitHub → Settings → Secrets → Actions:

| Secret                   | Purpose                                               |
| ------------------------ | ----------------------------------------------------- |
| `DOCKER_USERNAME`        | Docker Hub login                                      |
| `DOCKER_PASSWORD`        | Docker Hub access token                               |
| `REDHAT_API_TOKEN`       | Red Hat certification (only for `redhat-certify.yml`) |
| `REDHAT_CERT_PROJECT_ID` | Red Hat project ID (only for `redhat-certify.yml`)    |

***

## Local Preflight Check (Windows)

Run Red Hat certification checks locally without needing Linux:

```powershell
# Full build + label check + preflight
.\scripts\preflight-local.ps1

# Skip Maven/Docker build, use existing image
.\scripts\preflight-local.ps1 -SkipBuild

# Just verify Red Hat labels are present
.\scripts\preflight-local.ps1 -LabelsOnly
```

Requires Docker Desktop for Windows. The `preflight` tool runs inside a Docker container — no WSL needed.

***

## Version Lifecycle

```
Development             Release Candidates          General Availability
─────────────────       ──────────────────          ────────────────────
feature/version-6.0.0   v6.0.0-RC1                  v6.0.0
    │                       │                           │
    ├── merge to main       ├── tag → Docker push       ├── tag → Docker push
    │   → 6.0.0-b1          │   → 6.0.0-RC1 + latest    │   → 6.0.0 + latest
    ├── merge to main       │                           │
    │   → 6.0.0-b2         v6.0.0-RC2                  │
    ├── merge to main       │                           └── start v6.1.0 cycle
    │   → 6.0.0-b3          └── 6.0.0-RC2 + latest
    └── ...
```

### After a GA Release

After tagging `v6.0.0`, update `pom.xml` on the feature branch to the next version:

```bash
# On feature/version-6.1.0 (or rename the branch)
# Update pom.xml: <version>6.1.0</version>
# CI builds will now produce 6.1.0-b1, 6.1.0-b2, etc.
```

***

## Release Signing

All Docker images pushed by CI are **cryptographically signed** using [Sigstore cosign](https://github.com/sigstore/cosign) with keyless OIDC signing. This ensures that users can verify any image was built by the official `labsai/EDDI` GitHub Actions pipeline.

For full details on how signing works and how to verify images, see [Release Signing & Verification](/deployment-and-infrastructure/release-signing).

### Signed Git Tags

When creating release tags, use signed tags:

```bash
# Instead of: git tag v6.0.0
# Use:
git tag -s v6.0.0 -m "Release 6.0.0"
git push origin v6.0.0
```


# Release Signing & Verification

> **Audience:** Users, operators, and security auditors who need to verify the integrity of EDDI releases.

## What Is Signed?

EDDI's primary release artifacts are **Docker images** published to [Docker Hub: `labsai/eddi`](https://hub.docker.com/r/labsai/eddi). Starting with v6.0.0, every image pushed by the CI/CD pipeline is **cryptographically signed** using [Sigstore cosign](https://github.com/sigstore/cosign) with keyless OIDC signing.

This includes all images pushed after signing was enabled:

* Every build pushed from `main` (e.g., `labsai/eddi:6.0.0-b42`)
* Every release candidate (e.g., `labsai/eddi:6.0.0-RC2`)
* Every general availability release (e.g., `labsai/eddi:6.0.0`)
* The `latest` tag (updated on release tag pushes)

> **Note:** Images published before v6.0.0 are not signed. Signature verification only applies to images built after this feature was enabled.

***

## How Signing Works

EDDI uses **keyless signing** — there are no long-lived private keys to manage or protect:

1. The GitHub Actions CI pipeline builds and pushes the Docker image
2. GitHub provides an **OIDC identity token** proving the workflow identity
3. **Fulcio** (Sigstore's certificate authority) issues a short-lived certificate based on that identity
4. **cosign** signs the image using the ephemeral certificate
5. The signature is stored as an **OCI artifact** alongside the image in Docker Hub
6. The signing event is recorded in the **Rekor** public transparency log

```
GitHub Actions OIDC Token
        │
        ▼
   ┌─────────┐     ┌────────────┐
   │  Fulcio  │────▶│  Ephemeral │
   │   (CA)   │     │   Cert     │
   └─────────┘     └─────┬──────┘
                         │
                         ▼
                  ┌──────────────┐
                  │  cosign sign │──▶ Signature stored in Docker Hub
                  └──────┬───────┘
                         │
                         ▼
                  ┌──────────────┐
                  │    Rekor     │──▶ Transparency log entry
                  │  (public)    │
                  └──────────────┘
```

### Security Properties

| Property                                 | How it's achieved                                                                     |
| ---------------------------------------- | ------------------------------------------------------------------------------------- |
| **No private key exposure**              | Ephemeral keys exist only in runner memory for milliseconds — never stored anywhere   |
| **Tamper evidence**                      | Signatures are recorded in the immutable Rekor transparency log                       |
| **Identity binding**                     | The signature proves the image was built by the `labsai/EDDI` GitHub Actions workflow |
| **Private key not on distribution site** | Docker Hub only stores the signature and public certificate, never a private key      |

***

## How to Verify

### Prerequisites

Install cosign:

```bash
# macOS
brew install cosign

# Linux (download binary)
curl -LO https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign

# Or download from https://github.com/sigstore/cosign/releases
```

### Verify an Image

```bash
cosign verify \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp "^https://github\.com/labsai/EDDI/\.github/workflows/ci\.yml@refs/(heads/main|tags/.+)$" \
  labsai/eddi:6.0.0
```

Replace `6.0.0` with any tag you want to verify (`latest`, `6.0.0-RC2`, `6.0.0-b42`, etc.).

**Successful output** will show the verified certificate chain and Rekor log entry:

```
Verification for docker.io/labsai/eddi:6.0.0 --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - Existence of the claims in the transparency log was verified offline
  - The code-signing certificate was verified using trusted certificate authority
```

### Verify by Digest (Recommended)

For maximum security, verify by image digest instead of tag:

```bash
# Get the digest
docker pull labsai/eddi:6.0.0
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' labsai/eddi:6.0.0)

# Verify the digest
cosign verify \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp "^https://github\.com/labsai/EDDI/\.github/workflows/ci\.yml@refs/(heads/main|tags/.+)$" \
  $DIGEST
```

### Inspect the Transparency Log

Every signature is publicly recorded in [Rekor](https://rekor.sigstore.dev/). When you run `cosign verify`, the output includes the Rekor log index. You can also inspect the full transparency log entry for a signed image:

```bash
cosign verify \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp "^https://github\.com/labsai/EDDI/\.github/workflows/ci\.yml@refs/(heads/main|tags/.+)$" \
  --output-text \
  labsai/eddi:6.0.0
```

This outputs the full certificate chain and Rekor log entry as JSON.

***

## Git Tag Signing

For version tags in the Git repository (e.g., `v6.0.0`, `v6.0.0-RC2`), maintainers sign tags using GPG or SSH keys:

```bash
# Create a signed tag
git tag -s v6.0.0 -m "Release 6.0.0"
git push origin v6.0.0

# Verify a signed tag
git tag -v v6.0.0
```

> **Note:** The primary release integrity guarantee is provided by the Docker image signing described above. Git tag signing provides an additional layer of assurance that the tag was created by an authorized maintainer.

***

## Related Documentation

* [Release & Versioning Strategy](/deployment-and-infrastructure/release-versioning) — Docker tags, branching model, how to release
* [Security Policy](https://github.com/labsai/EDDI/blob/main/SECURITY.md) — Vulnerability reporting, scope, security practices
* [CI/CD Pipeline](https://github.com/labsai/EDDI/blob/main/.github/workflows/ci.yml) — The signing implementation


# Metrics & Monitoring

E.D.D.I exposes comprehensive metrics via [Micrometer](https://micrometer.io/) in Prometheus format, covering conversations, tool execution, caching, rate limiting, cost tracking, multi-agent group discussions, scheduled triggers, tenant quotas, audit integrity, and JVM internals.

## Quick Start — Grafana Dashboard

E.D.D.I ships with a pre-built **Operations Command Center** dashboard (45 panels, 9 rows) that auto-provisions into Grafana.

### Enable Monitoring

```bash
# Docker Compose
docker compose -f docker-compose.yml -f docker-compose.monitoring.yml up -d

# Or via the install wizard
./install.sh --with-monitoring     # Linux / macOS
./install.ps1 -WithMonitoring      # Windows
```

| Service    | URL                               | Credentials   |
| ---------- | --------------------------------- | ------------- |
| Grafana    | <http://localhost:3000>           | admin / admin |
| Prometheus | <http://localhost:9090>           | —             |
| Metrics    | <http://localhost:7070/q/metrics> | —             |

The dashboard appears automatically as the Grafana home page. Anonymous viewer access is enabled by default.

### Dashboard Sections

| Row           | Title                            | Key Panels                                                                                                    |
| ------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **KPI Strip** | *(always visible)*               | Uptime, Agents Deployed, Active Conversations, Messages/sec, Tool Success %, Cache Hit %, Error Rate, Cost/hr |
| **Row 1**     | Platform Overview & HTTP Traffic | Request rate by status (2xx/4xx/5xx), latency P50/P95/P99, CPU usage, top 10 slowest endpoints                |
| **Row 2**     | Conversations                    | Start/end/processing rate, processing duration percentiles, active gauge, undo/redo, start vs load latency    |
| **Row 3**     | Tool Execution Engine            | Success vs failure rate, per-tool execution duration, cached/rate-limited breakdown, per-tool call counts     |
| **Row 4**     | Tool Cache Performance           | Hit rate %, hits vs misses, cache size, get/put duration                                                      |
| **Row 5**     | Rate Limiting & Cost             | Allowed vs denied, denied by tool, total cost, budget exceeded events, cost accumulation, cost by tool        |
| **Row 6**     | Multi-Agent Group Discussions    | Started vs failed, failure rate gauge, discussion duration                                                    |
| **Row 7**     | Scheduled Triggers               | Poll/fire/failed, fire duration, claim conflicts, dead-lettered                                               |
| **Row 8**     | Tenant Quotas & Audit            | Quota allowed vs denied, denied by type, audit entries dropped, tenant usage                                  |
| **Row 9**     | JVM & Infrastructure             | Heap/non-heap memory, threads, GC, MongoDB pool, PostgreSQL Agroal pool, NATS messaging                       |

> **Database-agnostic**: Row 9 includes panels for both MongoDB (`mongodb_driver_pool_*`) and PostgreSQL (`agroal_*`). Whichever backend is active shows data; the other gracefully shows "No data".

***

## Metrics Reference

All metrics are accessible at `/q/metrics`. Micrometer uses **dot notation** (e.g., `eddi.tool.cache.hits`); Prometheus automatically converts to **underscore notation** with `_total` suffix for counters (e.g., `eddi_tool_cache_hits_total`).

### Conversation Metrics

```
eddi_conversation_start_count_total         # Conversations started
eddi_conversation_end_count_total           # Conversations ended
eddi_conversation_processing_count_total    # Messages processed
eddi_conversation_load_count_total          # Conversations loaded from DB
eddi_conversation_undo_count_total          # Undo operations
eddi_conversation_redo_count_total          # Redo operations
eddi_processing_conversation_count          # Currently active (gauge)

eddi_conversation_start_duration_seconds    # Start latency (timer)
eddi_conversation_end_duration_seconds      # End latency (timer)
eddi_conversation_load_duration_seconds     # Load latency (timer)
eddi_conversation_processing_duration_seconds  # Processing latency (timer)
eddi_conversation_undo_duration_seconds     # Undo latency (timer)
eddi_conversation_redo_duration_seconds     # Redo latency (timer)
```

### Tool Execution Metrics

```
eddi_tool_execution_success_total           # Successful executions
eddi_tool_execution_failure_total           # Failed executions
eddi_tool_execution_cached_total            # Cache-served executions
eddi_tool_execution_ratelimited_total       # Rate-limited executions
eddi_tool_execution_duration_seconds        # Execution duration (timer)
```

All execution metrics support a `tool` label for per-tool breakdown:

```promql
rate(eddi_tool_execution_success_total{tool="weather"}[5m])
```

### Tool Cache Metrics

```
eddi_tool_cache_hits_total                  # Cache hits
eddi_tool_cache_misses_total                # Cache misses
eddi_tool_cache_puts_total                  # Cache puts (per tool)
eddi_tool_cache_get_duration_seconds        # Get latency (timer)
eddi_tool_cache_put_duration_seconds        # Put latency (timer)
eddi_tool_cache_size                        # Current entries (gauge)
eddi_tool_cache_bypassed_total              # Calls that skipped the cache (per tool)
```

> `eddi_tool_cache_bypassed_total` counts tool calls where caching was enabled but no identity could be derived to scope the entry to, so the cache was skipped on both the read and the write side. A sustained non-zero rate means tool calls are running without a user id **or** a conversation id and are paying full tool cost every time — investigate the caller rather than widening the cache scope.

### Rate Limiting Metrics

```
eddi_tool_ratelimit_allowed_total{tool="..."}  # Allowed calls (per tool)
eddi_tool_ratelimit_denied_total{tool="..."}   # Denied calls (per tool)
eddi_tool_ratelimit_remaining                  # Remaining capacity (gauge)
```

Per-tool and aggregate queries:

```promql
# Per-tool denied rate
rate(eddi_tool_ratelimit_denied_total{tool="weather"}[5m])

# Aggregate allowed rate across all tools
sum(rate(eddi_tool_ratelimit_allowed_total[5m]))
```

### Cost Tracking Metrics

```
eddi_tool_calls_total                       # Total tool calls
eddi_tool_costs_total                       # Total cumulative cost (gauge)
eddi_tool_budget_exceeded_total             # Budget exceeded events
```

Per-tool breakdown:

```
eddi_tool_calls{tool="weather"}             # Calls per tool
eddi_tool_costs{tool="weather"}             # Cost per tool
```

### Group Discussion Metrics

```
eddi_group_discussion_count_total           # Discussions started
eddi_group_discussion_failure_count_total   # Discussions failed
eddi_group_discussion_duration_seconds      # Duration (timer)
```

### Scheduled Trigger Metrics

```
eddi_schedule_poll_count_total              # Poll cycles
eddi_schedule_fire_count_total              # Schedules fired
eddi_schedule_fire_failed_total             # Fire failures
eddi_schedule_claim_conflict_total          # Claim conflicts (multi-instance)
eddi_schedule_fire_deadlettered_total       # Dead-lettered schedules
eddi_schedule_fire_duration_seconds         # Fire latency (timer)
```

### Tenant Quota Metrics

```
eddi_tenant_quota_allowed_total             # Quota checks passed
eddi_tenant_quota_denied_total              # Quota checks denied
eddi_tenant_usage_conversations_total       # Conversation usage (per tenant)
eddi_tenant_usage_api_calls_total           # API call usage (per tenant)
eddi_tenant_usage_cost_total                # Cost usage (per tenant)
```

Quota denied counters include `type` and `tenant` labels:

```promql
rate(eddi_tenant_quota_denied_total{type="cost", tenant="acme"}[5m])
```

### Audit Ledger Metrics

```
eddi_audit_entries_dropped_total            # Audit entries dropped (compliance-critical)
```

### Deployed Agents

```
eddi_agents_deployed                        # Currently deployed agents (gauge)
```

### NATS Messaging Metrics

> Only active when using the NATS messaging profile. Shows nothing under in-memory messaging.

```
eddi_nats_publish_count_total               # Messages published
eddi_nats_consume_count_total               # Messages consumed
eddi_nats_dead_letter_count_total           # Dead letters
eddi_nats_publish_duration_seconds          # Publish latency (timer)
eddi_nats_consume_duration_seconds          # Consume latency (timer)
```

### JVM & HTTP Server (auto-exposed)

Standard Micrometer metrics for Quarkus:

```
jvm_memory_used_bytes{area="heap|nonheap"}
jvm_memory_committed_bytes{area="heap|nonheap"}
jvm_memory_max_bytes{area="heap"}
jvm_threads_live_threads
jvm_threads_daemon_threads
jvm_threads_peak_threads
jvm_gc_pause_seconds{action="..."}
process_uptime_seconds
process_cpu_usage
system_cpu_usage
http_server_requests_seconds{method,uri,status}
```

### Database Connection Pool (auto-exposed)

**MongoDB** (when `eddi.datastore.type=mongo`):

```
mongodb_driver_pool_size
mongodb_driver_pool_checkedout
mongodb_driver_pool_waitqueuesize
```

**PostgreSQL / Agroal** (when `eddi.datastore.type=postgres`):

```
agroal_active_count
agroal_available_count
agroal_awaiting_count
agroal_max_used_count
```

***

## Prometheus Alerts

### Sample Alert Rules

```yaml
groups:
  - name: eddi_alerts
    rules:
      # Critical
      - alert: ToolSystemDown
        expr: rate(eddi_tool_execution_success_total[5m]) == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "No successful tool executions in 2 minutes"

      - alert: BudgetExceeded
        expr: eddi_tool_costs_total > 10
        labels:
          severity: critical
        annotations:
          summary: "Total tool costs exceeded $10"

      - alert: AuditEntriesDropped
        expr: eddi_audit_entries_dropped_total > 0
        labels:
          severity: critical
        annotations:
          summary: "Audit entries are being dropped — compliance risk"

      # Warning
      - alert: HighToolFailureRate
        expr: >
          rate(eddi_tool_execution_failure_total[5m]) /
          (rate(eddi_tool_execution_success_total[5m]) +
           rate(eddi_tool_execution_failure_total[5m])) > 0.05
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Tool failure rate above 5%"

      - alert: CacheDegraded
        expr: >
          sum(rate(eddi_tool_cache_hits_total[5m])) /
          (sum(rate(eddi_tool_cache_hits_total[5m])) +
           sum(rate(eddi_tool_cache_misses_total[5m]))) < 0.5
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Cache hit rate below 50%"

      - alert: HighRateLimitDenials
        expr: rate(eddi_tool_ratelimit_denied_total[5m]) > 5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High rate limit denials: {{ $value }}/sec"

      - alert: ScheduleDeadLetters
        expr: eddi_schedule_fire_deadlettered_total > 0
        labels:
          severity: warning
        annotations:
          summary: "Dead-lettered schedules detected"
```

***

## REST API Endpoints

EDDI also exposes tool metrics via REST:

```bash
# Cache stats
GET /langchain/tools/cache/stats

# Rate limit info
GET /langchain/tools/ratelimit/{toolName}

# Cost tracking
GET /langchain/tools/costs
GET /langchain/tools/costs/conversation/{conversationId}
GET /langchain/tools/costs/tool/{toolName}

# Tool history
GET /langchain/tools/history/{conversationId}
```

***

## Monitoring Best Practices

### Key Metrics to Watch

| Metric                | Target   | Why                                                                 |
| --------------------- | -------- | ------------------------------------------------------------------- |
| Cache Hit Rate        | > 70%    | Below this, tool calls are mostly un-cached → higher latency & cost |
| Tool Success Rate     | > 95%    | Dropping below indicates tool integration issues                    |
| P95 Latency           | < 2s     | Conversation responsiveness depends on tool speed                   |
| Cost Per Request      | < $0.001 | Runaway costs indicate misconfigured tools or abuse                 |
| Audit Drops           | = 0      | Any non-zero value is a compliance incident                         |
| Error Rate (HTTP 5xx) | < 1%     | Proxy for overall platform health                                   |

### Key PromQL Queries

**Cache Hit Rate:**

```promql
sum(rate(eddi_tool_cache_hits_total[5m])) /
  (sum(rate(eddi_tool_cache_hits_total[5m])) +
   sum(rate(eddi_tool_cache_misses_total[5m])))
```

**Tool Success Rate:**

```promql
sum(rate(eddi_tool_execution_success_total[5m])) /
  (sum(rate(eddi_tool_execution_success_total[5m])) +
   sum(rate(eddi_tool_execution_failure_total[5m])))
```

**P95 Conversation Processing Latency:**

```promql
histogram_quantile(0.95,
  sum(rate(eddi_conversation_processing_duration_seconds_bucket[5m])) by (le))
```

**Cost Per Hour:**

```promql
rate(eddi_tool_costs_total[1h])
```

***

## Additional Resources

* [**LLM Integration Guide**](/agent-configuration/langchain) — Full LangChain and agent documentation
* [**Audit Ledger**](/security-and-compliance/audit-ledger) — Audit compliance and dropped entry monitoring
* [**Security**](/security-and-compliance/security) — Authentication and RBAC configuration
* [**Kubernetes**](/deployment-and-infrastructure/kubernetes) — Production deployment with monitoring overlay
* [**Prometheus Documentation**](https://prometheus.io/docs/) — Prometheus setup
* [**Grafana Documentation**](https://grafana.com/docs/) — Dashboard creation
* [**Micrometer Documentation**](https://micrometer.io/docs) — Metrics framework


# Log Administration

> **Base path:** `/administration/logs`\
> **Security:** `@RolesAllowed("eddi-admin")` — requires `eddi-admin` role when OIDC is enabled (`QUARKUS_OIDC_TENANT_ENABLED=true`). Bypassed in dev mode (OIDC disabled by default).

EDDI provides a built-in log management API for platform-wide observability. It captures all application log records into an in-memory ring buffer and optionally persists them to the database for cross-restart history.

> **Note:** This API provides *system-level application logs* (JUL/JBoss log records). For conversation message history (user/assistant messages), use the [Conversation Log endpoint](#conversation-log) instead.

***

## Endpoints

### Recent Logs (Ring Buffer)

```
GET /administration/logs
```

Returns recent log entries from the in-memory ring buffer. These are fast to query but do not survive restarts.

| Parameter        | Type    | Default | Description                                                                                               |
| ---------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------- |
| `agentId`        | string  | —       | Filter by agent ID                                                                                        |
| `conversationId` | string  | —       | Filter by conversation ID                                                                                 |
| `level`          | string  | —       | Minimum log level (`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`). Returns entries **at or above** this level |
| `limit`          | integer | `200`   | Maximum number of entries to return                                                                       |

**Example:**

```bash
# Get the 50 most recent WARN+ entries for a specific agent
curl "http://localhost:7070/administration/logs?agentId=my-agent&level=WARN&limit=50"
```

**Response:**

```json
[
  {
    "timestamp": 1711900800000,
    "level": "WARN",
    "loggerName": "ai.labs.eddi.modules.llm.impl.LlmTask",
    "message": "LLM response was not valid JSON, storing as plain string",
    "environment": "production",
    "agentId": "my-agent",
    "agentVersion": 3,
    "conversationId": "conv-abc123",
    "userId": "user-42",
    "instanceId": "eddi-host-a1b2"
  }
]
```

***

### Historical Logs (Database)

```
GET /administration/logs/history
```

Returns historical logs from the database. These survive restarts and work across instances. Only logs at or above the configured `eddi.logs.db-persist-min-level` are persisted.

| Parameter        | Type    | Default | Description                                                       |
| ---------------- | ------- | ------- | ----------------------------------------------------------------- |
| `environment`    | enum    | —       | Filter by deployment environment (`production`, `test`, etc.)     |
| `agentId`        | string  | —       | Filter by agent ID                                                |
| `agentVersion`   | integer | —       | Filter by agent version                                           |
| `conversationId` | string  | —       | Filter by conversation ID                                         |
| `userId`         | string  | —       | Filter by user ID                                                 |
| `instanceId`     | string  | —       | Filter by EDDI instance ID (useful in multi-instance deployments) |
| `skip`           | integer | `0`     | Number of entries to skip (pagination)                            |
| `limit`          | integer | `50`    | Maximum entries to return                                         |

**Example:**

```bash
# Get historical errors for a specific conversation
curl "http://localhost:7070/administration/logs/history?conversationId=conv-abc123&limit=100"
```

***

### Live Log Stream (SSE)

```
GET /administration/logs/stream
```

Opens a Server-Sent Events (SSE) connection for real-time log tailing. Supports the same filters as the recent logs endpoint.

| Parameter        | Type   | Default | Description                                       |
| ---------------- | ------ | ------- | ------------------------------------------------- |
| `agentId`        | string | —       | Filter by agent ID                                |
| `conversationId` | string | —       | Filter by conversation ID                         |
| `level`          | string | —       | Minimum log level (same semantics as recent logs) |

**Example:**

```bash
# Live-tail all WARN+ logs
curl -N -H "Accept: text/event-stream" \
  "http://localhost:7070/administration/logs/stream?level=WARN"
```

Each SSE event contains a JSON-serialized `LogEntry`:

```
data: {"timestamp":1711900800000,"level":"ERROR","loggerName":"...","message":"..."}
```

***

### Instance ID

```
GET /administration/logs/instance-id
```

Returns the unique identifier for this EDDI instance. Useful for correlating logs in multi-instance deployments.

**Response:**

```json
{
  "instanceId": "eddi-host-a1b2c3d4"
}
```

***

## Configuration

All logging configuration lives in `application.properties`:

| Property                              | Default | Description                                                                          |
| ------------------------------------- | ------- | ------------------------------------------------------------------------------------ |
| `eddi.logs.buffer-size`               | `10000` | Ring buffer capacity (in-memory entries)                                             |
| `eddi.logs.db-enabled`                | `true`  | Enable/disable database persistence                                                  |
| `eddi.logs.db-flush-interval-seconds` | `5`     | How often the async writer flushes to the database                                   |
| `eddi.logs.db-persist-min-level`      | `WARN`  | Minimum level to persist to the database (`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`) |

***

## Architecture

```
┌──────────────────┐     ┌─────────────────────────────────────┐
│ Quarkus Logger   │────→│ LogCaptureFilter (@LoggingFilter)   │
│ (all log records)│     │ Intercepts every JBoss LogRecord    │
└──────────────────┘     └───────────┬─────────────────────────┘
                                     │
                                     ▼
                         ┌───────────────────────┐
                         │   BoundedLogStore      │
                         │                       │
                         │  ┌─────────────────┐  │
                         │  │  Ring Buffer     │──┼──→ GET /administration/logs
                         │  │  (ArrayDeque)    │  │
                         │  └─────────────────┘  │
                         │                       │
                         │  ┌─────────────────┐  │
                         │  │  SSE Listeners   │──┼──→ GET /administration/logs/stream
                         │  └─────────────────┘  │
                         │                       │
                         │  ┌─────────────────┐  │     ┌──────────────┐
                         │  │  Async DB Queue  │──┼────→│ IDatabaseLogs│
                         │  │  (batch flush)   │  │     │ (Mongo / PG) │──→ GET .../history
                         │  └─────────────────┘  │     └──────────────┘
                         └───────────────────────┘
```

The `LogCaptureFilter` captures **every** log record (all levels) into the ring buffer for instant query. Only entries meeting the `db-persist-min-level` threshold are enqueued for async batch persistence to the database.

***

## Related APIs

| API                                                                    | Path                           | Purpose                                                 |
| ---------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------- |
| **Log Admin** (this page)                                              | `/administration/logs`         | System-wide application logs                            |
| [**Conversation Log**](/conversations-and-orchestration/conversations) | `/agents/{conversationId}/log` | Per-conversation message history (user/assistant turns) |
| [**Audit Ledger**](/security-and-compliance/audit-ledger)              | `/auditstore`                  | EU AI Act compliance audit trail                        |

These three APIs serve distinct purposes and are not redundant.


# Agent Sync Architecture

> Transport-agnostic pipeline for import, export, upgrade, and live sync of EDDI agent configurations.

## Overview

EDDI agents are complex configuration trees: an `AgentConfiguration` contains references to `WorkflowConfiguration`s, which contain `WorkflowStep`s referencing extension resources (LLM configs, behavior rules, HTTP calls, etc.), plus `PromptSnippet`s.

The agent sync architecture provides:

1. **Selective export** — choose which resources to include in a ZIP
2. **Structural matching** — deterministic pairing of source/target resources
3. **Content diffing** — preview exactly what will change before committing
4. **In-place upgrade** — update existing resources without breaking URI references
5. **Live sync** — sync between instances without ZIP intermediary (Phase 3)

## Architecture

```
┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│  IResourceSource │     │ StructuralMatcher│     │  UpgradeExecutor │
│  (transport)     │────▶│  (analysis)      │────▶│  (write)         │
└──────────────────┘     └──────────────────┘     └──────────────────┘
        │                         │                        │
   ┌────┴────┐              ImportPreview            Store updates
   │         │           (resource diffs)         (version increments)
   ▼         ▼
ZipResource  RemoteApi     (Phase 3)
Source       ResourceSource
```

## Matching Algorithm

Resources are matched from source to target using a deterministic, structural approach:

| Resource Type  | Matching Strategy                                 | Rationale                                          |
| -------------- | ------------------------------------------------- | -------------------------------------------------- |
| **Agent**      | Direct (by `targetAgentId` parameter)             | User explicitly selects the target                 |
| **Workflows**  | Position index (0-based) in agent's workflow list | Workflows have a defined order; position is stable |
| **Extensions** | `WorkflowStep.type` URI (e.g., `ai.labs.llm`)     | Each type appears at most once per workflow        |
| **Snippets**   | `PromptSnippet.name` (natural key)                | Names are unique by convention                     |

### Why Not Match by ID?

Source and target resources have different IDs (generated by their respective MongoDB instances). Matching by origin ID only works when an agent was previously imported from the same source — structural matching works for independently-created agents too.

## Data Flow

### Import (ZIP)

```
1. User uploads ZIP → RestImportService.importAgent()
2. ZIP extracted to temp dir → ZipResourceSource created
3. If strategy=upgrade:
   a. StructuralMatcher.buildPreview() → ImportPreview (content diffs)
   b. UpgradeExecutor.executeUpgrade() → updates target resources in-place
4. If strategy=create:
   a. Standard import (creates all new resources)
5. ZipResourceSource.close() → cleans up temp dir
```

### Export (Selective)

```
1. User requests preview → RestExportService.previewExport()
2. Returns ExportPreview (resource tree with selectability flags)
3. User selects resources → exportAgent(selectedResourceIds)
4. Only selected resources included in ZIP (agent + workflow skeletons always included)
```

### Live Sync (Phase 3 — Planned)

```
1. RemoteApiResourceSource reads from remote EDDI REST API
2. Same StructuralMatcher + UpgradeExecutor pipeline
3. No ZIP intermediary — direct instance-to-instance sync
```

## Key Design Decisions

### Upgrade = Content Sync

An upgrade doesn't replace the target agent's resource tree — it syncs content into existing resources. This means:

* Target resource IDs are preserved
* URI references (workflow → extension) are updated, not rewritten
* Version numbers are incremented (creating new versions, not overwriting)
* External references to these resources (deployments, triggers) continue to work

### Extension Type Registry

`UpgradeExecutor` uses an `ExtensionStoreOps` registry to map extension type names to their configuration class and store operations. Adding a new extension type requires one entry in `resolveExtensionOps()` — no duplicated switch blocks.

### Transport Agnostic

The `IResourceSource` interface is the only transport abstraction. All downstream logic (`StructuralMatcher`, `UpgradeExecutor`, `ImportPreview`) operates on `IResourceSource` records, not on ZIP files or HTTP responses.

## ZIP Directory Structure

```
<rootDir>/
  <agentId>.agent.json
  <agentId>.descriptor.json
  <workflowId>/
    <version>/
      <workflowId>.workflow.json       (or .package.json for legacy v5)
      <workflowId>.descriptor.json
      <extId>.langchain.json           (LLM config)
      <extId>.httpcalls.json           (HTTP calls)
      <extId>.behavior.json            (rules)
      <extId>.regulardictionary.json   (dictionary)
      <extId>.property.json            (property setter)
      <extId>.output.json              (output templates)
      <extId>.mcpcalls.json            (MCP calls)
      <extId>.rag.json                 (RAG config)
      <extId>.descriptor.json          (per-extension)
  snippets/
    <snippetId>.snippet.json
```

## API Endpoints

### Export

| Method | Path                                             | Purpose                            |
| ------ | ------------------------------------------------ | ---------------------------------- |
| `POST` | `/backup/export/{agentId}/preview`               | Resource tree for selective export |
| `POST` | `/backup/export/{agentId}?selectedResources=...` | Export ZIP with selected resources |

### Import

| Method | Path                                                | Purpose                    |
| ------ | --------------------------------------------------- | -------------------------- |
| `POST` | `/backup/import/preview?targetAgentId=...`          | Preview with content diffs |
| `POST` | `/backup/import?strategy=upgrade&targetAgentId=...` | Execute upgrade import     |

### Live Sync (Phase 3)

| Method | Path                                | Purpose                   |
| ------ | ----------------------------------- | ------------------------- |
| `GET`  | `/backup/import/sync/agents`        | List remote agents        |
| `POST` | `/backup/import/sync/preview`       | Single-agent sync preview |
| `POST` | `/backup/import/sync/preview/batch` | Multi-agent sync preview  |
| `POST` | `/backup/import/sync`               | Execute single-agent sync |
| `POST` | `/backup/import/sync/batch`         | Execute multi-agent sync  |

## Component Reference

| Class               | Type           | Responsibility                                                   |
| ------------------- | -------------- | ---------------------------------------------------------------- |
| `IResourceSource`   | Interface      | Transport abstraction — reads agent config from any source       |
| `ZipResourceSource` | Implementation | Reads from unzipped directory, implements `AutoCloseable`        |
| `StructuralMatcher` | CDI Bean       | Matches source/target resources, produces `ImportPreview`        |
| `UpgradeExecutor`   | CDI Bean       | Writes source content into target resources (version increments) |
| `ImportPreview`     | Record         | Preview with resource diffs (CREATE/UPDATE/SKIP/CONFLICT)        |
| `ExportPreview`     | Record         | Resource tree with selectability flags                           |
| `SyncMapping`       | Record         | Source→target agent pair for batch sync                          |
| `SyncRequest`       | Record         | Full sync request with selections and workflow order             |


# FAQs

## How to...?

## ...add an LLM to my agent?

1. Create a LangChain configuration with your provider settings
2. Add a behavior rule that triggers the LLM action (e.g., `send_to_ai`)
3. Add the LangChain extension to your package/workflow

```json
{
  "tasks": [
    {
      "actions": ["send_to_ai"],
      "id": "openai_chat",
      "type": "openai",
      "parameters": {
        "apiKey": "${vault:OPENAI_KEY}",
        "modelName": "gpt-4o",
        "systemMessage": "You are a helpful assistant",
        "sendConversation": "true",
        "addToOutput": "true"
      }
    }
  ]
}
```

See [LLM Integration](/agent-configuration/langchain) for the complete guide with all 12 supported providers.

***

## ...store API keys securely?

Use the **Secrets Vault**. API keys and other sensitive values are encrypted at rest using envelope encryption (AES-256-GCM + PBKDF2).

**Via the Manager UI:** Navigate to **Secrets** in the sidebar. Enter a key name and value — values are write-only and can never be retrieved through the API.

**Via REST API:**

```bash
curl -X PUT http://localhost:7070/secretstore/secrets/MY_API_KEY \
  -H "Content-Type: text/plain" \
  -d "sk-abc123..."
```

**In LangChain configs,** reference secrets using vault syntax:

```json
{
  "parameters": {
    "apiKey": "${vault:MY_API_KEY}"
  }
}
```

See [Secrets Vault](/security-and-compliance/secrets-vault) for full documentation.

***

## ...use context to pass data from my app?

Send context with each message:

```bash
curl -X POST http://localhost:7070/agents/conv-123 \
  -H "Content-Type: application/json" \
  -d '{
    "input": "What is my name?",
    "context": {
      "userName": {"type": "string", "value": "John"},
      "userId": {"type": "string", "value": "user-123"}
    }
  }'
```

Access in templates: `{context.userName}`

See [Passing Context Information](/agent-configuration/passing-context-information) for full documentation.

***

## ...set up monitoring?

EDDI exposes Prometheus metrics at `/q/metrics` and includes pre-built Grafana dashboards.

**Quick setup with Docker Compose:**

```bash
docker compose -f docker-compose.yml -f docker-compose.monitoring.yml up
```

Then open Grafana at `http://localhost:3000` (admin/admin).

See [Metrics & Monitoring](/deployment-and-infrastructure/metrics) for details.

***

## ...deploy to Kubernetes?

```bash
# One-command quickstart
kubectl apply -f https://raw.githubusercontent.com/labsai/EDDI/main/k8s/quickstart.yaml

# Or use Kustomize overlays / Helm charts for production
kubectl apply -k k8s/overlays/mongodb/
```

See [Kubernetes](/deployment-and-infrastructure/kubernetes) for complete deployment options.

***

## ...start a conversation with a welcome / intro message?

You will need `behavior rules` and an `outputset` for that.

For the behavior rules, you have three possibilities (ordered by recommendation):

### 1) Match for the action `CONVERSATION_START`

```json
{
  "behaviorGroups": [
    {
      "name": "Onboarding",
      "behaviorRules": [
        {
          "name": "Welcome",
          "actions": [
            "welcome"
          ],
          "conditions": [
            {
              "type": "actionmatcher",
              "configs": {
                "actions": "CONVERSATION_START"
              }
            }
          ]
        }
      ]
    }
  ]
}
```

### 2) Check if the triggered action has never been triggered before

```json
{
  "behaviorGroups": [
    {
      "name": "Onboarding",
      "behaviorRules": [
        {
          "name": "Welcome",
          "actions": [
            "welcome"
          ],
          "conditions": [
            {
              "type": "actionmatcher",
              "configs": {
                "actions": "welcome",
                "occurrence": "never"
              }
            }
          ]
        }
      ]
    }
  ]
}
```

### 3) Check how often this rule has succeeded before

```json
{
  "behaviorGroups": [
    {
      "name": "Onboarding",
      "behaviorRules": [
        {
          "name": "Welcome",
          "actions": [
            "welcome"
          ],
          "conditions": [
            {
              "type": "occurrence",
              "configs": {
                "maxTimesOccurred": "0",
                "behaviorRuleName": "Welcome"
              }
            }
          ]
        }
      ]
    }
  ]
}
```

### Output set:

```json
{
  "outputSet": [
    {
      "action": "welcome",
      "timesOccurred": 0,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "Some output here...",
              "delay": 3000
            }
          ]
        }
      ],
      "quickReplies": []
    }
  ]
}
```

***

## ...say something based on what the agent previously said?

(Think of a form-like behavior, asking a couple of questions and sending these results somewhere.)

### Check whether a certain `action` had been triggered in the previous conversation step.

```json
{
  "behaviorGroups": [
    {
      "name": "Onboarding",
      "behaviorRules": [
        {
          "name": "Ask for Name",
          "actions": [
            "ask_for_name"
          ],
          "conditions": [
            {
              "type": "actionmatcher",
              "configs": {
                "actions": "some_previous_action",
                "occurrence": "lastStep"
              }
            }
          ]
        }
      ]
    }
  ]
}
```

***

Have a question that is not covered? Drop us an email at <contact@labs.ai>, we are happy to enhance our documentation!


