Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7260af8
feat(quarkus): add quarkus-agentic-dapr extension
salaboy Mar 10, 2026
920b13e
feat(registry): add agent registry and dapr-agents compatibility
javier-aliaga Apr 16, 2026
cf6e691
feat(quarkus): add Dapr Conversation API ChatModel provider
javier-aliaga May 13, 2026
378c2f8
feat(quarkus): add agent-level crash recovery via RecoveryAgentActivity
javier-aliaga Jun 9, 2026
5aa83e0
fix(quarkus): improve chatModel observability — fix null response and…
javier-aliaga Jun 9, 2026
20389f3
fix(quarkus): harden agent runtime per code review
javier-aliaga Jun 10, 2026
6205d97
ci: pass release version via env to prevent shell injection
javier-aliaga Jun 10, 2026
7331dd1
fix(quarkus): align build with SDK 1.19.0-SNAPSHOT from master
javier-aliaga Jun 10, 2026
7347160
test(quarkus): fix stale expectations and @QuarkusTest classpath conf…
javier-aliaga Jun 10, 2026
14eda0e
fix(quarkus): pin java-sdk 1.18.0-rc-3 and align dev services Dapr ru…
javier-aliaga Jun 10, 2026
8f0284d
feat(quarkus): checkpoint LangChain4j agentic scopes to Dapr state store
javier-aliaga Jun 10, 2026
6dd5afa
fix(quarkus): narrow extractPrompt catch to satisfy SpotBugs REC_CATC…
javier-aliaga Jun 10, 2026
70e0930
docs(quarkus): fix javadoc errors gating the Validate Javadocs CI job
javier-aliaga Jun 10, 2026
dbf26f9
chore(quarkus): use released java-sdk 1.18.0 and final Dapr runtime i…
javier-aliaga Jun 11, 2026
cc54b37
fix(quarkus): make parallel orchestration reliable and durable
javier-aliaga Jun 11, 2026
d0b1fa2
test(quarkus): skip workflow e2e tests on CI until dapr/dapr#10054 ships
javier-aliaga Jun 11, 2026
5946097
fix(quarkus): correct Dapr Conversation ChatModel tool schema and too…
javier-aliaga Jun 13, 2026
74e11cc
fix(quarkus): stop loop-iteration agents from claiming stale run bind…
javier-aliaga Jun 13, 2026
2fc5a0c
fix(quarkus): self-heal lost agent-events with a bounded wait timeout
javier-aliaga Jun 13, 2026
768aeae
docs(quarkus): reflect loop-binding and event self-heal fixes in limi…
javier-aliaga Jun 13, 2026
4c274fc
docs(quarkus): document single-replica execution constraint
javier-aliaga Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ on:
- release-*
tags:
- v*
paths-ignore:
- 'quarkus/**'

pull_request:
branches:
- master
- release-*
paths-ignore:
- 'quarkus/**'

jobs:
test:
Expand Down
82 changes: 82 additions & 0 deletions .github/workflows/quarkus-build.yml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is very important to be sure that the extension can run in native mode, I think we need to add it here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will do it in a followup PR, this one is getting too much bigger

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Quarkus Agentic Build

on:
workflow_dispatch:
push:
branches:
- master
- release-quarkus-*
paths:
- 'quarkus/**'
tags:
- quarkus-v*

pull_request:
branches:
- master
- release-quarkus-*
paths:
- 'quarkus/**'

jobs:
build:
name: "Build & Test"
runs-on: ubuntu-latest
timeout-minutes: 30
env:
JDK_VER: 17
steps:
- uses: actions/checkout@v5
- name: Set up OpenJDK ${{ env.JDK_VER }}
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: ${{ env.JDK_VER }}
- name: Build quarkus modules
run: |
cd quarkus
../mvnw clean install -B -q -DskipITs=true
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v7
with:
name: quarkus-test-reports
path: quarkus/**/target/surefire-reports/

