Skip to content

Commit 1d394f0

Browse files
authored
Agent SDK: merge Agentspan + porting-spec conformance
2 parents 06b9f14 + 0479e70 commit 1d394f0

388 files changed

Lines changed: 58658 additions & 24 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/agent-e2e.yml

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
name: Agent E2E
2+
3+
# Runs the agent e2e suites (conductor-ai-e2e/) against the Conductor OSS
4+
# server boot JAR (Maven Central) — the server this SDK ships against, with
5+
# the agent runtime on by default from 3.32.0-rc.8 onward. The Agentspan
6+
# server JAR is no longer used here.
7+
#
8+
# These tests call real LLMs via the OPENAI_API_KEY / ANTHROPIC_API_KEY
9+
# repo secrets. The suites themselves never read the keys (asserted by
10+
# Suite2ToolCallingCredentials) — only the server process gets them.
11+
# Fork PRs cannot see repo secrets, so for them the run fails at the
12+
# silently-empty guard rather than passing vacuously.
13+
14+
on: [pull_request, workflow_dispatch]
15+
16+
permissions:
17+
contents: read
18+
19+
concurrency:
20+
group: agent-e2e-${{ github.ref }}
21+
cancel-in-progress: true
22+
23+
env:
24+
CONDUCTOR_SERVER_VERSION: "3.32.0-rc.8" # pinned server release — bump deliberately
25+
26+
jobs:
27+
agent-e2e:
28+
runs-on: ubuntu-latest
29+
timeout-minutes: 45
30+
env:
31+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
32+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
33+
AGENTSPAN_SERVER_URL: http://localhost:8080/api
34+
steps:
35+
- name: Checkout code
36+
uses: actions/checkout@v4
37+
38+
- name: Set up Java
39+
uses: actions/setup-java@v4
40+
with:
41+
distribution: temurin
42+
java-version: '21'
43+
44+
# Python is only needed for mcp-testkit and the XML guard.
45+
# No `cache: pip` — it hard-fails without a requirements file.
46+
- name: Set up Python
47+
uses: actions/setup-python@v5
48+
with:
49+
python-version: '3.12'
50+
51+
- name: Cache server JAR
52+
id: jar_cache
53+
uses: actions/cache@v4
54+
with:
55+
path: conductor-server.jar
56+
key: conductor-oss-server-${{ env.CONDUCTOR_SERVER_VERSION }}
57+
58+
- name: Download server JAR from Maven Central
59+
if: steps.jar_cache.outputs.cache-hit != 'true'
60+
run: |
61+
curl -fL --retry 3 -o conductor-server.jar \
62+
"https://repo1.maven.org/maven2/org/conductoross/conductor-server/${CONDUCTOR_SERVER_VERSION}/conductor-server-${CONDUCTOR_SERVER_VERSION}-boot.jar"
63+
64+
- name: Install mcp-testkit
65+
run: |
66+
python -m pip install --upgrade pip
67+
pip install mcp-testkit
68+
69+
- name: Start mcp-testkit
70+
run: |
71+
mcp-testkit --transport http --port 3001 &
72+
sleep 2
73+
74+
- name: Start server
75+
run: |
76+
java -jar conductor-server.jar --server.port=8080 > server.log 2>&1 &
77+
78+
- name: Wait for server health
79+
run: |
80+
for i in $(seq 1 45); do
81+
if curl -sf http://localhost:8080/health | grep -q '"healthy"[[:space:]]*:[[:space:]]*true'; then
82+
echo "server healthy after ~$((i*2))s"; exit 0
83+
fi
84+
sleep 2
85+
done
86+
echo "::error::server failed to become healthy within 90s"
87+
tail -100 server.log
88+
exit 1
89+
90+
- name: Run e2e suites
91+
run: ./gradlew :conductor-ai-e2e:test -Pe2e
92+
93+
# BaseTest assumeTrue-skips every suite when the server is unreachable —
94+
# without this guard a boot failure after the health gate (or a future
95+
# gate regression) would yield a green job that ran nothing.
96+
- name: Guard against silently-empty runs
97+
if: always()
98+
run: |
99+
python - <<'EOF'
100+
import glob
101+
import sys
102+
import xml.etree.ElementTree as ET
103+
104+
total = executed = 0
105+
for path in glob.glob("conductor-ai-e2e/build/test-results/test/TEST-*.xml"):
106+
root = ET.parse(path).getroot()
107+
t = int(root.get("tests", 0))
108+
sk = int(root.get("skipped", 0))
109+
total += t
110+
executed += t - sk
111+
print(f"executed {executed}/{total} tests")
112+
sys.exit(0 if executed > 0 else 1)
113+
EOF
114+
115+
- name: Upload results
116+
if: always()
117+
uses: actions/upload-artifact@v4
118+
with:
119+
name: agent-e2e-results
120+
path: |
121+
conductor-ai-e2e/build/test-results/test/
122+
conductor-ai-e2e/build/reports/tests/test/
123+
server.log
124+
retention-days: 14

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@
22

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

