Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
143 changes: 143 additions & 0 deletions .github/workflows/agent-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: Agent E2E

# Runs the agent e2e suites (conductor-ai-e2e/) against a released server JAR
# with the agent runtime baked in, in two flavors:
# - conductor-oss: the Conductor OSS server boot JAR (Maven Central) — the
# server this SDK ships against
# - agentspan: the Agentspan server release JAR (kept alongside)
#
# These tests call real LLMs via the OPENAI_API_KEY / ANTHROPIC_API_KEY
# repo secrets. The suites themselves never read the keys (asserted by
# Suite2ToolCallingCredentials) — only the server process gets them.
# Fork PRs cannot see repo secrets, so for them the run fails at the
# silently-empty guard rather than passing vacuously.

on: [pull_request, workflow_dispatch]

# least privilege: the token is only used to download the public agentspan
# release JAR (gh release download) — read access suffices
permissions:
contents: read

concurrency:
group: agent-e2e-${{ github.ref }}
cancel-in-progress: true

jobs:
agent-e2e:
strategy:
fail-fast: false
matrix:
include:
# Pinned server releases — bump deliberately. Credential wire delivery
# (runtimeMetadata) needs conductor-oss PR #1255 (agentspan ≥ 0.4.3).
- server: conductor-oss
version: "3.32.0-rc.8"
- server: agentspan
version: "0.4.4"
name: agent-e2e (${{ matrix.server }} ${{ matrix.version }})
runs-on: ubuntu-latest
timeout-minutes: 45
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
AGENTSPAN_SERVER_URL: http://localhost:8080/api
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

# Python is only needed for mcp-testkit and the XML guard.
# No `cache: pip` — it hard-fails without a requirements file.
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Cache server JAR
id: jar_cache
uses: actions/cache@v4
with:
path: agentspan-server.jar
key: server-jar-${{ matrix.server }}-${{ matrix.version }}

- name: Download server JAR
if: steps.jar_cache.outputs.cache-hit != 'true'
env:
GH_TOKEN: ${{ github.token }}
SERVER_VERSION: ${{ matrix.version }}
run: |
if [ "${{ matrix.server }}" = "conductor-oss" ]; then
curl -fL --retry 3 -o agentspan-server.jar \
"https://repo1.maven.org/maven2/org/conductoross/conductor-server/${SERVER_VERSION}/conductor-server-${SERVER_VERSION}-boot.jar"
else
gh release download "v${SERVER_VERSION}" --repo agentspan-ai/agentspan \
--pattern "agentspan-server-${SERVER_VERSION}.jar" --output agentspan-server.jar
fi

- name: Install mcp-testkit
run: |
python -m pip install --upgrade pip
pip install mcp-testkit

- name: Start mcp-testkit
run: |
mcp-testkit --transport http --port 3001 &
sleep 2

- name: Start server
run: |
java -jar agentspan-server.jar --server.port=8080 > server.log 2>&1 &

- name: Wait for server health
run: |
for i in $(seq 1 45); do
if curl -sf http://localhost:8080/health | grep -q '"healthy"[[:space:]]*:[[:space:]]*true'; then
echo "server healthy after ~$((i*2))s"; exit 0
fi
sleep 2
done
echo "::error::server failed to become healthy within 90s"
tail -100 server.log
exit 1

- name: Run e2e suites
run: ./gradlew :conductor-ai-e2e:test -Pe2e

# BaseTest assumeTrue-skips every suite when the server is unreachable —
# without this guard a boot failure after the health gate (or a future
# gate regression) would yield a green job that ran nothing.
- name: Guard against silently-empty runs
if: always()
run: |
python - <<'EOF'
import glob
import sys
import xml.etree.ElementTree as ET

total = executed = 0
for path in glob.glob("conductor-ai-e2e/build/test-results/test/TEST-*.xml"):
root = ET.parse(path).getroot()
t = int(root.get("tests", 0))
sk = int(root.get("skipped", 0))
total += t
executed += t - sk
print(f"executed {executed}/{total} tests")
sys.exit(0 if executed > 0 else 1)
EOF

- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: agent-e2e-results-${{ matrix.server }}
path: |
conductor-ai-e2e/build/test-results/test/
conductor-ai-e2e/build/reports/tests/test/
server.log
retention-days: 14
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Merged the Agentspan agent SDK into this repository as four new modules: `conductor-ai` (durable AI agents — `Agent`, `AgentRuntime`, `@Tool` functions, guardrails, handoffs, multi-agent strategies), `conductor-ai-spring` (Spring Boot auto-configuration), `conductor-ai-examples` (150+ runnable examples), and `conductor-ai-e2e` (e2e suites, gated behind `-Pe2e`) — docs under [docs/agents/](docs/agents/index.md)
- `conductor-ai` and `conductor-ai-spring` publish as `org.conductoross:conductor-ai` and `org.conductoross:conductor-ai-spring`, superseding `org.conductoross.conductor:conductor-agent-sdk[-spring]@0.1.0`; the java package `org.conductoross.conductor.ai[.spring]` is unchanged apart from the client relocation below, so migrating is a dependency-coordinate swap plus — only if those types are imported directly — the relocated imports
- Agent control-plane clients now live in `conductor-client`: `AgentClient` is an interface (implemented by `io.orkes.conductor.client.http.OrkesAgentClient`) covering the full `/agent/*` control plane — `compileAgent`, `deployAgent`, `startAgent`, `getAgentStatus`, `getExecution`, `listExecutions`, `respond`, `stopAgent`, `signalAgent`, `streamSse`, `close` — handed out by `OrkesClients.getAgentClient()`; the SSE streaming client `SseClient` moved to `io.orkes.conductor.client`; their transport DTOs (`AgentRequest`, `StartResponse`, `AgentStatusResponse`, `PendingTool`, `RespondBody`, `CompileResponse`) moved to `io.orkes.conductor.client.model.agent` and the typed exceptions (`AgentspanException`, `AgentAPIException`, `AgentNotFoundException`, `SSEUnavailableException`) to `io.orkes.conductor.client.exceptions`
- SSE streaming hardened: the initial connect throws `SSEUnavailableException` when the server rejects streaming (never a silently-empty stream — `stream()` degrades to status polling), `id:` frames are tracked, and mid-stream drops reconnect with a `Last-Event-ID` header; `AgentRuntime.getClient()` exposes the runtime's own `AgentClient` instance
- Verb contract: `serve` = deploy + serve (each served agent is compiled + registered first, idempotently) with a non-blocking variant `serve(false, agents...)` that returns once workers are polling; `AgentHandle` gains the lifecycle verbs `send(message)` (now actually delivers `{"message": ...}` — it previously discarded the message), `stop()` (graceful, deterministic), `pause()`/`resume()` (un-pause; distinct from `AgentRuntime.resume(executionId, agent)` which re-attaches workers), `cancel(reason)`, and `getStatus()`
- `RunSettings` — per-run LLM overrides (`model`, `temperature`, `maxTokens`, `reasoningEffort`, `thinkingBudgetTokens`) on `run`/`start`/`stream` and their async variants; only non-null fields override the agent (zero values apply), mutating the serialized root config so overrides flow into the LLM tasks
- Connection environment: standard `CONDUCTOR_SERVER_URL`/`CONDUCTOR_AUTH_KEY`/`CONDUCTOR_AUTH_SECRET` now win over the legacy `AGENTSPAN_*` names (still honored as fallbacks); the default server URL is `http://localhost:8080` (was `6767`); blank variables no longer clobber the chain
- `AgentConfig` knobs with lenient env parsing (invalid/empty → default): `autoStartWorkers`, `daemonWorkers`, `streamingEnabled`, `livenessEnabled`, `livenessStallSeconds`, `livenessCheckIntervalSeconds`
- Worker credentials ride the `runtimeMetadata` wire contract: declared secret names are stamped on `TaskDef.runtimeMetadata` at (every) registration and a capable server (agentspan > 0.4.2 / conductor-oss ≥ 3.32.0-rc, conductor-oss PR #1255) delivers values on the wire-only `Task.runtimeMetadata` at poll time; dispatch is fail-closed (missing delivery → terminal failure; ambient process env is never read) — the `/workers/secrets` fetch path and its transport exceptions are deleted; see [docs/design/secret-injection-contract.md](docs/design/secret-injection-contract.md)
- Liveness for stateful runs: a `SCHEDULED` task with zero polls beyond `livenessStallSeconds` surfaces as `WorkerStallError` from the handle's wait instead of burning the full timeout
- Swarm hand-offs: transfer tools echo the hand-off `message`; `check_transfer` is first-wins with `transfer_message` and surfaces non-winning transfers in `dropped_transfers` (with a warning) instead of silently discarding them
- `agent-e2e` GitHub workflow runs the e2e suites as a two-server matrix: the Conductor OSS server boot JAR `3.32.0-rc.8` (Maven Central) and the released `agentspan-server-0.4.4.jar`

## [5.1.0]

### Added
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,10 @@ workflowClient.restartWorkflow(workflowId, false);

Conductor supports AI-native workflows including agentic tool calling, RAG pipelines, and multi-agent orchestration.

**Durable AI Agents**

The `conductor-ai` module is a full agent SDK on top of Conductor: `Agent`, `AgentRuntime`, `@Tool` functions, guardrails, handoffs, and multi-agent strategies, with Spring Boot auto-configuration in `conductor-ai-spring` and 150+ runnable examples in `conductor-ai-examples`. Start with the [agent docs](docs/agents/index.md).

**Agentic Workflows**

Build AI agents where LLMs dynamically select and call Java workers as tools. All agentic examples live in [`AgenticExamplesRunner.java`](examples/src/main/java/io/orkes/conductor/sdk/examples/agentic/AgenticExamplesRunner.java) — a single unified runner.
Expand Down Expand Up @@ -542,6 +546,7 @@ End-to-end examples covering all APIs for each domain:
| [Conductor Client](conductor-client/README.md) | HTTP client library documentation |
| [Client Metrics](conductor-client-metrics/README.md) | Prometheus metrics collection |
| [Spring Integration](conductor-client-spring/README.md) | Spring Boot auto-configuration |
| [AI Agents](docs/agents/index.md) | Durable AI agent SDK (`conductor-ai`) guide |
| [Examples](examples/README.md) | Complete examples catalog |

## Support
Expand Down
49 changes: 49 additions & 0 deletions conductor-ai-e2e/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// End-to-end suites for the agent SDK. They need a live agentspan server
// (AGENTSPAN_SERVER_URL) plus server-side LLM credentials, so — like the `tests`
// module — every task is gated behind a property: run with -Pe2e.

plugins {
id 'jacoco'
}

dependencies {
testImplementation project(':conductor-ai')

// test dependencies
testImplementation "org.junit.jupiter:junit-jupiter-api:${versions.junit}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${versions.junit}"
testImplementation "ch.qos.logback:logback-classic:1.5.32"

// LLM frameworks: not imported by the suites directly, but the SDK's bridge
// classes (compileOnly there) need them on the runtime classpath when the
// framework-facing suites execute.
testImplementation "dev.langchain4j:langchain4j:${versions.langchain4j}"
testImplementation "dev.langchain4j:langchain4j-open-ai:${versions.langchain4j}"
testImplementation "com.google.adk:google-adk:${versions.googleAdk}"
testImplementation "org.bsc.langgraph4j:langgraph4j-core:${versions.langgraph4j}"
testImplementation "org.bsc.langgraph4j:langgraph4j-agent-executor:${versions.langgraph4j}"
}

// tool/agent parameter names are read reflectively at runtime
compileTestJava.options.compilerArgs << '-parameters'

test {
useJUnitPlatform()
finalizedBy jacocoTestReport // report is always generated after tests run
testLogging {
events = ["SKIPPED", "FAILED"]
exceptionFormat = "full"
showStandardStreams = true
}
}

tasks.withType(Test) {
// e2e suites are I/O-bound (LLM calls) and use unique agent/task names,
// so they can safely run concurrently.
maxParallelForks = 3
}

jacocoTestReport {
dependsOn test // tests are required to run before generating the report
}
tasks.forEach(task -> task.onlyIf { project.hasProperty('e2e') })
Loading
Loading