publish:
runs-on: ubuntu-latest
needs: [ build ]
timeout-minutes: 30
env:
JDK_VER: 17
OSSRH_USER_TOKEN: ${{ secrets.OSSRH_USER_TOKEN }}
OSSRH_PWD_TOKEN: ${{ secrets.OSSRH_PWD_TOKEN }}
GPG_KEY: ${{ secrets.GPG_KEY }}
GPG_PWD: ${{ secrets.GPG_PWD }}
steps:
- uses: actions/checkout@v5
- name: Set up OpenJDK ${{ env.JDK_VER }}
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: ${{ env.JDK_VER }}
- name: Get quarkus version
run: |
QUARKUS_VERSION=$(./mvnw -B -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec -f quarkus/pom.xml)
echo "QUARKUS_VERSION=$QUARKUS_VERSION" >> $GITHUB_ENV
- name: Is SNAPSHOT release ?
if: contains(github.ref, 'master') && contains(env.QUARKUS_VERSION, '-SNAPSHOT')
run: |
echo "DEPLOY_OSSRH=true" >> $GITHUB_ENV
- name: Is Release version ?
if: startsWith(github.ref, 'refs/tags/quarkus-v') && !contains(env.QUARKUS_VERSION, '-SNAPSHOT')
run: |
echo "DEPLOY_OSSRH=true" >> $GITHUB_ENV
- name: Publish to ossrh
if: env.DEPLOY_OSSRH == 'true'
run: |
echo ${{ secrets.GPG_PRIVATE_KEY }} | base64 -d > private-key.gpg
export GPG_TTY=$(tty)
gpg --batch --import private-key.gpg
cd quarkus
../mvnw -V -B -Dgpg.skip=false -DskipTests -s ../settings.xml deploy
55 changes: 55 additions & 0 deletions .github/workflows/quarkus-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Quarkus Agentic Release

on:
workflow_dispatch:
inputs:
rel_version:
description: 'Release version (examples: 0.1.0, 0.2.0-rc-1, 0.2.0-SNAPSHOT)'
required: true
type: string

jobs:
create-release:
name: Creates quarkus release tag
runs-on: ubuntu-latest
env:
JDK_VER: '17'
steps:
- name: Check out code
uses: actions/checkout@v5
with:
fetch-depth: 0
token: ${{ secrets.DAPR_BOT_TOKEN }}
persist-credentials: false
- name: Set up OpenJDK ${{ env.JDK_VER }}
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: ${{ env.JDK_VER }}
- name: Update quarkus version and tag
env:
GITHUB_TOKEN: ${{ secrets.DAPR_BOT_TOKEN }}
REL_VERSION: ${{ inputs.rel_version }}
run: |
set -ue

git config user.email "daprweb@microsoft.com"
git config user.name "Dapr Bot"
git remote set-url origin https://x-access-token:${{ secrets.DAPR_BOT_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git

# Update version in all quarkus pom.xml files
cd quarkus
../mvnw versions:set -DnewVersion=$REL_VERSION -DprocessDependencies=true
cd ..

if [[ "$REL_VERSION" == *-SNAPSHOT ]]; then
git commit -s -m "Update quarkus version to ${REL_VERSION}" -a
git push origin master
echo "Updated master with quarkus version ${REL_VERSION}."
else
git commit -s -m "Release quarkus-v${REL_VERSION}" -a
git tag "quarkus-v${REL_VERSION}"
git push origin master
git push origin "quarkus-v${REL_VERSION}"
echo "Tagged and pushed quarkus-v${REL_VERSION}."
fi
4 changes: 4 additions & 0 deletions .github/workflows/validate-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ on:
- release-*
tags:
- v*
paths-ignore:
- 'quarkus/**'

pull_request:
branches:
- master
- release-*
paths-ignore:
- 'quarkus/**'

jobs:
build:
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ on:
- release-*
tags:
- v*
paths-ignore:
- 'quarkus/**'

pull_request:
branches:
- master
- release-*
paths-ignore:
- 'quarkus/**'
jobs:
validate:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,7 @@
<!-- We are following test containers artifact convention on purpose, don't rename -->
<module>testcontainers-dapr</module>
<module>durabletask-client</module>
<module>quarkus</module>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new module should be independent from dapr-sdk and have its own release process

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can send this code to quarkus-Dapr. @yaron2 which comments, can you be more specific?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets keep it here, but not as a module of the sdk, it will have a different release process

</modules>

<profiles>
Expand Down
195 changes: 195 additions & 0 deletions quarkus/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# Quarkus Agentic Dapr