5+
## [Unreleased]
6+
7+
### Added
8+
9+
- 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)
10+
- `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
11+
- 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`
12+
- 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
13+
- 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()`
14+
- `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
15+
- 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. The resolution lives in the client layer — `ApiClient.builder().useEnvVariables(true)` / `new ApiClient()` — not on `AgentRuntime` (matching the Python SDK, where `AgentRuntime()` defaults to `Configuration()`); `AgentRuntime` exposes no client factories
16+
- `AgentConfig` knobs with lenient env parsing (invalid/empty → default): `autoStartWorkers`, `daemonWorkers`, `streamingEnabled`, `livenessEnabled`, `livenessStallSeconds`, `livenessCheckIntervalSeconds`
17+
- 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)
18+
- 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
19+
- 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
20+
- `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`
21+
22+
### Changed
23+
24+
- `useEnvVariables(true)` (and the `new ApiClient()` no-arg constructor) no longer throws when `CONDUCTOR_SERVER_URL` is unset — it falls back to `AGENTSPAN_SERVER_URL`, then `http://localhost:8080/api`; the resolved URL is normalized to end in `/api`, and credentials gain the `AGENTSPAN_AUTH_KEY`/`AGENTSPAN_AUTH_SECRET` fallback
25+
526
## [5.1.0]
627

728
### Added

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,10 @@ workflowClient.restartWorkflow(workflowId, false);
424424
425425
Conductor supports AI-native workflows including agentic tool calling, RAG pipelines, and multi-agent orchestration.
426426
427+
**Durable AI Agents**
428+
429+
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).
430+
427431
**Agentic Workflows**
428432
429433
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.
@@ -542,6 +546,7 @@ End-to-end examples covering all APIs for each domain:
542546
| [Conductor Client](conductor-client/README.md) | HTTP client library documentation |
543547
| [Client Metrics](conductor-client-metrics/README.md) | Prometheus metrics collection |
544548
| [Spring Integration](conductor-client-spring/README.md) | Spring Boot auto-configuration |
549+
| [AI Agents](docs/agents/index.md) | Durable AI agent SDK (`conductor-ai`) guide |
545550
| [Examples](examples/README.md) | Complete examples catalog |
546551
547552
## Support

conductor-ai-e2e/build.gradle

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// End-to-end suites for the agent SDK. They need a live agentspan server
2+
// (AGENTSPAN_SERVER_URL) plus server-side LLM credentials, so — like the `tests`
3+
// module — every task is gated behind a property: run with -Pe2e.
4+
5+
plugins {
6+
id 'jacoco'
7+
}
8+
9+
dependencies {
10+
testImplementation project(':conductor-ai')
11+
12+
// test dependencies
13+
testImplementation "org.junit.jupiter:junit-jupiter-api:${versions.junit}"
14+
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${versions.junit}"
15+
testImplementation "ch.qos.logback:logback-classic:1.5.32"
16+
17+
// LLM frameworks: not imported by the suites directly, but the SDK's bridge
18+
// classes (compileOnly there) need them on the runtime classpath when the
19+
// framework-facing suites execute.
20+
testImplementation "dev.langchain4j:langchain4j:${versions.langchain4j}"
21+
testImplementation "dev.langchain4j:langchain4j-open-ai:${versions.langchain4j}"
22+
testImplementation "com.google.adk:google-adk:${versions.googleAdk}"
23+
testImplementation "org.bsc.langgraph4j:langgraph4j-core:${versions.langgraph4j}"
24+
testImplementation "org.bsc.langgraph4j:langgraph4j-agent-executor:${versions.langgraph4j}"
25+
}
26+
27+
// tool/agent parameter names are read reflectively at runtime
28+
compileTestJava.options.compilerArgs << '-parameters'
29+
30+
test {
31+
useJUnitPlatform()
32+
finalizedBy jacocoTestReport // report is always generated after tests run
33+
testLogging {
34+
events = ["SKIPPED", "FAILED"]
35+
exceptionFormat = "full"
36+
showStandardStreams = true
37+
}
38+
}
39+
40+
tasks.withType(Test) {
41+
// e2e suites are I/O-bound (LLM calls) and use unique agent/task names,
42+
// so they can safely run concurrently.
43+
maxParallelForks = 3
44+
}
45+
46+
jacocoTestReport {
47+
dependsOn test // tests are required to run before generating the report
48+
}
49+
tasks.forEach(task -> task.onlyIf { project.hasProperty('e2e') })

0 commit comments

Comments
 (0)