diff --git a/pom.xml b/pom.xml
index 910570b36..b26acb29c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,9 +38,9 @@
3.12.13.7.03.4.2
- 11
- 11
- 11
+ 17
+ 17
+ 17true2.16.2true
@@ -715,6 +715,7 @@
testcontainers-daprdurabletask-client
+ quarkus
diff --git a/quarkus/README.md b/quarkus/README.md
new file mode 100644
index 000000000..a6379ee17
--- /dev/null
+++ b/quarkus/README.md
@@ -0,0 +1,2 @@
+# quarkus-agentic-dapr
+Quarkus Agentic Dapr Workflows integration
diff --git a/quarkus/deployment/pom.xml b/quarkus/deployment/pom.xml
new file mode 100644
index 000000000..bb75c756d
--- /dev/null
+++ b/quarkus/deployment/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+
+ io.dapr.quarkus
+ dapr-quarkus-agentic-parent
+ 1.18.0-SNAPSHOT
+ ../pom.xml
+
+
+ quarkus-agentic-dapr-deployment
+ Quarkus Agentic Dapr - Deployment
+
+
+
+ io.quarkiverse.dapr
+ quarkus-dapr-deployment
+ ${quarkus-dapr.version}
+
+
+ io.quarkiverse.langchain4j
+ quarkus-langchain4j-agentic-deployment
+ ${quarkus-langchain4j.version}
+
+
+ io.quarkus
+ quarkus-arc-deployment
+
+
+ io.quarkus.gizmo
+ gizmo
+
+
+ io.dapr.quarkus
+ quarkus-agentic-dapr
+ ${project.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${quarkus.version}
+
+
+
+
+
+
+
diff --git a/quarkus/deployment/src/main/java/io/quarkiverse/dapr/langchain4j/deployment/DaprAgenticProcessor.java b/quarkus/deployment/src/main/java/io/quarkiverse/dapr/langchain4j/deployment/DaprAgenticProcessor.java
new file mode 100644
index 000000000..689a6fd81
--- /dev/null
+++ b/quarkus/deployment/src/main/java/io/quarkiverse/dapr/langchain4j/deployment/DaprAgenticProcessor.java
@@ -0,0 +1,647 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.langchain4j.deployment;
+
+import io.quarkiverse.dapr.deployment.items.WorkflowItemBuildItem;
+import io.quarkiverse.dapr.langchain4j.agent.AgentRunLifecycleManager;
+import io.quarkiverse.dapr.langchain4j.agent.DaprAgentMetadataHolder;
+import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
+import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
+import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
+import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
+import io.quarkus.arc.processor.AnnotationsTransformer;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
+import io.quarkus.deployment.builditem.FeatureBuildItem;
+import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
+import io.quarkus.gizmo.CatchBlockCreator;
+import io.quarkus.gizmo.ClassCreator;
+import io.quarkus.gizmo.ClassOutput;
+import io.quarkus.gizmo.FieldCreator;
+import io.quarkus.gizmo.FieldDescriptor;
+import io.quarkus.gizmo.MethodCreator;
+import io.quarkus.gizmo.MethodDescriptor;
+import io.quarkus.gizmo.ResultHandle;
+import io.quarkus.gizmo.TryBlock;
+import jakarta.annotation.Priority;
+import jakarta.decorator.Decorator;
+import jakarta.decorator.Delegate;
+import jakarta.enterprise.context.Dependent;
+import jakarta.enterprise.inject.Any;
+import jakarta.inject.Inject;
+import jakarta.interceptor.Interceptor;
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationTarget;
+import org.jboss.jandex.AnnotationValue;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.IndexView;
+import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.Type;
+import org.jboss.logging.Logger;
+
+import java.lang.reflect.Modifier;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Quarkus deployment processor for the Dapr Agentic extension.
+ *
+ *
{@code DaprWorkflowProcessor.searchWorkflows()} uses {@code ApplicationIndexBuildItem}
+ * which only indexes application classes -- extension runtime JARs are invisible to it.
+ * We fix this in two steps:
+ *
+ *
Produce an {@link IndexDependencyBuildItem} so our runtime JAR is indexed into
+ * the {@link CombinedIndexBuildItem} (and visible to Arc for CDI bean discovery).
+ *
Consume the {@link CombinedIndexBuildItem}, look up our Workflow and WorkflowActivity
+ * classes, and produce {@link WorkflowItemBuildItem} instances that the existing
+ * {@code DaprWorkflowProcessor} build steps consume to register with the Dapr
+ * workflow runtime.
+ *
Produce {@link AdditionalBeanBuildItem} instances so Arc explicitly discovers
+ * our Workflow and WorkflowActivity classes as CDI beans.
+ *
Apply {@code @DaprAgentToolInterceptorBinding} to all {@code @Tool}-annotated
+ * methods automatically so that {@code DaprToolCallInterceptor} routes those calls
+ * through Dapr Workflow Activities without requiring user code changes.
+ *
Generate a CDI {@code @Decorator} for every {@code @Agent}-annotated interface
+ * so the {@link AgentRunLifecycleManager} workflow is started at the very beginning
+ * of the agent method call -- before LangChain4j assembles the prompt -- giving Dapr
+ * full observability of the agent's lifecycle from its first instruction.
+ *
+ */
+public class DaprAgenticProcessor {
+
+ private static final Logger LOG = Logger.getLogger(DaprAgenticProcessor.class);
+
+ private static final String FEATURE = "dapr-agentic";
+
+ /**
+ * Generated decorator classes live in this package to avoid polluting user packages.
+ */
+ private static final String DECORATOR_PACKAGE = "io.quarkiverse.dapr.langchain4j.generated";
+
+ /**
+ * LangChain4j {@code @Tool} annotation (on CDI bean methods).
+ */
+ private static final DotName TOOL_ANNOTATION =
+ DotName.createSimple("dev.langchain4j.agent.tool.Tool");
+
+ /**
+ * LangChain4j {@code @Agent} annotation (on AiService interface methods).
+ */
+ private static final DotName AGENT_ANNOTATION =
+ DotName.createSimple("dev.langchain4j.agentic.Agent");
+
+ /**
+ * LangChain4j {@code @UserMessage} annotation.
+ */
+ private static final DotName USER_MESSAGE_ANNOTATION =
+ DotName.createSimple("dev.langchain4j.service.UserMessage");
+
+ /**
+ * LangChain4j {@code @SystemMessage} annotation.
+ */
+ private static final DotName SYSTEM_MESSAGE_ANNOTATION =
+ DotName.createSimple("dev.langchain4j.service.SystemMessage");
+
+ /**
+ * Our interceptor binding that triggers {@code DaprToolCallInterceptor}.
+ */
+ private static final DotName DAPR_TOOL_INTERCEPTOR_BINDING = DotName.createSimple(
+ "io.quarkiverse.dapr.langchain4j.agent.DaprAgentToolInterceptorBinding");
+
+ /**
+ * Our interceptor binding that triggers {@code DaprAgentMethodInterceptor}.
+ */
+ private static final DotName DAPR_AGENT_INTERCEPTOR_BINDING = DotName.createSimple(
+ "io.quarkiverse.dapr.langchain4j.agent.DaprAgentInterceptorBinding");
+
+ /**
+ * {@code @WorkflowMetadata} annotation for custom workflow registration names.
+ */
+ private static final DotName WORKFLOW_METADATA_DOTNAME = DotName.createSimple(
+ "io.quarkiverse.dapr.workflows.WorkflowMetadata");
+
+ /**
+ * {@code @ActivityMetadata} annotation for custom activity registration names.
+ */
+ private static final DotName ACTIVITY_METADATA_DOTNAME = DotName.createSimple(
+ "io.quarkiverse.dapr.workflows.ActivityMetadata");
+
+ private static final String[] WORKFLOW_CLASSES = {
+ "io.quarkiverse.dapr.langchain4j.workflow.orchestration.SequentialOrchestrationWorkflow",
+ "io.quarkiverse.dapr.langchain4j.workflow.orchestration.ParallelOrchestrationWorkflow",
+ "io.quarkiverse.dapr.langchain4j.workflow.orchestration.LoopOrchestrationWorkflow",
+ "io.quarkiverse.dapr.langchain4j.workflow.orchestration.ConditionalOrchestrationWorkflow",
+ // Per-agent workflow (one per @Agent invocation)
+ "io.quarkiverse.dapr.langchain4j.agent.workflow.AgentRunWorkflow",
+ };
+
+ private static final String[] ACTIVITY_CLASSES = {
+ "io.quarkiverse.dapr.langchain4j.workflow.orchestration.activities.AgentExecutionActivity",
+ "io.quarkiverse.dapr.langchain4j.workflow.orchestration.activities.ExitConditionCheckActivity",
+ "io.quarkiverse.dapr.langchain4j.workflow.orchestration.activities.ConditionCheckActivity",
+ // Per-tool-call activity
+ "io.quarkiverse.dapr.langchain4j.agent.activities.ToolCallActivity",
+ // Per-LLM-call activity
+ "io.quarkiverse.dapr.langchain4j.agent.activities.LlmCallActivity",
+ };
+
+ @BuildStep
+ FeatureBuildItem feature() {
+ return new FeatureBuildItem(FEATURE);
+ }
+
+ /**
+ * Index our runtime JAR so its classes appear in {@link CombinedIndexBuildItem}
+ * and are discoverable by Arc for CDI bean creation.
+ */
+ @BuildStep
+ IndexDependencyBuildItem indexRuntimeModule() {
+ return new IndexDependencyBuildItem("io.dapr.quarkus", "quarkus-agentic-dapr");
+ }
+
+ /**
+ * Produce {@link WorkflowItemBuildItem} for each of our Workflow and WorkflowActivity
+ * classes.
+ */
+ @BuildStep
+ void registerWorkflowsAndActivities(CombinedIndexBuildItem combinedIndex,
+ BuildProducer workflowItems) {
+ IndexView index = combinedIndex.getIndex();
+
+ for (String className : WORKFLOW_CLASSES) {
+ ClassInfo classInfo = index.getClassByName(DotName.createSimple(className));
+ if (classInfo != null) {
+ String regName = null;
+ String version = null;
+ Boolean isLatest = null;
+ AnnotationInstance meta = classInfo.annotation(WORKFLOW_METADATA_DOTNAME);
+ if (meta != null) {
+ regName = stringValueOrNull(meta, "name");
+ version = stringValueOrNull(meta, "version");
+ AnnotationValue isLatestVal = meta.value("isLatest");
+ if (isLatestVal != null) {
+ isLatest = isLatestVal.asBoolean();
+ }
+ }
+ workflowItems.produce(new WorkflowItemBuildItem(
+ classInfo, WorkflowItemBuildItem.Type.WORKFLOW, regName, version, isLatest));
+ }
+ }
+
+ for (String className : ACTIVITY_CLASSES) {
+ ClassInfo classInfo = index.getClassByName(DotName.createSimple(className));
+ if (classInfo != null) {
+ String regName = null;
+ AnnotationInstance meta = classInfo.annotation(ACTIVITY_METADATA_DOTNAME);
+ if (meta != null) {
+ regName = stringValueOrNull(meta, "name");
+ }
+ workflowItems.produce(new WorkflowItemBuildItem(
+ classInfo, WorkflowItemBuildItem.Type.WORKFLOW_ACTIVITY, regName, null, null));
+ }
+ }
+ }
+
+ /**
+ * Explicitly register our Workflow, WorkflowActivity, and CDI interceptor classes as beans.
+ */
+ @BuildStep
+ void registerAdditionalBeans(BuildProducer additionalBeans) {
+ for (String className : WORKFLOW_CLASSES) {
+ additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(className));
+ }
+ for (String className : ACTIVITY_CLASSES) {
+ additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(className));
+ }
+ // AgentRunLifecycleManager is injected by generated decorators and must be discoverable.
+ additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(
+ AgentRunLifecycleManager.class.getName()));
+ // CDI interceptors must be registered as unremovable beans.
+ additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(
+ "io.quarkiverse.dapr.langchain4j.agent.DaprToolCallInterceptor"));
+ additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(
+ "io.quarkiverse.dapr.langchain4j.agent.DaprAgentMethodInterceptor"));
+ // CDI decorator that wraps ChatModel to route LLM calls through Dapr activities.
+ // A decorator is used instead of an interceptor because quarkus-langchain4j registers
+ // ChatModel as a synthetic bean, and Arc does not apply CDI interceptors to synthetic
+ // beans via AnnotationsTransformer -- but it DOES apply decorators at the type level.
+ additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(
+ "io.quarkiverse.dapr.langchain4j.agent.DaprChatModelDecorator"));
+ }
+
+ /**
+ * Generates a CDI {@code @Decorator} for every interface that declares at least one
+ * {@code @Agent}-annotated method.
+ *
+ *
Why a generated decorator?
+ * quarkus-langchain4j registers {@code @Agent} AiService beans as synthetic beans
+ * (via {@code SyntheticBeanBuildItem}) -- CDI interceptors applied via
+ * {@code AnnotationsTransformer} are silently ignored on synthetic beans. CDI
+ * decorators, however, are matched at the bean type level and are applied
+ * by Arc to all beans (including synthetic beans) whose type includes the delegate type.
+ * This is the same mechanism used by
+ * {@link io.quarkiverse.dapr.langchain4j.agent.DaprChatModelDecorator}
+ * to wrap the synthetic {@code ChatModel} bean.
+ *
+ *
What the generated decorator does
+ * For each interface {@code I} with at least one {@code @Agent} method, Gizmo emits a class
+ * equivalent to:
+ *
+ */
+ private void generateDecoratedAgentMethod(ClassCreator cc, MethodInfo method,
+ FieldDescriptor delegateDesc, FieldDescriptor lcmDesc) {
+
+ String agentName = extractAgentName(method);
+ String userMessage = extractAnnotationText(method, USER_MESSAGE_ANNOTATION);
+ String systemMessage = extractAnnotationText(method, SYSTEM_MESSAGE_ANNOTATION);
+ final boolean isVoid = method.returnType().kind() == Type.Kind.VOID;
+
+ MethodCreator mc = cc.getMethodCreator(MethodDescriptor.of(method));
+ mc.setModifiers(Modifier.PUBLIC);
+ for (Type exType : method.exceptions()) {
+ mc.addException(exType.name().toString());
+ }
+
+ // Store @Agent metadata on the current thread so that DaprChatModelDecorator can
+ // retrieve the real agent name and messages if the activation below fails and the
+ // decorator falls through to direct delegation (lazy-activation path).
+ mc.invokeStaticMethod(
+ MethodDescriptor.ofMethod(DaprAgentMetadataHolder.class, "set",
+ void.class, String.class, String.class, String.class),
+ mc.load(agentName),
+ userMessage != null ? mc.load(userMessage) : mc.loadNull(),
+ systemMessage != null ? mc.load(systemMessage) : mc.loadNull());
+
+ // Try to activate the Dapr agent lifecycle. This may fail when running on
+ // threads without a CDI request scope (e.g., LangChain4j's parallel executor).
+ // In that case, fall through to a direct delegate call without Dapr routing.
+ TryBlock activateTry = mc.tryBlock();
+ ResultHandle lcm = activateTry.readInstanceField(lcmDesc, activateTry.getThis());
+ activateTry.invokeVirtualMethod(
+ MethodDescriptor.ofMethod(AgentRunLifecycleManager.class, "getOrActivate",
+ String.class, String.class, String.class, String.class),
+ lcm,
+ activateTry.load(agentName),
+ userMessage != null
+ ? activateTry.load(userMessage) : activateTry.loadNull(),
+ systemMessage != null
+ ? activateTry.load(systemMessage) : activateTry.loadNull());
+
+ // If activation fails (no request scope), delegate directly without Dapr routing.
+ CatchBlockCreator activateCatch = activateTry.addCatch(Throwable.class);
+ {
+ ResultHandle delFallback = activateCatch.readInstanceField(
+ delegateDesc, activateCatch.getThis());
+ ResultHandle[] fallbackParams = new ResultHandle[method.parametersCount()];
+ for (int i = 0; i < fallbackParams.length; i++) {
+ fallbackParams[i] = activateCatch.getMethodParam(i);
+ }
+ if (isVoid) {
+ activateCatch.invokeInterfaceMethod(
+ MethodDescriptor.of(method), delFallback, fallbackParams);
+ activateCatch.returnVoid();
+ } else {
+ ResultHandle fallbackResult = activateCatch.invokeInterfaceMethod(
+ MethodDescriptor.of(method), delFallback, fallbackParams);
+ activateCatch.returnValue(fallbackResult);
+ }
+ }
+
+ // Activation succeeded -- wrap the delegate call with triggerDone() on both paths.
+ // try { ... } catch (Throwable t) { ... }
+ TryBlock tryBlock = mc.tryBlock();
+
+ ResultHandle del = tryBlock.readInstanceField(delegateDesc, tryBlock.getThis());
+ ResultHandle[] params = new ResultHandle[method.parametersCount()];
+ for (int i = 0; i < params.length; i++) {
+ params[i] = tryBlock.getMethodParam(i);
+ }
+
+ // Normal path: delegate call + triggerDone + return
+ ResultHandle result = null;
+ if (!isVoid) {
+ result = tryBlock.invokeInterfaceMethod(
+ MethodDescriptor.of(method), del, params);
+ } else {
+ tryBlock.invokeInterfaceMethod(MethodDescriptor.of(method), del, params);
+ }
+
+ ResultHandle lcmInTry = tryBlock.readInstanceField(lcmDesc, tryBlock.getThis());
+ tryBlock.invokeVirtualMethod(
+ MethodDescriptor.ofMethod(
+ AgentRunLifecycleManager.class, "triggerDone", void.class),
+ lcmInTry);
+ tryBlock.invokeStaticMethod(
+ MethodDescriptor.ofMethod(DaprAgentMetadataHolder.class, "clear", void.class));
+
+ if (isVoid) {
+ tryBlock.returnVoid();
+ } else {
+ tryBlock.returnValue(result);
+ }
+
+ // Exception path: triggerDone + rethrow
+ CatchBlockCreator catchBlock = tryBlock.addCatch(Throwable.class);
+ ResultHandle lcmInCatch =
+ catchBlock.readInstanceField(lcmDesc, catchBlock.getThis());
+ catchBlock.invokeVirtualMethod(
+ MethodDescriptor.ofMethod(
+ AgentRunLifecycleManager.class, "triggerDone", void.class),
+ lcmInCatch);
+ catchBlock.invokeStaticMethod(
+ MethodDescriptor.ofMethod(DaprAgentMetadataHolder.class, "clear", void.class));
+ catchBlock.throwException(catchBlock.getCaughtException());
+ }
+
+ /**
+ * Generates a trivial delegation body for non-{@code @Agent} abstract interface methods.
+ *
+ * return delegate.method(params); // or just delegate.method(params); for void
+ *
+ */
+ private void generateDelegateMethod(ClassCreator cc, MethodInfo method,
+ FieldDescriptor delegateDesc) {
+
+ final boolean isVoid = method.returnType().kind() == Type.Kind.VOID;
+
+ MethodCreator mc = cc.getMethodCreator(MethodDescriptor.of(method));
+ mc.setModifiers(Modifier.PUBLIC);
+ for (Type exType : method.exceptions()) {
+ mc.addException(exType.name().toString());
+ }
+
+ ResultHandle del = mc.readInstanceField(delegateDesc, mc.getThis());
+ ResultHandle[] params = new ResultHandle[method.parametersCount()];
+ for (int i = 0; i < params.length; i++) {
+ params[i] = mc.getMethodParam(i);
+ }
+
+ if (isVoid) {
+ mc.invokeInterfaceMethod(MethodDescriptor.of(method), del, params);
+ mc.returnVoid();
+ } else {
+ ResultHandle result = mc.invokeInterfaceMethod(
+ MethodDescriptor.of(method), del, params);
+ mc.returnValue(result);
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Annotation metadata extraction (Jandex)
+ // -------------------------------------------------------------------------
+
+ /**
+ * Returns the {@code @Agent(name)} value if non-blank, otherwise falls back to
+ * {@code InterfaceName.methodName}.
+ */
+ private String extractAgentName(MethodInfo method) {
+ AnnotationInstance agent = method.annotation(AGENT_ANNOTATION);
+ if (agent != null) {
+ AnnotationValue nameVal = agent.value("name");
+ if (nameVal != null && !nameVal.asString().isBlank()) {
+ return nameVal.asString();
+ }
+ }
+ return method.declaringClass().name().withoutPackagePrefix()
+ + "." + method.name();
+ }
+
+ /**
+ * Returns the joined text of a {@code String[] value()} annotation attribute, or
+ * {@code null} when the annotation is absent or its value is empty.
+ *
+ *
Handles both the single-string form ({@code @UserMessage("text")}) and the
+ * array form ({@code @UserMessage({"line1", "line2"})}).
+ */
+ private String extractAnnotationText(MethodInfo method, DotName annotationName) {
+ AnnotationInstance annotation = method.annotation(annotationName);
+ if (annotation == null) {
+ return null;
+ }
+ AnnotationValue value = annotation.value(); // "value" is the default attribute
+ if (value == null) {
+ return null;
+ }
+ if (value.kind() == AnnotationValue.Kind.ARRAY) {
+ String[] parts = value.asStringArray();
+ return parts.length == 0 ? null : String.join("\n", parts);
+ }
+ // single String stored directly (rare but defensively handled)
+ return value.asString();
+ }
+
+ // -------------------------------------------------------------------------
+ // Annotation metadata extraction helpers
+ // -------------------------------------------------------------------------
+
+ private static String stringValueOrNull(AnnotationInstance annotation, String name) {
+ AnnotationValue value = annotation.value(name);
+ if (value == null) {
+ return null;
+ }
+ String sv = value.asString();
+ return (sv == null || sv.isEmpty()) ? null : sv;
+ }
+
+ // -------------------------------------------------------------------------
+ // Interceptor / annotation-transformer build steps (unchanged)
+ // -------------------------------------------------------------------------
+
+ /**
+ * Automatically apply {@code @DaprAgentToolInterceptorBinding} to every
+ * {@code @Tool}-annotated method in the application index.
+ */
+ @BuildStep
+ @SuppressWarnings("deprecation")
+ AnnotationsTransformerBuildItem addDaprInterceptorToToolMethods() {
+ return new AnnotationsTransformerBuildItem(
+ AnnotationsTransformer.appliedToMethod()
+ .whenMethod(m -> m.hasAnnotation(TOOL_ANNOTATION))
+ .thenTransform(t -> t.add(DAPR_TOOL_INTERCEPTOR_BINDING)));
+ }
+
+ /**
+ * Automatically apply {@code @DaprAgentInterceptorBinding} to every
+ * {@code @Agent}-annotated method in the application index.
+ *
+ *
This causes {@link io.quarkiverse.dapr.langchain4j.agent.DaprAgentMethodInterceptor}
+ * to fire when an {@code @Agent} method is called on a regular CDI bean (not a synthetic
+ * AiService bean). For synthetic AiService beans the generated CDI decorator (produced by
+ * {@link #generateAgentDecorators}) is the authoritative hook point.
+ */
+ @BuildStep
+ @SuppressWarnings("deprecation")
+ AnnotationsTransformerBuildItem addDaprInterceptorToAgentMethods() {
+ return new AnnotationsTransformerBuildItem(
+ AnnotationsTransformer.appliedToMethod()
+ .whenMethod(m -> m.hasAnnotation(AGENT_ANNOTATION))
+ .thenTransform(t -> t.add(DAPR_AGENT_INTERCEPTOR_BINDING)));
+ }
+
+ /**
+ * Also apply the interceptor binding at the class level for any CDI bean whose
+ * declared class itself has {@code @Tool} (less common but supported by LangChain4j).
+ */
+ @BuildStep
+ @SuppressWarnings("deprecation")
+ AnnotationsTransformerBuildItem addDaprInterceptorToToolClasses() {
+ return new AnnotationsTransformerBuildItem(
+ AnnotationsTransformer.appliedToClass()
+ .whenClass(c -> {
+ for (MethodInfo method : c.methods()) {
+ if (method.hasAnnotation(TOOL_ANNOTATION)) {
+ return true;
+ }
+ }
+ return false;
+ })
+ .thenTransform(t -> t.add(DAPR_TOOL_INTERCEPTOR_BINDING)));
+ }
+}
diff --git a/quarkus/examples/pom.xml b/quarkus/examples/pom.xml
new file mode 100644
index 000000000..2e8ac2639
--- /dev/null
+++ b/quarkus/examples/pom.xml
@@ -0,0 +1,95 @@
+
+
+ 4.0.0
+
+ io.dapr.quarkus
+ dapr-quarkus-agentic-parent
+ 1.18.0-SNAPSHOT
+ ../pom.xml
+
+
+ quarkus-agentic-dapr-examples
+ Quarkus Agentic Dapr - Examples
+
+
+
+
+ io.dapr.quarkus
+ quarkus-agentic-dapr
+ ${project.version}
+
+
+
+ io.dapr.quarkus
+ quarkus-agentic-dapr-agents-registry
+ ${project.version}
+
+
+
+
+ io.quarkiverse.langchain4j
+ quarkus-langchain4j-openai
+ ${quarkus-langchain4j.version}
+
+
+
+
+ io.quarkus
+ quarkus-rest
+
+
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+ ${quarkus.version}
+
+
+
+ build
+
+
+
+
+
+ maven-compiler-plugin
+ 3.13.0
+
+
+ -parameters
+
+
+
+
+ maven-surefire-plugin
+ 3.5.2
+
+
+ org.jboss.logmanager.LogManager
+
+
+
+
+
+
diff --git a/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/CreativeWriter.java b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/CreativeWriter.java
new file mode 100644
index 000000000..ec67d9a47
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/CreativeWriter.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.examples;
+
+import dev.langchain4j.agentic.Agent;
+import dev.langchain4j.service.UserMessage;
+import dev.langchain4j.service.V;
+import io.quarkiverse.langchain4j.ToolBox;
+
+/**
+ * Sub-agent that generates a creative story draft based on a given topic.
+ */
+public interface CreativeWriter {
+
+ @UserMessage("""
+ You are a creative writer.
+ Generate a draft of a story no more than 3 sentences around the given topic.
+ Return only the story and nothing else.
+ The topic is {{topic}}.
+ """)
+ @Agent(name = "creative-writer-agent",
+ description = "Generate a story based on the given topic", outputKey = "story")
+ String generateStory(@V("topic") String topic);
+}
diff --git a/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ParallelCreator.java b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ParallelCreator.java
new file mode 100644
index 000000000..30277c329
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ParallelCreator.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.examples;
+
+import dev.langchain4j.agentic.declarative.Output;
+import dev.langchain4j.agentic.declarative.ParallelAgent;
+import dev.langchain4j.service.V;
+
+/**
+ * Composite agent that orchestrates {@link StoryCreator} and {@link ResearchWriter}
+ * in parallel, backed by a Dapr Workflow.
+ *
+ *
Both sub-agents execute concurrently via a {@code ParallelOrchestrationWorkflow}.
+ * {@link StoryCreator} is itself a {@code @SequenceAgent} that chains
+ * {@link CreativeWriter} and {@link StyleEditor} — demonstrating nested composite agents.
+ * Meanwhile {@link ResearchWriter} gathers facts about the country.
+ */
+public interface ParallelCreator {
+
+ @ParallelAgent(name = "parallel-creator-agent",
+ outputKey = "storyAndCountryResearch",
+ subAgents = { StoryCreator.class, ResearchWriter.class })
+ ParallelStatus create(@V("topic") String topic, @V("country") String country, @V("style") String style);
+
+ /**
+ * Produces the final output from the parallel agent results.
+ *
+ * @param story the generated story
+ * @param summary the generated summary
+ * @return the combined parallel status
+ */
+ @Output
+ static ParallelStatus output(String story, String summary) {
+ if (story == null || summary == null) {
+ return new ParallelStatus("ERROR", story, summary);
+ }
+ return new ParallelStatus("OK", story, summary);
+ }
+}
diff --git a/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ParallelResource.java b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ParallelResource.java
new file mode 100644
index 000000000..52a44879b
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ParallelResource.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.examples;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+
+/**
+ * REST endpoint that triggers the parallel creation workflow.
+ *
+ *
Runs {@link StoryCreator} (a nested {@code @SequenceAgent}) and {@link ResearchWriter}
+ * in parallel via a {@code ParallelOrchestrationWorkflow} Dapr Workflow.
+ *
+ *
+ */
+@Path("/parallel")
+public class ParallelResource {
+
+ @Inject
+ ParallelCreator parallelCreator;
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ public ParallelStatus create(
+ @QueryParam("topic") @DefaultValue("dragons and wizards") String topic,
+ @QueryParam("country") @DefaultValue("France") String country,
+ @QueryParam("style") @DefaultValue("fantasy") String style) {
+ return parallelCreator.create(topic, country, style);
+ }
+}
diff --git a/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ParallelStatus.java b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ParallelStatus.java
new file mode 100644
index 000000000..f4737d3b3
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ParallelStatus.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.examples;
+
+public record ParallelStatus(String status, String story, String summary) {
+}
diff --git a/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ResearchResource.java b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ResearchResource.java
new file mode 100644
index 000000000..db3e5e4e6
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ResearchResource.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.examples;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+
+/**
+ * REST endpoint that triggers a research workflow with tool calls routed through
+ * Dapr Workflow Activities.
+ *
+ *
Each request:
+ *
+ *
Starts a {@code SequentialOrchestrationWorkflow} (orchestration level).
+ *
For the {@link ResearchWriter} sub-agent, starts an {@code AgentRunWorkflow}
+ * (per-agent level).
+ *
Each LLM tool call ({@code getPopulation} / {@code getCapital}) is executed
+ * inside a {@code ToolCallActivity} (tool-call level).
+ */
+@Path("/research")
+public class ResearchResource {
+
+ @Inject
+ ResearchWriter researchWriter;
+
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ public String research(
+ @QueryParam("country") @DefaultValue("France") String country) {
+ return researchWriter.research(country);
+ }
+}
diff --git a/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ResearchTools.java b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ResearchTools.java
new file mode 100644
index 000000000..9af9e2918
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ResearchTools.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.examples;
+
+import dev.langchain4j.agent.tool.Tool;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/**
+ * CDI bean providing research tools for the {@link ResearchWriter} agent.
+ *
+ *
Because the {@code quarkus-agentic-dapr} extension automatically applies
+ * {@code @DaprAgentToolInterceptorBinding} to all {@code @Tool}-annotated methods at
+ * build time, every call to these methods that occurs inside a Dapr-backed agent run is
+ * transparently routed through a {@code ToolCallActivity} Dapr Workflow Activity.
+ *
+ *
This means:
+ *
+ *
Each tool call is recorded in the Dapr Workflow history.
+ *
If the process crashes during a tool call, Dapr retries the activity automatically.
+ *
No code changes are needed here — the routing is applied automatically.
+ *
+ */
+@ApplicationScoped
+public class ResearchTools {
+
+ /**
+ * Looks up real-time population data for a given country.
+ *
+ * @param country the country to look up
+ * @return population data string
+ */
+ @Tool("Looks up real-time population data for a given country")
+ public String getPopulation(String country) {
+ // In a real implementation this would call an external API.
+ // Here we return a stub so the example runs without network access.
+ return switch (country.toLowerCase()) {
+ case "france" -> "France has approximately 68 million inhabitants (2024).";
+ case "germany" -> "Germany has approximately 84 million inhabitants (2024).";
+ case "japan" -> "Japan has approximately 124 million inhabitants (2024).";
+ default -> country + " population data is not available in this demo.";
+ };
+ }
+
+ /**
+ * Returns the official capital city of a given country.
+ *
+ * @param country the country to look up
+ * @return capital city string
+ */
+ @Tool("Returns the official capital city of a given country")
+ public String getCapital(String country) {
+ return switch (country.toLowerCase()) {
+ case "france" -> "The capital of France is Paris.";
+ case "germany" -> "The capital of Germany is Berlin.";
+ case "japan" -> "The capital of Japan is Tokyo.";
+ default -> "Capital city for " + country + " is not available in this demo.";
+ };
+ }
+}
diff --git a/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ResearchWriter.java b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ResearchWriter.java
new file mode 100644
index 000000000..aa6f2c183
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/ResearchWriter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.examples;
+
+import dev.langchain4j.agentic.Agent;
+import dev.langchain4j.service.UserMessage;
+import dev.langchain4j.service.V;
+import io.quarkiverse.langchain4j.ToolBox;
+
+/**
+ * Sub-agent that writes a short research summary about a country by calling tools.
+ *
+ *
The {@link ToolBox} annotation tells quarkus-langchain4j to make {@link ResearchTools}
+ * available to the LLM for this agent's method. When the LLM decides to call
+ * {@code getPopulation} or {@code getCapital}, the call is intercepted by the Dapr
+ * extension and executed inside a {@code ToolCallActivity} Dapr Workflow Activity —
+ * providing a durable, auditable record of every tool invocation.
+ *
+ *
Architecture note: No changes are required in this interface to enable the
+ * Dapr routing. The {@code quarkus-agentic-dapr} deployment module applies
+ * {@code @DaprAgentToolInterceptorBinding} to all {@code @Tool}-annotated methods at
+ * build time, and {@code DaprWorkflowPlanner} sets the per-agent context on the
+ * executing thread before the agent starts.
+ */
+public interface ResearchWriter {
+
+ @ToolBox(ResearchTools.class)
+ @UserMessage("""
+ You are a research assistant.
+ Write a concise 2-sentence summary about the country {{country}}
+ using the available tools to fetch accurate data.
+ Return only the summary.
+ """)
+ @Agent(name = "research-location-agent",
+ description = "Researches and summarises facts about a country", outputKey = "summary")
+ String research(@V("country") String country);
+}
diff --git a/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/StoryCreator.java b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/StoryCreator.java
new file mode 100644
index 000000000..26b6dd41e
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/StoryCreator.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.examples;
+
+import dev.langchain4j.agentic.declarative.SequenceAgent;
+import dev.langchain4j.service.V;
+
+/**
+ * Composite agent that orchestrates {@link CreativeWriter} and {@link StyleEditor}
+ * in sequence, backed by a Dapr Workflow.
+ *
+ *
Uses {@code @SequenceAgent} which discovers the {@code DaprWorkflowAgentsBuilder}
+ * via Java SPI to create the Dapr Workflow-based sequential orchestration.
+ */
+public interface StoryCreator {
+
+ @SequenceAgent(name = "story-creator-agent",
+ outputKey = "story",
+ subAgents = { CreativeWriter.class, StyleEditor.class })
+ String write(@V("topic") String topic, @V("style") String style);
+}
diff --git a/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/StoryResource.java b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/StoryResource.java
new file mode 100644
index 000000000..afabf11f5
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/StoryResource.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.examples;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+
+/**
+ * REST endpoint that triggers the sequential story creation workflow.
+ *
+ *
+ */
+@Path("/story")
+public class StoryResource {
+
+ @Inject
+ StoryCreator storyCreator;
+
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ public String createStory(
+ @QueryParam("topic") @DefaultValue("dragons and wizards") String topic,
+ @QueryParam("style") @DefaultValue("fantasy") String style) {
+ return storyCreator.write(topic, style);
+ }
+}
diff --git a/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/StyleEditor.java b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/StyleEditor.java
new file mode 100644
index 000000000..871c88d03
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/quarkiverse/dapr/examples/StyleEditor.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.examples;
+
+import dev.langchain4j.agentic.Agent;
+import dev.langchain4j.service.UserMessage;
+import dev.langchain4j.service.V;
+
+/**
+ * Sub-agent that edits a story to improve its writing style.
+ */
+public interface StyleEditor {
+
+ @UserMessage("""
+ You are a style editor.
+ Review the following story and improve its style to match the requested style: {{style}}.
+ Return only the improved story and nothing else.
+ Story: {{story}}
+ """)
+ @Agent(name = "style-editor-agent", description = "Edit a story to improve its writing style",
+ outputKey = "story")
+ String editStory(@V("story") String story, @V("style") String style);
+}
diff --git a/quarkus/examples/src/main/resources/application.properties b/quarkus/examples/src/main/resources/application.properties
new file mode 100644
index 000000000..a1c050062
--- /dev/null
+++ b/quarkus/examples/src/main/resources/application.properties
@@ -0,0 +1,26 @@
+# Dapr Dev Services (automatically starts Dapr sidecar, placement, scheduler, state store)
+quarkus.dapr.devservices.enabled=true
+quarkus.dapr.workflow.enabled=true
+# OpenAI configuration
+# Set your API key via environment variable: export OPENAI_API_KEY=sk-...
+quarkus.langchain4j.openai.api-key=${OPENAI_API_KEY:demo}
+quarkus.langchain4j.openai.chat-model.model-name=gpt-4o-mini
+
+quarkus.log.category."io.quarkiverse.dapr.agents.registry".level=DEBUG
+
+# OpenTelemetry Configuration
+quarkus.otel.propagators=tracecontext,baggage
+
+# LangChain4j Tracing - gen_ai spans
+quarkus.langchain4j.tracing.include-prompt=true
+quarkus.langchain4j.tracing.include-completion=true
+quarkus.langchain4j.tracing.include-tool-arguments=true
+quarkus.langchain4j.tracing.include-tool-result=true
+
+# Dapr Workflows
+quarkus.log.category."io.quarkiverse.dapr.workflows".level=DEBUG
+
+
+#dapr.agents.statestore=agent-registry
+#dapr.agents.team=default
+#dapr.appid=agentic-example
diff --git a/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/DaprWorkflowClientTest.java b/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/DaprWorkflowClientTest.java
new file mode 100644
index 000000000..228152710
--- /dev/null
+++ b/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/DaprWorkflowClientTest.java
@@ -0,0 +1,28 @@
+package io.quarkiverse.dapr.examples;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.dapr.workflows.client.DaprWorkflowClient;
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+
+/**
+ * Verifies that the Dapr infrastructure is properly started by dev services.
+ * Tests that the DaprWorkflowClient CDI bean is available and connected
+ * to the Dapr sidecar backed by PostgreSQL state store.
+ */
+@QuarkusTest
+@ExtendWith(DockerAvailableCondition.class)
+class DaprWorkflowClientTest {
+
+ @Inject
+ DaprWorkflowClient workflowClient;
+
+ @Test
+ void daprWorkflowClientShouldBeAvailable() {
+ assertNotNull(workflowClient, "DaprWorkflowClient should be injected by Dapr dev services");
+ }
+}
diff --git a/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/DockerAvailableCondition.java b/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/DockerAvailableCondition.java
new file mode 100644
index 000000000..0747891d1
--- /dev/null
+++ b/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/DockerAvailableCondition.java
@@ -0,0 +1,48 @@
+package io.quarkiverse.dapr.examples;
+
+import java.io.File;
+
+import org.junit.jupiter.api.extension.ConditionEvaluationResult;
+import org.junit.jupiter.api.extension.ExecutionCondition;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+/**
+ * JUnit 5 {@link ExecutionCondition} that disables tests when Docker is not available.
+ * This prevents Dapr Testcontainers-based integration tests from failing in
+ * environments without Docker (e.g., CI without Docker-in-Docker).
+ *
+ * Checks for Docker by looking for the Docker socket file or running
+ * {@code docker info}. Usage: annotate test classes with
+ * {@code @ExtendWith(DockerAvailableCondition.class)}.
+ */
+public class DockerAvailableCondition implements ExecutionCondition {
+
+ @Override
+ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
+ // Check common Docker socket locations
+ if (new File("/var/run/docker.sock").exists()) {
+ return ConditionEvaluationResult.enabled("Docker socket found at /var/run/docker.sock");
+ }
+
+ // Try Docker Desktop on macOS
+ String home = System.getProperty("user.home");
+ if (home != null && new File(home + "/.docker/run/docker.sock").exists()) {
+ return ConditionEvaluationResult.enabled("Docker socket found at ~/.docker/run/docker.sock");
+ }
+
+ // Fallback: try running 'docker info'
+ try {
+ Process process = new ProcessBuilder("docker", "info")
+ .redirectErrorStream(true)
+ .start();
+ int exitCode = process.waitFor();
+ if (exitCode == 0) {
+ return ConditionEvaluationResult.enabled("Docker is available (docker info succeeded)");
+ }
+ } catch (Exception e) {
+ // docker command not found or failed
+ }
+
+ return ConditionEvaluationResult.disabled("Docker is not available, skipping integration test");
+ }
+}
diff --git a/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/MockChatModel.java b/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/MockChatModel.java
new file mode 100644
index 000000000..2a0d4a232
--- /dev/null
+++ b/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/MockChatModel.java
@@ -0,0 +1,32 @@
+package io.quarkiverse.dapr.examples;
+
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.request.ChatRequest;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import dev.langchain4j.model.output.FinishReason;
+import dev.langchain4j.model.output.TokenUsage;
+import jakarta.annotation.Priority;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Alternative;
+
+/**
+ * Mock ChatModel that returns predictable responses for integration testing.
+ * Takes priority over the OpenAI ChatModel bean via {@code @Alternative @Priority(1)}.
+ */
+@Alternative
+@Priority(1)
+@ApplicationScoped
+public class MockChatModel implements ChatModel {
+
+ @Override
+ public ChatResponse doChat(ChatRequest request) {
+ return ChatResponse.builder()
+ .aiMessage(AiMessage.from("Once upon a time, a brave dragon befriended a wizard. "
+ + "Together they embarked on an epic adventure across enchanted lands. "
+ + "Their story became legend, told for generations."))
+ .tokenUsage(new TokenUsage(10, 30))
+ .finishReason(FinishReason.STOP)
+ .build();
+ }
+}
diff --git a/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/ParallelResourceTest.java b/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/ParallelResourceTest.java
new file mode 100644
index 000000000..03ce2263e
--- /dev/null
+++ b/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/ParallelResourceTest.java
@@ -0,0 +1,51 @@
+package io.quarkiverse.dapr.examples;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+/**
+ * Integration test for the parallel creation workflow.
+ *
+ * {@link ParallelCreator} runs {@link StoryCreator} (a nested {@code @SequenceAgent})
+ * and {@link ResearchWriter} in parallel, verifying that nested composite agents work
+ * correctly with Dapr Workflows.
+ *
+ * Requires Docker for Dapr dev services. Uses {@link MockChatModel} instead of a real LLM.
+ */
+@QuarkusTest
+@ExtendWith(DockerAvailableCondition.class)
+class ParallelResourceTest {
+
+ @Test
+ void testParallelEndpointReturnsResponse() {
+ given()
+ .queryParam("topic", "dragons")
+ .queryParam("country", "France")
+ .queryParam("style", "comedy")
+ .when()
+ .get("/parallel")
+ .then()
+ .statusCode(200)
+ .body("status", equalTo("OK"))
+ .body("story", notNullValue())
+ .body("summary", notNullValue());
+ }
+
+ @Test
+ void testParallelEndpointWithDefaultParams() {
+ given()
+ .when()
+ .get("/parallel")
+ .then()
+ .statusCode(200)
+ .body("status", equalTo("OK"))
+ .body("story", notNullValue())
+ .body("summary", notNullValue());
+ }
+}
\ No newline at end of file
diff --git a/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/StoryResourceTest.java b/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/StoryResourceTest.java
new file mode 100644
index 000000000..d0d110bce
--- /dev/null
+++ b/quarkus/examples/src/test/java/io/quarkiverse/dapr/examples/StoryResourceTest.java
@@ -0,0 +1,59 @@
+package io.quarkiverse.dapr.examples;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.notNullValue;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+/**
+ * Integration test for the story creation workflow.
+ *
+ * Requires Docker for Dapr dev services (starts daprd, placement, scheduler,
+ * PostgreSQL state store, and dashboard containers via Testcontainers).
+ * Uses {@link MockChatModel} instead of a real LLM.
+ */
+@QuarkusTest
+@ExtendWith(DockerAvailableCondition.class)
+class StoryResourceTest {
+
+ @Test
+ void testStoryEndpointReturnsResponse() {
+ given()
+ .queryParam("topic", "dragons")
+ .queryParam("style", "comedy")
+ .when()
+ .get("/story")
+ .then()
+ .statusCode(200)
+ .body(notNullValue());
+ }
+
+ @Test
+ void testStoryEndpointWithDefaultParams() {
+ given()
+ .when()
+ .get("/story")
+ .then()
+ .statusCode(200)
+ .body(notNullValue());
+ }
+
+ @Test
+ void testStoryEndpointResponseContainsContent() {
+ String body = given()
+ .queryParam("topic", "space exploration")
+ .queryParam("style", "sci-fi")
+ .when()
+ .get("/story")
+ .then()
+ .statusCode(200)
+ .extract()
+ .asString();
+
+ // The mock model always returns the same text; verify it's non-empty
+ assert !body.isBlank() : "Story response should not be blank";
+ }
+}
diff --git a/quarkus/examples/src/test/resources/application.properties b/quarkus/examples/src/test/resources/application.properties
new file mode 100644
index 000000000..e437b9459
--- /dev/null
+++ b/quarkus/examples/src/test/resources/application.properties
@@ -0,0 +1,8 @@
+# Dapr Dev Services with PostgreSQL state store (dashboard enabled = uses PostgreSQL)
+quarkus.dapr.devservices.enabled=true
+quarkus.dapr.devservices.dashboard.enabled=true
+quarkus.dapr.workflow.enabled=true
+
+# Dummy OpenAI key (MockChatModel overrides the real provider in tests)
+quarkus.langchain4j.openai.api-key=test-key
+quarkus.langchain4j.openai.chat-model.model-name=gpt-4o-mini
diff --git a/quarkus/pom.xml b/quarkus/pom.xml
new file mode 100644
index 000000000..c15a341e5
--- /dev/null
+++ b/quarkus/pom.xml
@@ -0,0 +1,82 @@
+
+
+ 4.0.0
+
+
+ io.dapr
+ dapr-sdk-parent
+ 1.18.0-SNAPSHOT
+ ../pom.xml
+
+ io.dapr.quarkus
+ dapr-quarkus-agentic-parent
+
+ pom
+ Dapr Quarkus Agentic - Parent
+
+
+ runtime
+ deployment
+ quarkus-agentic-dapr-agents-registry
+ examples
+
+
+
+ 17
+ 17
+ UTF-8
+ 3.31.2
+ 2.5.0
+ 1.7.1
+
+
+
+
+
+ io.quarkus
+ quarkus-bom
+ ${quarkus.version}
+ pom
+ import
+
+
+
+ commons-io
+ commons-io
+ 2.20.0
+
+
+
+ org.junit
+ junit-bom
+ 6.0.2
+ pom
+ import
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 6.0.2
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 6.0.2
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 6.0.2
+
+
+
+
diff --git a/quarkus/quarkus-agentic-dapr-agents-registry/pom.xml b/quarkus/quarkus-agentic-dapr-agents-registry/pom.xml
new file mode 100644
index 000000000..37f51de9b
--- /dev/null
+++ b/quarkus/quarkus-agentic-dapr-agents-registry/pom.xml
@@ -0,0 +1,92 @@
+
+
+ 4.0.0
+
+ io.dapr.quarkus
+ dapr-quarkus-agentic-parent
+ 1.18.0-SNAPSHOT
+ ../pom.xml
+
+
+ quarkus-agentic-dapr-agents-registry
+ Quarkus Agentic Dapr - Agent Registry
+
+
+
+ io.quarkiverse.dapr
+ quarkus-dapr
+ ${quarkus-dapr.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ io.quarkiverse.langchain4j
+ quarkus-langchain4j-agentic
+ ${quarkus-langchain4j.version}
+
+
+ io.quarkus
+ quarkus-arc
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.26.3
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.14.2
+ test
+
+
+ io.quarkus
+ quarkus-junit
+ test
+
+
+ io.quarkiverse.langchain4j
+ quarkus-langchain4j-openai
+ ${quarkus-langchain4j.version}
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+
+
+
+ maven-compiler-plugin
+ 3.13.0
+
+
+ -parameters
+
+
+
+
+ maven-surefire-plugin
+ 3.5.2
+
+
+ org.jboss.logmanager.LogManager
+
+
+
+
+
+
diff --git a/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/AgentMetadata.java b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/AgentMetadata.java
new file mode 100644
index 000000000..3d4a5ca0b
--- /dev/null
+++ b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/AgentMetadata.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.agents.registry.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+public class AgentMetadata {
+
+ @JsonProperty("appid")
+ private String appId;
+
+ @JsonProperty("type")
+ private String type;
+
+ @JsonProperty("orchestrator")
+ private boolean orchestrator;
+
+ @JsonProperty("role")
+ private String role = "";
+
+ @JsonProperty("goal")
+ private String goal = "";
+
+ @JsonProperty("instructions")
+ private List instructions;
+
+ @JsonProperty("statestore")
+ private String statestore;
+
+ @JsonProperty("system_prompt")
+ private String systemPrompt;
+
+ @JsonProperty("framework")
+ private String framework;
+
+ public AgentMetadata() {
+ }
+
+ private AgentMetadata(Builder builder) {
+ this.appId = builder.appId;
+ this.type = builder.type;
+ this.orchestrator = builder.orchestrator;
+ this.role = builder.role;
+ this.goal = builder.goal;
+ this.instructions = builder.instructions;
+ this.statestore = builder.statestore;
+ this.systemPrompt = builder.systemPrompt;
+ this.framework = builder.framework;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public String getAppId() {
+ return appId;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public boolean isOrchestrator() {
+ return orchestrator;
+ }
+
+ public String getRole() {
+ return role;
+ }
+
+ public String getGoal() {
+ return goal;
+ }
+
+ public List getInstructions() {
+ return instructions;
+ }
+
+ public String getStatestore() {
+ return statestore;
+ }
+
+ public String getSystemPrompt() {
+ return systemPrompt;
+ }
+
+ public String getFramework() {
+ return framework;
+ }
+
+ public static class Builder {
+ private String appId;
+ private String type;
+ private boolean orchestrator = false;
+ private String role = "";
+ private String goal = "";
+ private List instructions;
+ private String statestore;
+ private String systemPrompt;
+ private String framework;
+
+ public Builder appId(String appId) {
+ this.appId = appId;
+ return this;
+ }
+
+ public Builder type(String type) {
+ this.type = type;
+ return this;
+ }
+
+ public Builder orchestrator(boolean orchestrator) {
+ this.orchestrator = orchestrator;
+ return this;
+ }
+
+ public Builder role(String role) {
+ this.role = role;
+ return this;
+ }
+
+ public Builder goal(String goal) {
+ this.goal = goal;
+ return this;
+ }
+
+ public Builder instructions(List instructions) {
+ this.instructions = instructions;
+ return this;
+ }
+
+ public Builder statestore(String statestore) {
+ this.statestore = statestore;
+ return this;
+ }
+
+ public Builder systemPrompt(String systemPrompt) {
+ this.systemPrompt = systemPrompt;
+ return this;
+ }
+
+ public Builder framework(String framework) {
+ this.framework = framework;
+ return this;
+ }
+
+ /**
+ * Builds the AgentMetadata, validating required fields.
+ *
+ * @return the constructed AgentMetadata
+ */
+ public AgentMetadata build() {
+ if (appId == null || type == null) {
+ throw new IllegalStateException("appId and type are required");
+ }
+ return new AgentMetadata(this);
+ }
+ }
+}
diff --git a/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/AgentMetadataSchema.java b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/AgentMetadataSchema.java
new file mode 100644
index 000000000..e5572b6e3
--- /dev/null
+++ b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/AgentMetadataSchema.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.agents.registry.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class AgentMetadataSchema {
+
+ @JsonProperty("schema_version")
+ private String schemaVersion;
+
+ @JsonProperty("agent")
+ private AgentMetadata agent;
+
+ @JsonProperty("name")
+ private String name;
+
+ @JsonProperty("registered_at")
+ private String registeredAt;
+
+ @JsonProperty("pubsub")
+ private PubSubMetadata pubsub;
+
+ @JsonProperty("memory")
+ private MemoryMetadata memory;
+
+ @JsonProperty("llm")
+ private LlmMetadata llm;
+
+ @JsonProperty("registry")
+ private RegistryMetadata registry;
+
+ @JsonProperty("tools")
+ private List tools;
+
+ @JsonProperty("max_iterations")
+ private Integer maxIterations;
+
+ @JsonProperty("tool_choice")
+ private String toolChoice;
+
+ @JsonProperty("agent_metadata")
+ private Map agentMetadata;
+
+ public AgentMetadataSchema() {
+ }
+
+ private AgentMetadataSchema(Builder builder) {
+ this.schemaVersion = builder.schemaVersion;
+ this.agent = builder.agent;
+ this.name = builder.name;
+ this.registeredAt = builder.registeredAt;
+ this.pubsub = builder.pubsub;
+ this.memory = builder.memory;
+ this.llm = builder.llm;
+ this.registry = builder.registry;
+ this.tools = builder.tools;
+ this.maxIterations = builder.maxIterations;
+ this.toolChoice = builder.toolChoice;
+ this.agentMetadata = builder.agentMetadata;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public String getSchemaVersion() {
+ return schemaVersion;
+ }
+
+ public AgentMetadata getAgent() {
+ return agent;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getRegisteredAt() {
+ return registeredAt;
+ }
+
+ public PubSubMetadata getPubsub() {
+ return pubsub;
+ }
+
+ public MemoryMetadata getMemory() {
+ return memory;
+ }
+
+ public LlmMetadata getLlm() {
+ return llm;
+ }
+
+ public RegistryMetadata getRegistry() {
+ return registry;
+ }
+
+ public List getTools() {
+ return tools;
+ }
+
+ public Integer getMaxIterations() {
+ return maxIterations;
+ }
+
+ public String getToolChoice() {
+ return toolChoice;
+ }
+
+ public Map getAgentMetadata() {
+ return agentMetadata;
+ }
+
+ public static class Builder {
+ private String schemaVersion;
+ private AgentMetadata agent;
+ private String name;
+ private String registeredAt;
+ private PubSubMetadata pubsub;
+ private MemoryMetadata memory;
+ private LlmMetadata llm;
+ private RegistryMetadata registry;
+ private List tools;
+ private Integer maxIterations;
+ private String toolChoice;
+ private Map agentMetadata;
+
+ public Builder schemaVersion(String schemaVersion) {
+ this.schemaVersion = schemaVersion;
+ return this;
+ }
+
+ public Builder agent(AgentMetadata agent) {
+ this.agent = agent;
+ return this;
+ }
+
+ public Builder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder registeredAt(String registeredAt) {
+ this.registeredAt = registeredAt;
+ return this;
+ }
+
+ public Builder pubsub(PubSubMetadata pubsub) {
+ this.pubsub = pubsub;
+ return this;
+ }
+
+ public Builder memory(MemoryMetadata memory) {
+ this.memory = memory;
+ return this;
+ }
+
+ public Builder llm(LlmMetadata llm) {
+ this.llm = llm;
+ return this;
+ }
+
+ public Builder registry(RegistryMetadata registry) {
+ this.registry = registry;
+ return this;
+ }
+
+ public Builder tools(List tools) {
+ this.tools = tools;
+ return this;
+ }
+
+ /**
+ * Adds a single tool to the tools list, initializing the list if needed.
+ *
+ * @param tool the tool metadata to add
+ * @return this builder
+ */
+ public Builder addTool(ToolMetadata tool) {
+ if (this.tools == null) {
+ this.tools = new ArrayList<>();
+ }
+ this.tools.add(tool);
+ return this;
+ }
+
+ public Builder maxIterations(Integer maxIterations) {
+ this.maxIterations = maxIterations;
+ return this;
+ }
+
+ public Builder toolChoice(String toolChoice) {
+ this.toolChoice = toolChoice;
+ return this;
+ }
+
+ public Builder agentMetadata(Map agentMetadata) {
+ this.agentMetadata = agentMetadata;
+ return this;
+ }
+
+ /**
+ * Builds the AgentMetadataSchema, validating required fields.
+ *
+ * @return the constructed AgentMetadataSchema
+ */
+ public AgentMetadataSchema build() {
+ if (schemaVersion == null || agent == null || name == null || registeredAt == null) {
+ throw new IllegalStateException("schemaVersion, agent, name, and registeredAt are required");
+ }
+ return new AgentMetadataSchema(this);
+ }
+ }
+}
diff --git a/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/LlmMetadata.java b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/LlmMetadata.java
new file mode 100644
index 000000000..d2cfdbb34
--- /dev/null
+++ b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/LlmMetadata.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.agents.registry.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class LlmMetadata {
+
+ @JsonProperty("client")
+ private String client;
+
+ @JsonProperty("provider")
+ private String provider;
+
+ @JsonProperty("api")
+ private String api = "unknown";
+
+ @JsonProperty("model")
+ private String model = "unknown";
+
+ @JsonProperty("component_name")
+ private String componentName;
+
+ @JsonProperty("base_url")
+ private String baseUrl;
+
+ @JsonProperty("azure_endpoint")
+ private String azureEndpoint;
+
+ @JsonProperty("azure_deployment")
+ private String azureDeployment;
+
+ @JsonProperty("prompt_template")
+ private String promptTemplate;
+
+ public LlmMetadata() {
+ }
+
+ private LlmMetadata(Builder builder) {
+ this.client = builder.client;
+ this.provider = builder.provider;
+ this.api = builder.api;
+ this.model = builder.model;
+ this.componentName = builder.componentName;
+ this.baseUrl = builder.baseUrl;
+ this.azureEndpoint = builder.azureEndpoint;
+ this.azureDeployment = builder.azureDeployment;
+ this.promptTemplate = builder.promptTemplate;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public String getClient() {
+ return client;
+ }
+
+ public String getProvider() {
+ return provider;
+ }
+
+ public String getApi() {
+ return api;
+ }
+
+ public String getModel() {
+ return model;
+ }
+
+ public String getComponentName() {
+ return componentName;
+ }
+
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ public String getAzureEndpoint() {
+ return azureEndpoint;
+ }
+
+ public String getAzureDeployment() {
+ return azureDeployment;
+ }
+
+ public String getPromptTemplate() {
+ return promptTemplate;
+ }
+
+ public static class Builder {
+ private String client;
+ private String provider;
+ private String api = "unknown";
+ private String model = "unknown";
+ private String componentName;
+ private String baseUrl;
+ private String azureEndpoint;
+ private String azureDeployment;
+ private String promptTemplate;
+
+ public Builder client(String client) {
+ this.client = client;
+ return this;
+ }
+
+ public Builder provider(String provider) {
+ this.provider = provider;
+ return this;
+ }
+
+ public Builder api(String api) {
+ this.api = api;
+ return this;
+ }
+
+ public Builder model(String model) {
+ this.model = model;
+ return this;
+ }
+
+ public Builder componentName(String componentName) {
+ this.componentName = componentName;
+ return this;
+ }
+
+ public Builder baseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ return this;
+ }
+
+ public Builder azureEndpoint(String azureEndpoint) {
+ this.azureEndpoint = azureEndpoint;
+ return this;
+ }
+
+ public Builder azureDeployment(String azureDeployment) {
+ this.azureDeployment = azureDeployment;
+ return this;
+ }
+
+ public Builder promptTemplate(String promptTemplate) {
+ this.promptTemplate = promptTemplate;
+ return this;
+ }
+
+ /**
+ * Builds the LlmMetadata, validating required fields.
+ *
+ * @return the constructed LlmMetadata
+ */
+ public LlmMetadata build() {
+ if (client == null || provider == null) {
+ throw new IllegalStateException("client and provider are required");
+ }
+ return new LlmMetadata(this);
+ }
+ }
+}
diff --git a/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/MemoryMetadata.java b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/MemoryMetadata.java
new file mode 100644
index 000000000..57cc11031
--- /dev/null
+++ b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/MemoryMetadata.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.agents.registry.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class MemoryMetadata {
+
+ @JsonProperty("type")
+ private String type;
+
+ @JsonProperty("statestore")
+ private String statestore;
+
+ public MemoryMetadata() {
+ }
+
+ private MemoryMetadata(Builder builder) {
+ this.type = builder.type;
+ this.statestore = builder.statestore;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getStatestore() {
+ return statestore;
+ }
+
+ public static class Builder {
+ private String type;
+ private String statestore;
+
+ public Builder type(String type) {
+ this.type = type;
+ return this;
+ }
+
+ public Builder statestore(String statestore) {
+ this.statestore = statestore;
+ return this;
+ }
+
+ /**
+ * Builds a new MemoryMetadata instance.
+ *
+ * @return the built MemoryMetadata
+ */
+ public MemoryMetadata build() {
+ if (type == null) {
+ throw new IllegalStateException("type is required");
+ }
+ return new MemoryMetadata(this);
+ }
+ }
+}
diff --git a/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/PubSubMetadata.java b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/PubSubMetadata.java
new file mode 100644
index 000000000..1a844c18b
--- /dev/null
+++ b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/PubSubMetadata.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.agents.registry.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class PubSubMetadata {
+
+ @JsonProperty("name")
+ private String name;
+
+ @JsonProperty("broadcast_topic")
+ private String broadcastTopic;
+
+ @JsonProperty("agent_topic")
+ private String agentTopic;
+
+ public PubSubMetadata() {
+ }
+
+ private PubSubMetadata(Builder builder) {
+ this.name = builder.name;
+ this.broadcastTopic = builder.broadcastTopic;
+ this.agentTopic = builder.agentTopic;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getBroadcastTopic() {
+ return broadcastTopic;
+ }
+
+ public String getAgentTopic() {
+ return agentTopic;
+ }
+
+ public static class Builder {
+ private String name;
+ private String broadcastTopic;
+ private String agentTopic;
+
+ public Builder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder broadcastTopic(String broadcastTopic) {
+ this.broadcastTopic = broadcastTopic;
+ return this;
+ }
+
+ public Builder agentTopic(String agentTopic) {
+ this.agentTopic = agentTopic;
+ return this;
+ }
+
+ /**
+ * Builds a new PubSubMetadata instance.
+ *
+ * @return the built PubSubMetadata
+ */
+ public PubSubMetadata build() {
+ if (name == null) {
+ throw new IllegalStateException("name is required");
+ }
+ return new PubSubMetadata(this);
+ }
+ }
+}
diff --git a/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/RegistryMetadata.java b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/RegistryMetadata.java
new file mode 100644
index 000000000..3ca17bc54
--- /dev/null
+++ b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/RegistryMetadata.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.agents.registry.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class RegistryMetadata {
+
+ @JsonProperty("statestore")
+ private String statestore;
+
+ @JsonProperty("name")
+ private String name;
+
+ public RegistryMetadata() {
+ }
+
+ private RegistryMetadata(Builder builder) {
+ this.statestore = builder.statestore;
+ this.name = builder.name;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public String getStatestore() {
+ return statestore;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public static class Builder {
+ private String statestore;
+ private String name;
+
+ public Builder statestore(String statestore) {
+ this.statestore = statestore;
+ return this;
+ }
+
+ public Builder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public RegistryMetadata build() {
+ return new RegistryMetadata(this);
+ }
+ }
+}
diff --git a/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/ToolMetadata.java b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/ToolMetadata.java
new file mode 100644
index 000000000..cca5a0a68
--- /dev/null
+++ b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/model/ToolMetadata.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.agents.registry.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class ToolMetadata {
+
+ @JsonProperty("tool_name")
+ private String toolName;
+
+ @JsonProperty("tool_description")
+ private String toolDescription;
+
+ @JsonProperty("tool_args")
+ private String toolArgs;
+
+ public ToolMetadata() {
+ }
+
+ private ToolMetadata(Builder builder) {
+ this.toolName = builder.toolName;
+ this.toolDescription = builder.toolDescription;
+ this.toolArgs = builder.toolArgs;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public String getToolName() {
+ return toolName;
+ }
+
+ public String getToolDescription() {
+ return toolDescription;
+ }
+
+ public String getToolArgs() {
+ return toolArgs;
+ }
+
+ public static class Builder {
+ private String toolName;
+ private String toolDescription;
+ private String toolArgs;
+
+ public Builder toolName(String toolName) {
+ this.toolName = toolName;
+ return this;
+ }
+
+ public Builder toolDescription(String toolDescription) {
+ this.toolDescription = toolDescription;
+ return this;
+ }
+
+ public Builder toolArgs(String toolArgs) {
+ this.toolArgs = toolArgs;
+ return this;
+ }
+
+ /**
+ * Builds the ToolMetadata, validating required fields.
+ *
+ * @return the constructed ToolMetadata
+ */
+ public ToolMetadata build() {
+ if (toolName == null || toolDescription == null || toolArgs == null) {
+ throw new IllegalStateException("toolName, toolDescription, and toolArgs are required");
+ }
+ return new ToolMetadata(this);
+ }
+ }
+}
diff --git a/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/service/AgentRegistry.java b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/service/AgentRegistry.java
new file mode 100644
index 000000000..0be11bb9c
--- /dev/null
+++ b/quarkus/quarkus-agentic-dapr-agents-registry/src/main/java/io/quarkiverse/dapr/agents/registry/service/AgentRegistry.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.agents.registry.service;
+
+import io.dapr.client.DaprClient;
+import io.quarkiverse.dapr.agents.registry.model.AgentMetadata;
+import io.quarkiverse.dapr.agents.registry.model.AgentMetadataSchema;
+import io.quarkus.runtime.StartupEvent;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.event.Observes;
+import jakarta.enterprise.inject.Any;
+import jakarta.enterprise.inject.spi.Bean;
+import jakarta.enterprise.inject.spi.BeanManager;
+import jakarta.inject.Inject;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.jboss.logging.Logger;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@ApplicationScoped
+public class AgentRegistry {
+
+ private static final Logger LOG = Logger.getLogger(AgentRegistry.class);
+
+ /**
+ * Fully-qualified name of langchain4j {@code @Agent} annotation.
+ */
+ private static final String AGENT_ANNOTATION_NAME = "dev.langchain4j.agentic.Agent";
+
+ /**
+ * Fully-qualified name of langchain4j {@code @SystemMessage} annotation.
+ */
+ private static final String SYSTEM_MESSAGE_ANNOTATION_NAME = "dev.langchain4j.service.SystemMessage";
+
+ @Inject
+ DaprClient client;
+
+ @Inject
+ BeanManager beanManager;
+
+ @ConfigProperty(name = "dapr.agents.statestore", defaultValue = "kvstore")
+ String statestore;
+
+ @ConfigProperty(name = "dapr.appid", defaultValue = "local-dapr-app")
+ String appId;
+
+ @ConfigProperty(name = "dapr.agents.team", defaultValue = "default")
+ String team;
+
+ void onStartup(@Observes StartupEvent event) {
+ discoverAndRegisterAgents();
+ }
+
+ void discoverAndRegisterAgents() {
+ LOG.info("Starting agent auto-discovery...");
+ Set> beans = beanManager.getBeans(Object.class, Any.Literal.INSTANCE);
+ LOG.debugf("Found %d CDI beans to scan", beans.size());
+
+ Set scannedInterfaces = new HashSet<>();
+ // Collect all interface classes from CDI beans first
+ List> interfacesToScan = new ArrayList<>();
+
+ for (Bean> bean : beans) {
+ for (Type type : bean.getTypes()) {
+ if (type instanceof Class> clazz && clazz.isInterface()) {
+ if (scannedInterfaces.add(clazz.getName())) {
+ interfacesToScan.add(clazz);
+ }
+ }
+ }
+ }
+
+ // Also discover sub-agent classes referenced by composite agent annotations
+ // (e.g., @SequenceAgent(subAgents = {CreativeWriter.class, StyleEditor.class})).
+ // Sub-agents are often not CDI beans themselves, so they won't appear in BeanManager.
+ List> subAgentClasses = new ArrayList<>();
+ for (Class> iface : interfacesToScan) {
+ for (Method method : iface.getDeclaredMethods()) {
+ for (Annotation ann : method.getDeclaredAnnotations()) {
+ for (Class> subAgent : extractSubAgentClasses(ann)) {
+ if (subAgent.isInterface() && scannedInterfaces.add(subAgent.getName())) {
+ subAgentClasses.add(subAgent);
+ LOG.debugf("Discovered sub-agent interface %s from %s on %s",
+ subAgent.getName(), ann.annotationType().getSimpleName(), iface.getName());
+ }
+ }
+ }
+ }
+ }
+ interfacesToScan.addAll(subAgentClasses);
+
+ // Scan all collected interfaces for @Agent methods
+ int registered = 0;
+ int failed = 0;
+ for (Class> iface : interfacesToScan) {
+ LOG.debugf("Scanning interface: %s", iface.getName());
+ List agents = scanForAgents(iface, appId);
+ if (!agents.isEmpty()) {
+ LOG.debugf("Found %d @Agent method(s) on interface %s", agents.size(), iface.getName());
+ }
+ for (AgentMetadataSchema schema : agents) {
+ try {
+ registerAgent(schema);
+ registered++;
+ } catch (Exception e) {
+ failed++;
+ LOG.errorf(e, "Failed to register agent '%s' in state store '%s': %s",
+ schema.getName(), statestore, e.getMessage());
+ }
+ }
+ }
+
+ LOG.debugf("Scanned %d unique interfaces", scannedInterfaces.size());
+
+ if (registered == 0 && failed == 0) {
+ LOG.warn("No @Agent-annotated methods found on any CDI bean interface. "
+ + "Ensure your @Agent interfaces are registered as CDI beans.");
+ } else if (failed > 0) {
+ LOG.warnf("Agent discovery complete: %d registered, %d failed", registered, failed);
+ } else {
+ LOG.infof("Agent discovery complete: %d agent(s) registered successfully", registered);
+ }
+ }
+
+ /**
+ * Extracts sub-agent classes from a composite agent annotation.
+ *
+ *
Looks for a {@code subAgents()} method returning {@code Class>[]} on the annotation.
+ * This works for any composite agent annotation (e.g., {@code @SequenceAgent},
+ * {@code @ParallelAgent}, etc.) without coupling to specific annotation types.
+ */
+ static Class>[] extractSubAgentClasses(Annotation ann) {
+ try {
+ Method subAgentsMethod = ann.annotationType().getMethod("subAgents");
+ Object result = subAgentsMethod.invoke(ann);
+ if (result instanceof Class>[] classes) {
+ return classes;
+ }
+ } catch (NoSuchMethodException e) {
+ // Not a composite agent annotation — expected for most annotations
+ } catch (Exception e) {
+ LOG.debugf("Failed to extract subAgents from %s: %s",
+ ann.annotationType().getSimpleName(), e.getMessage());
+ }
+ return new Class>[0];
+ }
+
+ /**
+ * Scans an interface for methods annotated with {@code @Agent} and extracts metadata.
+ *
+ *
+ * Methods have no parameters to avoid validation errors from the langchain4j
+ * AgenticProcessor which expects parameters to resolve from agent output keys.
+ */
+public interface TestAgent {
+
+ @Agent(name = "test-agent-with-prompt", description = "Agent with system prompt")
+ @SystemMessage("You are a test agent for integration testing.")
+ String chatWithPrompt();
+
+ @Agent(name = "test-agent-simple", description = "Simple agent without prompt")
+ String chatSimple();
+
+ @Agent(description = "Agent with default name")
+ String defaultNameAgent();
+}
diff --git a/quarkus/quarkus-agentic-dapr-agents-registry/src/test/java/io/quarkiverse/dapr/agents/registry/service/TestAgentBean.java b/quarkus/quarkus-agentic-dapr-agents-registry/src/test/java/io/quarkiverse/dapr/agents/registry/service/TestAgentBean.java
new file mode 100644
index 000000000..7edb7721b
--- /dev/null
+++ b/quarkus/quarkus-agentic-dapr-agents-registry/src/test/java/io/quarkiverse/dapr/agents/registry/service/TestAgentBean.java
@@ -0,0 +1,30 @@
+package io.quarkiverse.dapr.agents.registry.service;
+
+import jakarta.annotation.Priority;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Alternative;
+
+/**
+ * CDI bean implementing {@link TestAgent} so that {@link AgentRegistry}
+ * can discover it via {@code BeanManager} during startup.
+ */
+@Alternative
+@Priority(1)
+@ApplicationScoped
+public class TestAgentBean implements TestAgent {
+
+ @Override
+ public String chatWithPrompt() {
+ return "mock response";
+ }
+
+ @Override
+ public String chatSimple() {
+ return "mock response";
+ }
+
+ @Override
+ public String defaultNameAgent() {
+ return "mock response";
+ }
+}
diff --git a/quarkus/quarkus-agentic-dapr-agents-registry/src/test/resources/application.properties b/quarkus/quarkus-agentic-dapr-agents-registry/src/test/resources/application.properties
new file mode 100644
index 000000000..c9b76a36a
--- /dev/null
+++ b/quarkus/quarkus-agentic-dapr-agents-registry/src/test/resources/application.properties
@@ -0,0 +1,13 @@
+# Dapr Dev Services with PostgreSQL state store (dashboard enabled = uses PostgreSQL)
+quarkus.dapr.devservices.enabled=true
+quarkus.dapr.devservices.dashboard.enabled=true
+
+# Agent registry configuration
+# Dev services name the state store component "kvstore"
+dapr.agents.statestore=kvstore
+dapr.appid=local-dapr-app
+dapr.agents.team=test-team
+
+# Dummy OpenAI key (MockChatModel overrides the real provider in tests)
+quarkus.langchain4j.openai.api-key=test-key
+quarkus.langchain4j.openai.chat-model.model-name=gpt-4o-mini
diff --git a/quarkus/runtime/pom.xml b/quarkus/runtime/pom.xml
new file mode 100644
index 000000000..d58450e0d
--- /dev/null
+++ b/quarkus/runtime/pom.xml
@@ -0,0 +1,88 @@
+
+
+ 4.0.0
+
+ io.dapr.quarkus
+ dapr-quarkus-agentic-parent
+ 1.18.0-SNAPSHOT
+ ../pom.xml
+
+
+ quarkus-agentic-dapr
+ Dapr Quarkus Agentic - Runtime
+
+
+
+ io.quarkiverse.dapr
+ quarkus-dapr
+ ${quarkus-dapr.version}
+
+
+ io.dapr
+ dapr-sdk-workflows
+
+
+ io.quarkiverse.langchain4j
+ quarkus-langchain4j-agentic
+ ${quarkus-langchain4j.version}
+
+
+ io.quarkus
+ quarkus-arc
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.14.2
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.26.3
+ test
+
+
+
+
+
+
+ io.quarkus
+ quarkus-extension-maven-plugin
+ ${quarkus.version}
+
+
+ compile
+
+ extension-descriptor
+
+
+ ${project.groupId}:${project.artifactId}-deployment:${project.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${quarkus.version}
+
+
+
+
+
+
+
diff --git a/quarkus/runtime/src/main/java/io/quarkiverse/dapr/langchain4j/agent/AgentRunContext.java b/quarkus/runtime/src/main/java/io/quarkiverse/dapr/langchain4j/agent/AgentRunContext.java
new file mode 100644
index 000000000..f69296be7
--- /dev/null
+++ b/quarkus/runtime/src/main/java/io/quarkiverse/dapr/langchain4j/agent/AgentRunContext.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2026 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.quarkiverse.dapr.langchain4j.agent;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Holds the synchronization state for a single agent execution.
+ *
+ *
When a {@code @Tool}-annotated method is intercepted by {@link DaprToolCallInterceptor},
+ * it registers a {@link PendingCall} here and blocks until
+ * {@link io.quarkiverse.dapr.langchain4j.agent.activities.ToolCallActivity} executes the
+ * tool on the Dapr Workflow Activity thread and completes the future.
+ */
+public class AgentRunContext {
+
+ /**
+ * Holds all the information needed for {@code ToolCallActivity} to execute the tool
+ * and unblock the waiting agent thread.
+ */
+ @SuppressWarnings("EI_EXPOSE_REP")
+ public record PendingCall(
+ Object target,
+ Method method,
+ Object[] args,
+ CompletableFuture