diff --git a/AGENTS.md b/AGENTS.md
index c369c24d13..6016f687a6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -305,7 +305,7 @@ A new `ILifecycleTask` requires ALL of:
- [ ] `ExtensionDescriptor` (UI field definitions via `getExtensionDescriptor()`)
- [ ] Unit test with Mockito
-All task implementations MUST implement: `getId()`, `getType()`, `execute()`, `configure()`, `getExtensionDescriptor()`.
+All task implementations MUST implement: `getId()` (returns `TaskId`), `getType()`, `execute()`, `configure()`, `getExtensionDescriptor()`.
### 4.4 Code Patterns
@@ -441,7 +441,7 @@ public class MyFeatureTask implements ILifecycleTask {
this.dataFactory = dataFactory;
}
- @Override public String getId() { return ID; }
+ @Override public TaskId getId() { return new TaskId(ID); }
@Override public String getType() { return KEY_MYFEATURE; }
@Override
@@ -487,7 +487,7 @@ public class MyFeatureTask implements ILifecycleTask {
@Override
public ExtensionDescriptor getExtensionDescriptor() {
- ExtensionDescriptor descriptor = new ExtensionDescriptor(ID);
+ ExtensionDescriptor descriptor = new ExtensionDescriptor(getId());
descriptor.setDisplayName("My Feature");
ConfigValue uriConfig = new ConfigValue("Resource URI", FieldType.URI, false, null);
descriptor.getConfigs().put("uri", uriConfig);
diff --git a/docs/architecture.md b/docs/architecture.md
index 1830cd22c8..30ec82b518 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -144,7 +144,7 @@ A typical agent lifecycle includes these task types:
```java
public interface ILifecycleTask {
- String getId();
+ TaskId getId();
String getType();
void execute(IConversationMemory memory, Object component)
throws LifecycleException;
diff --git a/docs/developer-quickstart.md b/docs/developer-quickstart.md
index 3888c1651a..3b76abbe8c 100644
--- a/docs/developer-quickstart.md
+++ b/docs/developer-quickstart.md
@@ -547,8 +547,8 @@ Create a custom lifecycle task:
@ApplicationScoped
public class MyCustomTask implements ILifecycleTask {
@Override
- public String getId() {
- return "ai.labs.mycompany.customtask";
+ public TaskId getId() {
+ return new TaskId("ai.labs.mycompany.customtask");
}
@Override
diff --git a/mise.toml b/mise.toml
index 5e9510aa9d..6265f44f8d 100644
--- a/mise.toml
+++ b/mise.toml
@@ -1,23 +1,49 @@
[tools]
java = "temurin-25.0.3+9.0.LTS"
maven = "3.9.12"
+"npm:chromadb" = "3.4.3"
-[tasks]
-dev = { run = "mvn compile quarkus:dev", description = "Start dev mode with live reload (port 7070)" }
-compile = { run = "mvn compile", description = "Compile sources only (fast feedback)" }
+[tasks.dev]
+depends = ["clean"]
+run = "mvn compile quarkus:dev"
+description = "Start dev mode with live reload (port 7070)"
-clean-build = { run = "mvn clean compile", description = "Clean build — delete target/ and recompile from scratch" }
-test = { run = "mvn test", description = "Run unit tests (excludes *IT.java integration tests)" }
+[tasks.compile]
+depends = ["clean"]
+run = "mvn compile"
+description = "Compile sources only (fast feedback)"
-verify = { run = "mvn verify -DskipITs", description = "Compile + unit tests + package (no integration tests)" }
-full-build = { run = "mvn verify", description = "Full build — compile + unit tests + integration tests (requires Docker)" }
-validate = { run = "mvn validate", description = "Run Checkstyle code style checks" }
-format = { run = "mvn formatter:format", description = "Auto-format Java sources using the project Eclipse formatter" }
+[tasks.test]
+run = "mvn verify"
+description = "Compile + unit tests + integration tests (requires Docker)"
-docker-build = { run = "mvn clean package '-Dquarkus.container-image.build=true'", description = "Build the app + Docker image" }
-license-gen = { run = "mvn package -Plicense-gen -DskipTests", description = "Generate third-party licenses (Red Hat certification)" }
+[tasks.validate]
+run = "mvn validate"
+description = "Run Checkstyle code style checks"
-debug = { run = "mvn quarkus:dev -Dsuspend", description = "Start dev mode and wait for debugger on port 5005" }
-dev-no-debug = { run = "mvn quarkus:dev -Ddebug=false", description = "Start dev mode without the debug agent" }
-chroma = { run = "docker compose -f docker-compose.chroma.yml up", description = "Starts ChromaDB" }
+[tasks.format]
+run = "mvn formatter:format"
+description = "Auto-format Java sources using the project Eclipse formatter"
+
+[tasks.docker-build]
+run = "mvn clean package '-Dquarkus.container-image.build=true'"
+description = "Build the app + Docker image"
+
+
+[tasks.license-gen]
+run = "mvn package -Plicense-gen -DskipTests"
+description = "Generate third-party licenses (Red Hat certification)"
+
+[tasks.debug]
+run = "mvn quarkus:dev -Dsuspend"
+description = "Start dev mode and wait for debugger on port 5005"
+
+[tasks.clean]
+run = "mvn clean"
+description = "Clean build — delete target"
+
+
+[tasks.chroma]
+run = "docker compose -f docker-compose.chroma.yml up"
+description = "Starts ChromaDB"
diff --git a/planning/memory-architecture-plan.md b/planning/memory-architecture-plan.md
index 91acb9a85f..800e647938 100644
--- a/planning/memory-architecture-plan.md
+++ b/planning/memory-architecture-plan.md
@@ -327,9 +327,9 @@ Changes required:
} catch (Exception e) {
// Failure → mark all NEW data written by this task as uncommitted
markNewDataUncommitted(memory.getCurrentStep(), preTaskData);
- auditLedger.logTaskFailure(task.getId(), e, conversationId);
- // Inject error action, NOT the exception, into the pipeline
- memory.getCurrentStep().storeData(new Data<>("actions", List.of("task_failed_" + task.getId())));
+ auditLedger.logTaskFailure(task.getId().name(), e, conversationId);
+ // Inject error action, NOT the exception, into the pipeline
+ memory.getCurrentStep().storeData(new Data<>("actions", List.of("task_failed_" + task.getId().name())));
}
```
diff --git a/planning/observability-and-pipeline-plan.md b/planning/observability-and-pipeline-plan.md
index 3984687ff2..e932e3269b 100644
--- a/planning/observability-and-pipeline-plan.md
+++ b/planning/observability-and-pipeline-plan.md
@@ -31,7 +31,7 @@
The main execution path is in [`LifecycleManager.executeLifecycle()`](../../src/main/java/ai/labs/eddi/engine/lifecycle/LifecycleManager.java). Add span-per-task:
```java
-Span span = tracer.spanBuilder("eddi.task." + task.getId())
+Span span = tracer.spanBuilder("eddi.task." + task.getId().name())
.setAttribute("conversationId", conversationId)
.setAttribute("agentId", agentId)
.startSpan();
diff --git a/src/main/java/ai/labs/eddi/configs/workflows/model/ExtensionDescriptor.java b/src/main/java/ai/labs/eddi/configs/workflows/model/ExtensionDescriptor.java
index e7164120e4..fca7e281dc 100644
--- a/src/main/java/ai/labs/eddi/configs/workflows/model/ExtensionDescriptor.java
+++ b/src/main/java/ai/labs/eddi/configs/workflows/model/ExtensionDescriptor.java
@@ -9,13 +9,16 @@
import java.util.List;
import java.util.Map;
+import ai.labs.eddi.engine.lifecycle.TaskId;
+
public class ExtensionDescriptor {
- private String type;
+ private TaskId type;
+
private String displayName;
private Map configs = new HashMap<>();
private Map> extensions = new HashMap<>();
- public ExtensionDescriptor(String type) {
+ public ExtensionDescriptor(TaskId type) {
this.type = type;
}
@@ -73,11 +76,11 @@ public void addExtension(String extensionName, ExtensionDescriptor extensionDesc
extensions.computeIfAbsent(extensionName, k -> new LinkedList<>()).add(extensionDescriptor);
}
- public String getType() {
+ public TaskId getType() {
return type;
}
- public void setType(String type) {
+ public void setType(TaskId type) {
this.type = type;
}
diff --git a/src/main/java/ai/labs/eddi/configs/workflows/rest/RestWorkflowStepStore.java b/src/main/java/ai/labs/eddi/configs/workflows/rest/RestWorkflowStepStore.java
index 11dd77aeac..a75e62c216 100644
--- a/src/main/java/ai/labs/eddi/configs/workflows/rest/RestWorkflowStepStore.java
+++ b/src/main/java/ai/labs/eddi/configs/workflows/rest/RestWorkflowStepStore.java
@@ -14,21 +14,23 @@
import jakarta.inject.Provider;
import java.util.List;
import java.util.Map;
+
@ApplicationScoped
public class RestWorkflowStepStore implements IRestWorkflowStepStore {
private final Map> lifecycleExtensionsProvider;
@Inject
public RestWorkflowStepStore(@LifecycleExtensions Map> lifecycleExtensionsProvider) {
-
this.lifecycleExtensionsProvider = lifecycleExtensionsProvider;
}
@Override
public List getWorkflowSteps(String filter) {
- return lifecycleExtensionsProvider.keySet().stream().filter(type -> filter.isEmpty() || type.contains(filter)).map(type -> {
- Provider taskProvider = lifecycleExtensionsProvider.get(type);
- return taskProvider.get().getExtensionDescriptor();
- }).toList();
+ return lifecycleExtensionsProvider.keySet().stream()
+ .filter(type -> filter == null || filter.isEmpty() || type.contains(filter))
+ .map(type -> {
+ Provider taskProvider = lifecycleExtensionsProvider.get(type);
+ return taskProvider.get().getExtensionDescriptor();
+ }).toList();
}
}
diff --git a/src/main/java/ai/labs/eddi/engine/api/IConversationService.java b/src/main/java/ai/labs/eddi/engine/api/IConversationService.java
index 865a8984f8..7d366e1550 100644
--- a/src/main/java/ai/labs/eddi/engine/api/IConversationService.java
+++ b/src/main/java/ai/labs/eddi/engine/api/IConversationService.java
@@ -8,6 +8,7 @@
import ai.labs.eddi.datastore.IResourceStore.ResourceStoreException;
import ai.labs.eddi.engine.memory.model.SimpleConversationMemorySnapshot;
import ai.labs.eddi.engine.model.Context;
+import ai.labs.eddi.engine.lifecycle.TaskId;
import ai.labs.eddi.engine.memory.model.ConversationState;
import ai.labs.eddi.engine.model.Deployment.Environment;
import ai.labs.eddi.engine.model.InputData;
@@ -105,9 +106,9 @@ interface ConversationResponseHandler {
* Callback for streaming conversation responses via SSE.
*/
interface StreamingResponseHandler {
- void onTaskStart(String taskId, String taskType, int index);
+ void onTaskStart(TaskId taskId, String taskType, int index);
- void onTaskComplete(String taskId, String taskType, long durationMs, Map summary);
+ void onTaskComplete(TaskId taskId, String taskType, long durationMs, Map summary);
void onToken(String token);
diff --git a/src/main/java/ai/labs/eddi/engine/internal/ConversationService.java b/src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
index e35306bb0a..530756431c 100644
--- a/src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
+++ b/src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
@@ -19,6 +19,7 @@
import ai.labs.eddi.engine.caching.ICacheFactory;
import ai.labs.eddi.engine.lifecycle.ConversationEventSink;
import ai.labs.eddi.engine.lifecycle.IConversation;
+import ai.labs.eddi.engine.lifecycle.TaskId;
import ai.labs.eddi.engine.lifecycle.exceptions.LifecycleException;
import ai.labs.eddi.engine.memory.ConversationLogGenerator;
import ai.labs.eddi.engine.memory.IConversationMemory;
@@ -420,12 +421,12 @@ public void sayStreaming(Environment environment, String agentId, String convers
// Create event sink that delegates to the streaming handler
var eventSink = new ConversationEventSink() {
@Override
- public void onTaskStart(String taskId, String taskType, int index) {
+ public void onTaskStart(TaskId taskId, String taskType, int index) {
streamingHandler.onTaskStart(taskId, taskType, index);
}
@Override
- public void onTaskComplete(String taskId, String taskType, long durationMs, Map summary) {
+ public void onTaskComplete(TaskId taskId, String taskType, long durationMs, Map summary) {
streamingHandler.onTaskComplete(taskId, taskType, durationMs, summary);
}
diff --git a/src/main/java/ai/labs/eddi/engine/internal/RestAgentEngineStreaming.java b/src/main/java/ai/labs/eddi/engine/internal/RestAgentEngineStreaming.java
index 0710d8d67a..36e46bd179 100644
--- a/src/main/java/ai/labs/eddi/engine/internal/RestAgentEngineStreaming.java
+++ b/src/main/java/ai/labs/eddi/engine/internal/RestAgentEngineStreaming.java
@@ -8,6 +8,7 @@
import ai.labs.eddi.engine.api.IRestAgentEngineStreaming;
import ai.labs.eddi.engine.memory.model.SimpleConversationMemorySnapshot;
+import ai.labs.eddi.engine.lifecycle.TaskId;
import ai.labs.eddi.engine.model.InputData;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@@ -55,15 +56,16 @@ public void sayStreaming(String conversationId, Boolean returnDetailed, Boolean
conversationService.sayStreaming(conversationId, returnDetailed, returnCurrentStepOnly, returningFields, inputData,
new IConversationService.StreamingResponseHandler() {
@Override
- public void onTaskStart(String taskId, String taskType, int index) {
+ public void onTaskStart(TaskId taskId, String taskType, int index) {
sendEvent(eventSink, sse, "task_start",
- String.format("{\"taskId\":\"%s\",\"taskType\":\"%s\",\"index\":%d}", taskId, taskType, index));
+ String.format("{\"taskId\":\"%s\",\"taskType\":\"%s\",\"index\":%d}", taskId.getIdentifier(), taskType, index));
}
@Override
- public void onTaskComplete(String taskId, String taskType, long durationMs, Map summary) {
+ public void onTaskComplete(TaskId taskId, String taskType, long durationMs, Map summary) {
var sb = new StringBuilder();
- sb.append(String.format("{\"taskId\":\"%s\",\"taskType\":\"%s\",\"durationMs\":%d", taskId, taskType, durationMs));
+ sb.append(String.format("{\"taskId\":\"%s\",\"taskType\":\"%s\",\"durationMs\":%d", taskId.getIdentifier(), taskType,
+ durationMs));
if (summary.containsKey("actions")) {
sb.append(",\"actions\":").append(toJsonArray(summary.get("actions")));
}
diff --git a/src/main/java/ai/labs/eddi/engine/lifecycle/ConversationEventSink.java b/src/main/java/ai/labs/eddi/engine/lifecycle/ConversationEventSink.java
index 01048a6aa2..d2063d3085 100644
--- a/src/main/java/ai/labs/eddi/engine/lifecycle/ConversationEventSink.java
+++ b/src/main/java/ai/labs/eddi/engine/lifecycle/ConversationEventSink.java
@@ -33,7 +33,7 @@ public interface ConversationEventSink {
* @param index
* 0-based position in the workflow
*/
- void onTaskStart(String taskId, String taskType, int index);
+ void onTaskStart(TaskId taskId, String taskType, int index);
/**
* Called after a lifecycle task completes execution.
@@ -48,7 +48,7 @@ public interface ConversationEventSink {
* optional task-specific summary data (e.g. emitted actions). May be
* empty, never null.
*/
- void onTaskComplete(String taskId, String taskType, long durationMs, Map summary);
+ void onTaskComplete(TaskId taskId, String taskType, long durationMs, Map summary);
/**
* Called for each LLM token during streaming chat completion.
diff --git a/src/main/java/ai/labs/eddi/engine/lifecycle/ILifecycleTask.java b/src/main/java/ai/labs/eddi/engine/lifecycle/ILifecycleTask.java
index add277eba7..eb02af297e 100644
--- a/src/main/java/ai/labs/eddi/engine/lifecycle/ILifecycleTask.java
+++ b/src/main/java/ai/labs/eddi/engine/lifecycle/ILifecycleTask.java
@@ -99,7 +99,7 @@ public interface ILifecycleTask {
* @return unique identifier of this task (e.g., "ai.labs.parser",
* "ai.labs.behavior")
*/
- String getId();
+ TaskId getId();
/**
* Returns the type identifier for this lifecycle task.
diff --git a/src/main/java/ai/labs/eddi/engine/lifecycle/TaskId.java b/src/main/java/ai/labs/eddi/engine/lifecycle/TaskId.java
new file mode 100644
index 0000000000..5e3e1fecbe
--- /dev/null
+++ b/src/main/java/ai/labs/eddi/engine/lifecycle/TaskId.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright EDDI contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package ai.labs.eddi.engine.lifecycle;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * Immutable identifier for lifecycle tasks in the EDDI conversation pipeline.
+ *
+ *
+ * TaskId provides a type-safe wrapper around task identifiers with a canonical
+ * URI format ({@code eddi://ai.labs.taskname}). It ensures consistent
+ * identifier handling across the system and provides clean JSON serialization.
+ *
+ *
+ *
+ * Usage:
+ *
+ *
+ * {@code
+ * TaskId parserTask = new TaskId("ai.labs.parser");
+ * String identifier = parserTask.getIdentifier(); // "eddi://ai.labs.parser"
+ * String name = parserTask.name(); // "ai.labs.parser"
+ * }
+ *
+ *
+ * JSON Serialization:
+ *
+ *
+ * - Serialize: {@code "eddi://ai.labs.parser"} (via
+ * {@link #getIdentifier()})
+ * - Deserialize: Accepts both
+ * {@code "eddi://ai.labs.parser"} and {@code "ai.labs.parser"}
+ *
+ *
+ * @param name
+ * the task identifier (e.g., "ai.labs.parser", "ai.labs.behavior")
+ * @author EDDI Team
+ * @since 6.0
+ * @see ILifecycleTask#getId()
+ */
+public record TaskId(String name) {
+
+ public TaskId {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("TaskId name cannot be null or blank");
+ }
+ }
+
+ /**
+ * URI scheme prefix for all task identifiers. Full identifiers have the format
+ * {@code eddi://}.
+ */
+ private static final String SCHEME = "eddi://";
+
+ /**
+ * Returns the full URI identifier for this task.
+ *
+ *
+ * This is the canonical string representation used for:
+ *
+ * - JSON serialization (via {@link JsonValue})
+ * - Component cache lookups
+ * - Logging and debugging
+ * - OpenTelemetry span attributes
+ *
+ *
+ *
+ * @return the full URI identifier (e.g., "eddi://ai.labs.parser")
+ */
+ @JsonValue
+ public String getIdentifier() {
+ return SCHEME + name;
+ }
+
+ /**
+ * Returns the full URI identifier.
+ *
+ *
+ * Delegates to {@link #getIdentifier()} for consistent string representation.
+ *
+ *
+ * @return the full URI identifier
+ */
+ @Override
+ public String toString() {
+ return getIdentifier();
+ }
+
+ /**
+ * Factory method for deserializing TaskId from a string value.
+ *
+ *
+ * Accepts both formats:
+ *
+ * - Full URI: {@code "eddi://ai.labs.parser"}
+ * - Bare type: {@code "ai.labs.parser"}
+ *
+ *
+ *
+ * @param value
+ * the string to deserialize (must not be null or blank)
+ * @return a new TaskId instance
+ * @throws IllegalArgumentException
+ * if value is null or blank
+ */
+ @JsonCreator
+ public static TaskId fromValue(String value) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException("TaskId cannot be null or blank");
+ }
+
+ if (value.startsWith(SCHEME)) {
+ return new TaskId(value.substring(SCHEME.length()));
+ }
+
+ return new TaskId(value);
+ }
+}
diff --git a/src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java b/src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java
index 802d6faf10..860b44d5d2 100644
--- a/src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java
+++ b/src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java
@@ -214,6 +214,11 @@ public void executeLifecycle(final IConversationMemory conversationMemory, List<
for (int index = 0; index < lifecycleTasks.size(); index++) {
ILifecycleTask task = lifecycleTasks.get(index);
+ // Fail-fast: every task must have a non-null TaskId
+ if (task.getId() == null) {
+ throw new LifecycleException("Lifecycle task returned null TaskId: " + task.getClass().getName());
+ }
+
// Check if execution should be interrupted (graceful shutdown)
if (Thread.currentThread().isInterrupted()) {
throw new LifecycleException.LifecycleInterruptedException("Execution was interrupted!");
@@ -236,7 +241,7 @@ public void executeLifecycle(final IConversationMemory conversationMemory, List<
// === OpenTelemetry: create span per task ===
Span taskSpan = getTracer().spanBuilder("eddi.pipeline.task")
- .setAttribute("eddi.task.id", Objects.requireNonNullElse(task.getId(), "unknown"))
+ .setAttribute("eddi.task.id", task.getId().name())
.setAttribute("eddi.task.type", Objects.requireNonNullElse(task.getType(), "unknown"))
.setAttribute("eddi.task.index", (long) index)
.setAttribute("eddi.conversation.id",
@@ -251,7 +256,7 @@ public void executeLifecycle(final IConversationMemory conversationMemory, List<
// Retrieve task's component from cache
// Component contains task-specific configuration loaded during agent
// initialization
- var components = componentCache.getComponentMap(task.getId());
+ var components = componentCache.getComponentMap(task.getId().name());
var componentKey = createComponentKey(workflowId.getId(), workflowId.getVersion(), index);
var component = components.getOrDefault(componentKey, null);
@@ -286,7 +291,7 @@ public void executeLifecycle(final IConversationMemory conversationMemory, List<
taskSpan.recordException(e);
// Record error counter for dashboards & alerting
- String errTaskId = task.getId() != null ? task.getId() : "unknown";
+ String errTaskId = task.getId().name();
String errTaskType = task.getType() != null ? task.getType() : "unknown";
String errMeterKey = errTaskId + "|" + errTaskType;
TASK_ERROR_COUNTERS.computeIfAbsent(errMeterKey, k -> Counter.builder("eddi.pipeline.task.errors")
@@ -315,7 +320,7 @@ public void executeLifecycle(final IConversationMemory conversationMemory, List<
// duration histogram (failing tasks with long timeouts are especially
// important for latency monitoring during incidents).
long durationMs = (System.nanoTime() - taskStartTime) / 1_000_000;
- String taskId = task.getId() != null ? task.getId() : "unknown";
+ String taskId = task.getId().name();
String taskType = task.getType() != null ? task.getType() : "unknown";
String meterKey = taskId + "|" + taskType;
TASK_TIMERS.computeIfAbsent(meterKey, k -> Timer.builder("eddi.pipeline.task.duration")
@@ -342,7 +347,7 @@ private Map buildTaskSummary(IConversationMemory conversationMem
summary.put("actions", actionData.getResult());
}
// Tool execution trace (for LLM tasks) — enables live tool call display in UI
- IData> traceData = conversationMemory.getCurrentStep().getLatestData("langchain:trace:" + task.getId());
+ IData> traceData = conversationMemory.getCurrentStep().getLatestData("langchain:trace:" + task.getId().name());
if (traceData != null && traceData.getResult() != null) {
summary.put("toolTrace", traceData.getResult());
}
@@ -397,9 +402,11 @@ private AuditEntry buildAuditEntry(IConversationMemory memory, ILifecycleTask ta
@SuppressWarnings("unchecked")
List actions = summary.containsKey("actions") ? (List) summary.get("actions") : null;
- return new AuditEntry(UUID.randomUUID().toString(), memory.getConversationId(), memory.getAgentId(), memory.getAgentVersion(),
+ return new AuditEntry(UUID.randomUUID().toString(), memory.getConversationId(), memory.getAgentId(),
+ memory.getAgentVersion(),
memory.getUserId(), null, // environment is set by ConversationService
- stepIndex, task.getId(), task.getType(), taskIndex, durationMs, input.isEmpty() ? null : input, output.isEmpty() ? null : output,
+ stepIndex, task.getId().name(), task.getType(), taskIndex, durationMs, input.isEmpty() ? null : input,
+ output.isEmpty() ? null : output,
llmDetail, null, // toolCalls — set by LlmTask in memory
actions, 0.0, // cost — set by ToolCostTracker integration
Instant.now(), null // HMAC computed by AuditLedgerService
@@ -583,7 +590,7 @@ private void injectErrorDigest(ConversationStep step, ILifecycleTask task, Excep
// Store under separate "taskErrors" key — never mixed into "output"
var errorOutput = new LinkedHashMap();
errorOutput.put("type", "errorDigest");
- errorOutput.put("taskId", task.getId());
+ errorOutput.put("taskId", task.getId().name());
errorOutput.put("taskType", task.getType());
errorOutput.put("text", digestText);
step.addConversationOutputList("taskErrors", List.of(errorOutput));
@@ -605,7 +612,7 @@ private void injectErrorDigest(ConversationStep step, ILifecycleTask task, Excep
*/
private void injectFailureAction(ConversationStep step, ILifecycleTask task,
List actionsBefore) {
- String failureAction = "task_failed_" + task.getId();
+ String failureAction = "task_failed_" + task.getId().name();
// Rebuild from pre-failure state + failure action
List actions = new ArrayList<>(actionsBefore);
diff --git a/src/main/java/ai/labs/eddi/engine/runtime/client/workflows/WorkflowStoreClientLibrary.java b/src/main/java/ai/labs/eddi/engine/runtime/client/workflows/WorkflowStoreClientLibrary.java
index 36e5dc8875..84a13c70e2 100644
--- a/src/main/java/ai/labs/eddi/engine/runtime/client/workflows/WorkflowStoreClientLibrary.java
+++ b/src/main/java/ai/labs/eddi/engine/runtime/client/workflows/WorkflowStoreClientLibrary.java
@@ -84,7 +84,11 @@ private IExecutableWorkflow createExecutableWorkflow(final DocumentDescriptor do
var component = lifecycleTask.configure(workflowStep.getConfig(), workflowStep.getExtensions());
if (component != null) {
- componentCache.put(lifecycleTask.getId(), componentKey, component);
+ if (lifecycleTask.getId() == null) {
+ throw new WorkflowInitializationException(
+ "Lifecycle task returned null TaskId: " + lifecycleTask.getClass().getName(), null);
+ }
+ componentCache.put(lifecycleTask.getId().name(), componentKey, component);
}
lifecycleManager.addLifecycleTask(lifecycleTask);
}
diff --git a/src/main/java/ai/labs/eddi/modules/apicalls/impl/ApiCallsTask.java b/src/main/java/ai/labs/eddi/modules/apicalls/impl/ApiCallsTask.java
index 33ff99a3b6..32d9f8af83 100644
--- a/src/main/java/ai/labs/eddi/modules/apicalls/impl/ApiCallsTask.java
+++ b/src/main/java/ai/labs/eddi/modules/apicalls/impl/ApiCallsTask.java
@@ -9,6 +9,7 @@
import ai.labs.eddi.configs.workflows.model.ExtensionDescriptor.ConfigValue;
import ai.labs.eddi.configs.workflows.model.ExtensionDescriptor.FieldType;
import ai.labs.eddi.engine.lifecycle.ILifecycleTask;
+import ai.labs.eddi.engine.lifecycle.TaskId;
import ai.labs.eddi.engine.lifecycle.exceptions.LifecycleException;
import ai.labs.eddi.engine.lifecycle.exceptions.WorkflowConfigurationException;
import ai.labs.eddi.engine.memory.IConversationMemory;
@@ -32,6 +33,8 @@
@ApplicationScoped
public class ApiCallsTask implements ILifecycleTask {
public static final String ID = "ai.labs.httpcalls";
+ public static final TaskId TASK_ID = new TaskId(ID);
+
private static final String KEY_HTTP_CALLS = "httpCalls";
private final IResourceClientLibrary resourceClientLibrary;
private final IMemoryItemConverter memoryItemConverter;
@@ -47,8 +50,8 @@ public ApiCallsTask(IResourceClientLibrary resourceClientLibrary, IMemoryItemCon
}
@Override
- public String getId() {
- return ID;
+ public TaskId getId() {
+ return TASK_ID;
}
@Override
@@ -118,7 +121,7 @@ public Object configure(Map configuration, Map e
@Override
public ExtensionDescriptor getExtensionDescriptor() {
- ExtensionDescriptor extensionDescriptor = new ExtensionDescriptor(ID);
+ ExtensionDescriptor extensionDescriptor = new ExtensionDescriptor(new TaskId(ID));
extensionDescriptor.setDisplayName("Http Calls");
ConfigValue configValue = new ConfigValue("Resource URI", FieldType.URI, false, null);
extensionDescriptor.getConfigs().put("uri", configValue);
diff --git a/src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java b/src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java
index e95c8e7a43..c8c253f08d 100644
--- a/src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java
+++ b/src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java
@@ -15,6 +15,7 @@
import ai.labs.eddi.engine.attachments.IAttachmentStore;
import ai.labs.eddi.engine.lifecycle.ConversationEventSink;
import ai.labs.eddi.engine.lifecycle.ILifecycleTask;
+import ai.labs.eddi.engine.lifecycle.TaskId;
import ai.labs.eddi.engine.lifecycle.exceptions.LifecycleException;
import ai.labs.eddi.engine.lifecycle.exceptions.WorkflowConfigurationException;
import ai.labs.eddi.engine.memory.*;
@@ -62,6 +63,7 @@
@ApplicationScoped
public class LlmTask implements ILifecycleTask {
public static final String ID = "ai.labs.llm";
+ public static final TaskId TASK_ID = new TaskId(ID);
private static final String KEY_URI = "uri";
private static final String KEY_LANGCHAIN = "langchain";
@@ -152,8 +154,8 @@ public LlmTask(IResourceClientLibrary resourceClientLibrary, IDataFactory dataFa
}
@Override
- public String getId() {
- return ID;
+ public TaskId getId() {
+ return TASK_ID;
}
@Override
@@ -672,7 +674,7 @@ private static String resolveModelName(Map processedParams) {
@Override
public ExtensionDescriptor getExtensionDescriptor() {
- ExtensionDescriptor extensionDescriptor = new ExtensionDescriptor(ID);
+ ExtensionDescriptor extensionDescriptor = new ExtensionDescriptor(new TaskId(ID));
extensionDescriptor.setDisplayName("Lang Chain");
ConfigValue configValue = new ConfigValue("Resource URI", FieldType.URI, false, null);
diff --git a/src/main/java/ai/labs/eddi/modules/mcpcalls/impl/McpCallsTask.java b/src/main/java/ai/labs/eddi/modules/mcpcalls/impl/McpCallsTask.java
index 0c235143a7..d675454c8c 100644
--- a/src/main/java/ai/labs/eddi/modules/mcpcalls/impl/McpCallsTask.java
+++ b/src/main/java/ai/labs/eddi/modules/mcpcalls/impl/McpCallsTask.java
@@ -9,6 +9,7 @@
import ai.labs.eddi.configs.workflows.model.ExtensionDescriptor;
import ai.labs.eddi.datastore.serialization.IJsonSerialization;
import ai.labs.eddi.engine.lifecycle.ILifecycleTask;
+import ai.labs.eddi.engine.lifecycle.TaskId;
import ai.labs.eddi.engine.lifecycle.exceptions.LifecycleException;
import ai.labs.eddi.engine.lifecycle.exceptions.WorkflowConfigurationException;
import ai.labs.eddi.engine.memory.IConversationMemory;
@@ -62,8 +63,9 @@
*/
@ApplicationScoped
public class McpCallsTask implements ILifecycleTask {
-
public static final String ID = "ai.labs.mcpcalls";
+ public static final TaskId TASK_ID = new TaskId(ID);
+
private static final String KEY_ACTIONS = "actions";
private static final String KEY_MCP_CALLS = "mcpCalls";
@@ -86,8 +88,8 @@ public McpCallsTask(IResourceClientLibrary resourceClientLibrary, IMemoryItemCon
}
@Override
- public String getId() {
- return ID;
+ public TaskId getId() {
+ return TASK_ID;
}
@Override
@@ -277,7 +279,7 @@ public Object configure(Map configuration, Map e
@Override
public ExtensionDescriptor getExtensionDescriptor() {
- ExtensionDescriptor descriptor = new ExtensionDescriptor(ID);
+ ExtensionDescriptor descriptor = new ExtensionDescriptor(new TaskId(ID));
descriptor.setDisplayName("MCP Calls");
descriptor.getConfigs().put("uri", new ExtensionDescriptor.ConfigValue("Resource URI", ExtensionDescriptor.FieldType.URI, false, null));
return descriptor;
diff --git a/src/main/java/ai/labs/eddi/modules/nlp/InputParserTask.java b/src/main/java/ai/labs/eddi/modules/nlp/InputParserTask.java
index 5be182904f..fa0ec2dc0c 100644
--- a/src/main/java/ai/labs/eddi/modules/nlp/InputParserTask.java
+++ b/src/main/java/ai/labs/eddi/modules/nlp/InputParserTask.java
@@ -7,6 +7,7 @@
import ai.labs.eddi.configs.workflows.model.ExtensionDescriptor;
import ai.labs.eddi.configs.workflows.model.ExtensionDescriptor.ConfigValue;
import ai.labs.eddi.engine.lifecycle.ILifecycleTask;
+import ai.labs.eddi.engine.lifecycle.TaskId;
import ai.labs.eddi.engine.lifecycle.exceptions.IllegalExtensionConfigurationException;
import ai.labs.eddi.engine.lifecycle.exceptions.WorkflowConfigurationException;
import ai.labs.eddi.engine.lifecycle.exceptions.UnrecognizedExtensionException;
@@ -61,6 +62,8 @@
@ApplicationScoped
public class InputParserTask implements ILifecycleTask {
public static final String ID = "ai.labs.parser";
+ public static final TaskId TASK_ID = new TaskId(ID);
+
private static final String CONFIG_APPEND_EXPRESSIONS = "appendExpressions";
private static final String CONFIG_INCLUDE_UNUSED = "includeUnused";
private static final String CONFIG_INCLUDE_UNKNOWN = "includeUnknown";
@@ -94,8 +97,8 @@ public InputParserTask(IExpressionProvider expressionProvider,
}
@Override
- public String getId() {
- return "ai.labs.parser";
+ public TaskId getId() {
+ return TASK_ID;
}
@Override
@@ -258,11 +261,11 @@ private List