{@code DaprWorkflowProcessor.searchWorkflows()} uses {@code ApplicationIndexBuildItem} + * which only indexes application classes -- extension runtime JARs are invisible to it. + * We fix this in two steps: + *
This gives full control over workflow naming: the same class can be
+ * registered under multiple names (e.g., {@code AgentRunWorkflow} as both
+ * {@code dapr.langchain4j.AgentRun.workflow} and
+ * {@code dapr.langchain4j.WeatherAssistant.workflow}).
+ */
+ @BuildStep
+ @Record(ExecutionTime.RUNTIME_INIT)
+ void setupWorkflowRuntime(DaprWorkflowRuntimeRecorder recorder,
+ CombinedIndexBuildItem combinedIndex) {
+
+ @SuppressWarnings("rawtypes") RuntimeValue builder = recorder.createBuilder();
+ IndexView index = combinedIndex.getIndex();
+
+ // Register generic workflows (from @WorkflowMetadata name)
+ for (String className : WORKFLOW_CLASSES) {
+ ClassInfo classInfo = index.getClassByName(DotName.createSimple(className));
+ if (classInfo == null) {
+ continue;
+ }
+ String regName = className;
+ AnnotationInstance meta = classInfo.annotation(WORKFLOW_METADATA_DOTNAME);
+ if (meta != null) {
+ String metaName = stringValueOrNull(meta, "name");
+ if (metaName != null) {
+ regName = metaName;
+ }
+ }
+ recorder.registerWorkflow(builder, regName, className);
+ }
+
+ // Register activities (from @ActivityMetadata name)
+ for (String className : ACTIVITY_CLASSES) {
+ ClassInfo classInfo = index.getClassByName(DotName.createSimple(className));
+ if (classInfo == null) {
+ continue;
+ }
+ String regName = className;
+ AnnotationInstance meta = classInfo.annotation(ACTIVITY_METADATA_DOTNAME);
+ if (meta != null) {
+ String metaName = stringValueOrNull(meta, "name");
+ if (metaName != null) {
+ regName = metaName;
+ }
+ }
+ recorder.registerActivity(builder, regName, className);
+ }
+
+ // Register agent-specific workflow names for composite agents
+ for (Map.Entry Non-{@code @Agent} abstract methods are delegated transparently. Static and default
+ * (non-abstract) interface methods are not overridden.
+ */
+ @BuildStep
+ void generateAgentDecorators(
+ CombinedIndexBuildItem combinedIndex,
+ BuildProducer 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 toTitleCase(String name) {
+ StringBuilder sb = new StringBuilder();
+ boolean capitalizeNext = true;
+ for (char c : name.toCharArray()) {
+ if (c == '-' || c == '_' || c == ' ') {
+ capitalizeNext = true;
+ } else if (capitalizeNext) {
+ sb.append(Character.toUpperCase(c));
+ capitalizeNext = false;
+ } else {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ 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.dapr.quarkus.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 0000000000..7db01a61d1
--- /dev/null
+++ b/quarkus/examples/pom.xml
@@ -0,0 +1,95 @@
+
+ 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/dapr/quarkus/examples/ParallelResource.java b/quarkus/examples/src/main/java/io/dapr/quarkus/examples/ParallelResource.java
new file mode 100644
index 0000000000..b520705f11
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/dapr/quarkus/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.dapr.quarkus.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.
+ *
+ * Example usage:
+ * Each request:
+ * Example usage:
+ * 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:
+ * 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/dapr/quarkus/examples/StoryCreator.java b/quarkus/examples/src/main/java/io/dapr/quarkus/examples/StoryCreator.java
new file mode 100644
index 0000000000..f7ea1ad513
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/dapr/quarkus/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.dapr.quarkus.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/dapr/quarkus/examples/StoryResource.java b/quarkus/examples/src/main/java/io/dapr/quarkus/examples/StoryResource.java
new file mode 100644
index 0000000000..00aa063462
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/dapr/quarkus/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.dapr.quarkus.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.
+ *
+ * Example usage:
+ *
+ * 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/dapr/quarkus/examples/MockChatModel.java b/quarkus/examples/src/test/java/io/dapr/quarkus/examples/MockChatModel.java
new file mode 100644
index 0000000000..b23def8292
--- /dev/null
+++ b/quarkus/examples/src/test/java/io/dapr/quarkus/examples/MockChatModel.java
@@ -0,0 +1,32 @@
+package io.dapr.quarkus.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/dapr/quarkus/examples/ParallelResourceTest.java b/quarkus/examples/src/test/java/io/dapr/quarkus/examples/ParallelResourceTest.java
new file mode 100644
index 0000000000..09a41d9e78
--- /dev/null
+++ b/quarkus/examples/src/test/java/io/dapr/quarkus/examples/ParallelResourceTest.java
@@ -0,0 +1,51 @@
+package io.dapr.quarkus.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/dapr/quarkus/examples/StoryResourceTest.java b/quarkus/examples/src/test/java/io/dapr/quarkus/examples/StoryResourceTest.java
new file mode 100644
index 0000000000..5de3a60bd4
--- /dev/null
+++ b/quarkus/examples/src/test/java/io/dapr/quarkus/examples/StoryResourceTest.java
@@ -0,0 +1,59 @@
+package io.dapr.quarkus.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 0000000000..e437b94593
--- /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 0000000000..fe8db8f313
--- /dev/null
+++ b/quarkus/pom.xml
@@ -0,0 +1,92 @@
+
+ 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.
+ *
+ * Uses name-based annotation matching ({@code annotationType().getName()}) instead of
+ * class identity ({@code method.getAnnotation(Agent.class)}) to handle classloader
+ * differences between library JARs and the Quarkus application classloader.
+ */
+ static List Two-step operation matching the Python dapr-agents registration protocol:
+ * The team index is how dapr-agents discovers peers in multi-agent workflows.
+ *
+ * @param schema the agent metadata schema to register
+ */
+ @SuppressWarnings("unchecked")
+ public void registerAgent(AgentMetadataSchema schema) {
+ String registryPrefix = "agents:" + team;
+ MapWhy 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.dapr.quarkus.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:
+ * {@code
+ * @Decorator @Priority(APPLICATION) @Dependent
+ * class DaprDecorator_I implements I {
+ * @Inject @Delegate @Any I delegate;
+ * @Inject AgentRunLifecycleManager lifecycleManager;
+ *
+ * @Override
+ * ReturnType agentMethod(Params...) {
+ * lifecycleManager.getOrActivate(agentName, userMessage, systemMessage);
+ * try {
+ * ReturnType result = delegate.agentMethod(params);
+ * lifecycleManager.triggerDone();
+ * return result;
+ * } catch (Throwable t) {
+ * lifecycleManager.triggerDone();
+ * throw t;
+ * }
+ * }
+ * // non-@Agent abstract methods: pure delegation to delegate
+ * }
+ * }
+ *
+ *
+ * lifecycleManager.getOrActivate(agentName, userMsg, sysMsg);
+ * try {
+ * [result =] delegate.method(params);
+ * lifecycleManager.triggerDone();
+ * return [result]; // or returnVoid()
+ * } catch (Throwable t) {
+ * lifecycleManager.triggerDone();
+ * throw t;
+ * }
+ *
+ */
+ 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.
+ *
+ *
+ * curl "http://localhost:8080/parallel?topic=dragons&country=France&style=comedy"
+ *
+ */
+@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/dapr/quarkus/examples/ParallelStatus.java b/quarkus/examples/src/main/java/io/dapr/quarkus/examples/ParallelStatus.java
new file mode 100644
index 0000000000..e01a5e0f19
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/dapr/quarkus/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.dapr.quarkus.examples;
+
+public record ParallelStatus(String status, String story, String summary) {
+}
diff --git a/quarkus/examples/src/main/java/io/dapr/quarkus/examples/ResearchResource.java b/quarkus/examples/src/main/java/io/dapr/quarkus/examples/ResearchResource.java
new file mode 100644
index 0000000000..5521643ad6
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/dapr/quarkus/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.dapr.quarkus.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.
+ *
+ *
+ *
+ *
+ *
+ * curl "http://localhost:8080/research?country=France"
+ * curl "http://localhost:8080/research?country=Germany"
+ *
+ */
+@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/dapr/quarkus/examples/ResearchTools.java b/quarkus/examples/src/main/java/io/dapr/quarkus/examples/ResearchTools.java
new file mode 100644
index 0000000000..b89c4ff02d
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/dapr/quarkus/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.dapr.quarkus.examples;
+
+import dev.langchain4j.agent.tool.Tool;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/**
+ * CDI bean providing research tools for the {@link ResearchWriter} agent.
+ *
+ *
+ *
+ */
+@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/dapr/quarkus/examples/ResearchWriter.java b/quarkus/examples/src/main/java/io/dapr/quarkus/examples/ResearchWriter.java
new file mode 100644
index 0000000000..62aeb16162
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/dapr/quarkus/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.dapr.quarkus.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.
+ *
+ *
+ * curl "http://localhost:8080/story?topic=dragons&style=comedy"
+ *
+ */
+@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/dapr/quarkus/examples/StyleEditor.java b/quarkus/examples/src/main/java/io/dapr/quarkus/examples/StyleEditor.java
new file mode 100644
index 0000000000..3d1e67db95
--- /dev/null
+++ b/quarkus/examples/src/main/java/io/dapr/quarkus/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.dapr.quarkus.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 0000000000..8ceffbc59c
--- /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.dapr.quarkus.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/dapr/quarkus/examples/DaprWorkflowClientTest.java b/quarkus/examples/src/test/java/io/dapr/quarkus/examples/DaprWorkflowClientTest.java
new file mode 100644
index 0000000000..2f527db893
--- /dev/null
+++ b/quarkus/examples/src/test/java/io/dapr/quarkus/examples/DaprWorkflowClientTest.java
@@ -0,0 +1,28 @@
+package io.dapr.quarkus.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/dapr/quarkus/examples/DockerAvailableCondition.java b/quarkus/examples/src/test/java/io/dapr/quarkus/examples/DockerAvailableCondition.java
new file mode 100644
index 0000000000..db8c786ede
--- /dev/null
+++ b/quarkus/examples/src/test/java/io/dapr/quarkus/examples/DockerAvailableCondition.java
@@ -0,0 +1,48 @@
+package io.dapr.quarkus.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).
+ *
+ *
+ *
+ *