A Quarkus extension that bridges [LangChain4j's agentic framework](https://docs.langchain4j.dev/) with [Dapr Workflows](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/) for durable, observable AI agent orchestration.

## What it does

Every LLM call and tool call becomes a **durable Dapr Workflow activity**. If the process crashes, completed activities are recovered from the workflow history. Agent orchestration (sequential, parallel, loop, conditional) is backed by Dapr Workflows with parent-child hierarchy.

| Capability | Without Dapr | With `quarkus-agentic-dapr` |
|---|---|---|
| Durability | Lost on crash | Full workflow history persisted |
| Observability | Logs only | Dapr dashboard + per-activity tracking |
| Tool call audit trail | None | Every call recorded with inputs/outputs |
| LLM call audit trail | None | Every LLM request/response recorded |
| Code changes | — | **None** — just swap the dependency |

## Modules

```
quarkus/
├── runtime/ # Core extension (interceptors, workflows, activities)
├── deployment/ # Build-time processing (annotation scanning, CDI decorators)
├── quarkus-agentic-dapr-agents-registry/ # Optional: registers agents in Dapr state store
├── quarkus-langchain4j-dapr/ # Optional: Dapr Conversation API as ChatModel provider
└── examples/ # Built-in examples
```

## Supported Agent Types

All 5 LangChain4j orchestration types are supported:

| Annotation | Dapr Workflow |
|------------|---------------|
| `@Agent` | `AgentRunWorkflow` (per-agent ReAct loop) |
| `@SequenceAgent` | `SequentialOrchestrationWorkflow` |
| `@ParallelAgent` | `ParallelOrchestrationWorkflow` |
| `@LoopAgent` | `LoopOrchestrationWorkflow` |
| `@ConditionalAgent` | `ConditionalOrchestrationWorkflow` |

## Quick Start

### 1. Add the dependency

```xml
<dependency>
<groupId>io.dapr.quarkus</groupId>
<artifactId>quarkus-agentic-dapr</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
```

### 2. Configure

```properties
quarkus.dapr.devservices.enabled=false
quarkus.dapr.workflow.enabled=false

# Dapr sidecar endpoints
dapr.grpc.endpoint=${DAPR_GRPC_ENDPOINT:http://localhost:40001}
dapr.http.endpoint=${DAPR_HTTP_ENDPOINT:http://localhost:3500}
```

### 3. Write agents (standard LangChain4j — no Dapr-specific code)

```java
public interface WeatherAssistant {

@ToolBox(WeatherTools.class)
@UserMessage("Check the weather in {{city}}")
@Agent(name = "weather-assistant", outputKey = "weather")
String checkWeather(@V("city") String city);
}
```

### 4. Run with Dapr

```bash
# Terminal 1: Dapr sidecar
dapr run --app-id my-agent --dapr-grpc-port 40001 --dapr-http-port 3500 \
--resources-path ./components

# Terminal 2: Quarkus app
DAPR_GRPC_ENDPOINT=http://localhost:40001 \
DAPR_HTTP_ENDPOINT=http://localhost:3500 \
mvn quarkus:dev -Ddebug=false
```

## LLM Provider Options

### Ollama (direct)
```properties
quarkus.langchain4j.ollama.chat-model.model-id=llama3.1:8b
```

### Dapr Conversation API (provider-agnostic)
```properties
quarkus.langchain4j.chat-model.provider=dapr-conversation
quarkus.langchain4j.dapr.component-name=llm
```

Swap LLM providers by changing the Dapr component YAML — no Java code changes.

## Example Project

See [travel-planner-agents](https://github.com/javier-aliaga/travel-planner-agents) for a complete example with all agent types, Makefile targets, and Dapr components.

## Crash Recovery

If the process crashes mid-execution, completed agents are not re-run. The in-progress agent is automatically re-run from scratch using its original prompt and tools.

### How it works

1. **Normal operation**: LangChain4j's AiServices drives the ReAct loop. Each LLM call and tool call is recorded as a Dapr Workflow activity.
2. **Crash**: The process dies. Dapr workflow history persists.
3. **Restart**: Dapr replays the workflow. Completed activities return cached results instantly.
4. **Recovery detection**: The in-progress activity is re-dispatched but fails because the in-memory `AgentRunContext` is gone. `AgentRunWorkflow` catches the failure.
5. **Agent re-run**: `RecoveryAgentActivity` re-executes the agent's entire ReAct loop from scratch — calling `ChatModel.chat()` directly and invoking tools via `ToolRegistry`.

### Recovery granularity

| Scope | Behavior |
|-------|----------|
| Orchestration (e.g., Agent1 → Agent2 → Agent3) | Completed agents are skipped (Dapr child workflow replay). Only the in-progress agent re-runs. |
| Single agent (LLM calls + tool calls) | The entire agent re-runs from its original prompt. Individual LLM/tool calls within the agent are not skipped. |

### Demo: simulating a crash

```bash
# 1. Start the app and trigger a multi-agent workflow
curl "http://localhost:8080/travel/plan?origin=NYC&destination=Paris"

# 2. Kill the process mid-execution (e.g., during the second agent)
kill -9 <pid>

# 3. Restart the app — the workflow resumes automatically
mvn quarkus:dev
```

In the Dapr dashboard, completed agents show cached results. The crashed agent is detected via `TaskFailedException`, then re-runs and completes.

### Key classes

| Class | Role |
|-------|------|
| `AgentRunWorkflow` | Catches `TaskFailedException` on activity failure, triggers recovery |
| `RecoveryAgentActivity` | Self-contained ReAct loop: ChatModel.chat() + tool dispatch |
| `ToolRegistry` | CDI bean that discovers @Tool methods at startup for recovery |
| `AgentToolClassRegistry` | Maps agent names to their @ToolBox classes (populated at build time) |

## AgenticScope Checkpointing

LangChain4j's agentic scope (the shared state and conversation context of a multi-agent
workflow) can be checkpointed to a Dapr state store — the LangChain4j equivalent of a
LangGraph checkpointer. Every scope update is persisted, so agentic state survives
restarts and is shareable across replicas.

```properties
dapr.agentic.scope-store.enabled=true
dapr.agentic.scope-store.name=kvstore
```

| Class | Role |
|-------|------|
| `DaprAgenticScopeStore` | `AgenticScopeStore` implementation over the Dapr state API |
| `AgenticScopeStoreInitializer` | Registers the store with LangChain4j's `AgenticScopePersister` at startup |

## Known Limitations

- **Single replica only (current design)**: live agent execution keeps per-run state in
JVM memory — the `AgentRunContext` registry, the `CompletableFuture` the agent thread
blocks on, and the per-thread Dapr context. Dapr Workflow, however,
[randomly load-balances activities across all replicas](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-architecture/)
of an app-id, with no locality to where a run started. With more than one replica an
LLM/tool activity can be dispatched to a replica that lacks the in-memory context, fail
to find it, and surface as a (false) crash-recovery while the originating request blocks
until the call timeout. **Deploy a single replica** until execution state is moved into
workflow history (control inversion — see [Crash Recovery](#crash-recovery)); this is the
same root cause as agent-level (not per-call) recovery.
- **Recovery granularity**: Agent-level only — individual LLM/tool calls within an agent are re-executed (not skipped)
- **Same agent name in concurrent _parallel_ orchestrations**: within a single
orchestration each run's Dapr context is bound to its own run ID — including every
iteration of a `@LoopAgent` and all sequential agents, which run on the planner's
thread and route through the context it sets directly. Only when two *different*
concurrent requests run the same agent name on parallel executor threads at the same
instant can the FIFO name binding cross-attribute their runs; run IDs stay unique so
routing is still correct, but observability may interleave.
- **daprd 1.18.0 workflow race**: a workflow event can be lost by the runtime (the
workflow completes app-side but stays RUNNING in daprd, its event reminder fires on
an empty inbox) — fix upstream in
[dapr/dapr#10054](https://github.com/dapr/dapr/pull/10054). `AgentRunWorkflow` now
self-heals: each `agent-event` wait has a 60s timeout that re-arms and lets replay
redeliver the missed event from history, turning a permanent hang into a bounded
delay. The e2e tests still retry once locally and are skipped on CI (slow runners
widen the race window) until a fixed runtime ships.
- **Small models**: llama3.2 (3B) sometimes malforms tool call arguments; llama3.1:8b+ recommended
Loading
Loading