Skip to content

Agent SDK: merge Agentspan + porting-spec conformance#123

Open
kowser-orkes wants to merge 10 commits into
mainfrom
feature/combine-agentspan
Open

Agent SDK: merge Agentspan + porting-spec conformance#123
kowser-orkes wants to merge 10 commits into
mainfrom
feature/combine-agentspan

Conversation

@kowser-orkes

@kowser-orkes kowser-orkes commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Pull Request type

  • Bugfix
  • Feature
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • WHOSUSING.md
  • Other (please describe):

Changes in this PR

What

This PR contains two changes:

1. Combine Agentspan SDK into the Conductor SDK

  • 4 new modules: conductor-ai (durable AI agents — Agent, AgentRuntime, @Tool, guardrails, handoffs), conductor-ai-spring, conductor-ai-examples, conductor-ai-e2e (-Pe2e gated)
  • publishes org.conductoross:conductor-ai[-spring], supersedes conductor-agent-sdk 0.1.0 — migration = coordinate swap + relocated client/DTO/exception imports
  • agent control plane folded into conductor-client: AgentClient interface + OrkesAgentClient (11 /agent/* ops), via OrkesClients.getAgentClient(); DTOs → model.agent, exceptions → .exceptions
  • docs under docs/agents/; agent-e2e CI workflow

2. Conform to the Agent SDK porting spec (R1–R13)

  • SSE: connect failure throws SSEUnavailableException; reconnect w/ Last-Event-ID
  • verbs: serve = deploy + serve, serve(false, ...) non-blocking; AgentHandle send (now actually delivers) / stop / pause / resume / cancel / getStatus
  • RunSettings per-run LLM overrides
  • env: CONDUCTOR_* wins over AGENTSPAN_*; default server 8080 (was 6767); 6 AgentConfig knobs
  • worker credentials via runtimeMetadata wire contract, fail-closed; /workers/secrets fetch path deleted — see docs/design/secret-injection-contract.md
  • liveness: unpolled SCHEDULED task → WorkerStallError
  • swarm: transfer tools echo message; check_transfer first-wins + dropped_transfers
  • agent-e2e CI matrix: conductor-oss 3.32.0-rc.8 + agentspan 0.4.4; workflow token pinned contents: read

Validation

  • unit: 1001 + 324 + 9 (baseline 987 + 271 + 9)
  • live e2e both servers: agentspan 0.4.4 → 142/142; conductor-oss rc.8 → 142, 0 fail (Suite2 secret-write steps skip — read-only secret store)
  • live credential wire-delivery + DoD smoke green on both; spotless clean

Breaking

  • credentials need capable server: agentspan > 0.4.2 / conductor-oss ≥ 3.32.0-rc
  • default URL 67678080; send(message) no longer discards the message

🤖 Generated with Claude Code

Kowser and others added 3 commits July 9, 2026 19:07
Four new Gradle modules carrying the agent SDK from
agentspan-ai/agentspan sdk/java @ f8704fa3:

- conductor-ai         core SDK (107 main + 29 test files)
- conductor-ai-spring  Boot 3 auto-configuration (3 main + 2 test)
- conductor-ai-e2e     26 e2e suites, all tasks gated behind -Pe2e
- conductor-ai-examples 158 runnable examples (-PmainClass runner)

Sources are copied verbatim; the only churn is a mechanical
spotlessApply conforming them to the repo conventions (Apache header,
Conductor import order). Build files clone the shapes of
conductor-client-metrics, conductor-client-spring, tests, and harness
respectively; conductor-ai[-spring] publish via publish-config as
org.conductoross:conductor-ai[-spring]. Upstream docs land under
docs/agents/ (content pass to follow).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Modeled on the python-sdk agent-e2e action. Downloads the pinned
agentspan-server-0.4.0.jar release asset (cached), boots it on :8080
with the LLM repo secrets in the server env only, starts mcp-testkit,
then runs ./gradlew :conductor-ai-e2e:test -Pe2e.

A guard parses the JUnit XML and fails on zero executed tests, because
BaseTest assumeTrue-skips every suite when the server is unreachable —
without it a boot failure would green-wash the job. Fork PRs (no
secrets) fail at the guard by design. Needs OPENAI_API_KEY and
ANTHROPIC_API_KEY repo secrets before it can go green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- docs/agents/: dependency snippets now point at
  org.conductoross:conductor-ai[-spring]:5.1.0 (was
  org.conductoross.conductor:conductor-agent-sdk[-spring]:0.1.0);
  license note switched to Apache 2.0 (repo LICENSE); one source path
  updated to conductor-ai/src. Java package references are unchanged
  (the package itself did not move).
- Root README: Durable AI Agents blurb in the AI & LLM Workflows
  section and an AI Agents row in the Documentation table.
- CHANGELOG: Unreleased entry for the merge, coordinate supersession
  note, and the new agent-e2e workflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +23 to +121
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: agentspan-server-${{ env.AGENTSPAN_VERSION }}

- name: Download server JAR from release
if: steps.jar_cache.outputs.cache-hit != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download "v${AGENTSPAN_VERSION}" --repo agentspan-ai/agentspan \
--pattern "agentspan-server-${AGENTSPAN_VERSION}.jar" --output agentspan-server.jar

- 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
path: |
conductor-ai-e2e/build/test-results/test/
conductor-ai-e2e/build/reports/tests/test/
server.log
retention-days: 14
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ Complexity Δ
...ss/conductor/ai/spring/AgentAutoConfiguration.java 100.00% <ø> (ø) 4.00 <0.00> (?)
...conductoross/conductor/ai/spring/AgentCatalog.java 92.30% <ø> (ø) 19.00 <0.00> (?)
...ductoross/conductor/ai/spring/AgentProperties.java 100.00% <ø> (ø) 5.00 <0.00> (?)
...main/java/org/conductoross/conductor/ai/Agent.java 72.24% <ø> (ø) 68.00 <0.00> (?)
...ava/org/conductoross/conductor/ai/AgentConfig.java 64.28% <ø> (ø) 6.00 <0.00> (?)
...va/org/conductoross/conductor/ai/AgentRuntime.java 2.66% <ø> (ø) 7.00 <0.00> (?)
...org/conductoross/conductor/ai/CallbackHandler.java 0.00% <ø> (ø) 0.00 <0.00> (?)
...g/conductoross/conductor/ai/enums/AgentStatus.java 100.00% <ø> (ø) 1.00 <0.00> (?)
...org/conductoross/conductor/ai/enums/EventType.java 88.23% <ø> (ø) 2.00 <0.00> (?)
...org/conductoross/conductor/ai/enums/Framework.java 0.00% <ø> (ø) 0.00 <0.00> (?)
... and 72 more

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Kowser and others added 6 commits July 9, 2026 23:28
AgentClient (compile/deploy/start/status/respond) and the SSE streaming
client SseClient move from conductor-ai's internal package to
io.orkes.conductor.client; OrkesClients gains getAgentClient(). Their
transport DTOs (AgentRequest, StartResponse, AgentStatusResponse,
PendingTool, RespondBody, CompileResponse) land in
io.orkes.conductor.client.model.agent, and the typed exceptions
(AgentspanException, AgentAPIException, AgentNotFoundException) in
io.orkes.conductor.client.exceptions — the 404 -> AgentNotFoundException
mapping stays on the client (SuiteHttpApi404 contract).

Boundary adaptations, since conductor-client cannot see conductor-ai
types:
- AgentRequest is now a pure JSON transport DTO (@JsonInclude NON_NULL,
  no custom serializer). Domain serialization stays in conductor-ai:
  AgentRuntime.agentRequest() calls AgentConfigSerializer.serialize()
  and resolves the Framework wire name at build time; static plans are
  passed as plan.toJson().
- SseClient is transport-only and emits raw parsed JSON maps ([DONE]
  and type=done handling unchanged); AgentStream maps them to AgentEvent,
  keeping the log-and-skip contract for unmappable events.
- Credential* exceptions stay in conductor-ai, still extending the
  (relocated) AgentspanException base.

Tests for the moved exceptions relocate to conductor-client
(AgentExceptionsTest); docs/agents and CHANGELOG updated (migration is
a coordinate swap plus the relocated imports where used directly).

Validation: spotlessCheck (incl. -Pe2e) green; root test green
(conductor-client 987 incl. +3 relocated, conductor-ai 271/271 with
live server incl. 10 schedule integration tests, 0 failures);
live e2e vs agentspan-server-0.4.0: 138 tests, 137 executed, 0 failed
(1 env-gated media skip, same as baseline); Example01 smoke COMPLETED;
javadoc builds with no new warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…control-plane surface (spec R1/R5)

Split the concrete AgentClient into an interface (io.orkes.conductor.client.AgentClient)
plus an Orkes implementation (http.OrkesAgentClient), matching the SDK's
SchedulerClient/OrkesSchedulerClient convention. OrkesClients.getAgentClient()
now returns the interface.

New control-plane operations (Python-SDK parity, porting spec R1):
- getExecution(id)            GET  /agent/execution/{id}   (wire-faithful Map)
- listExecutions(params)      GET  /agent/executions       (query passthrough)
- stopAgent(id)               POST /agent/{id}/stop        (no body)
- signalAgent(id, message)    POST /agent/{id}/signal      ({"message": ...})
- streamSse(id, lastEventId)  factory for a connected SseClient
- close()                     no-op (shared ConductorClient owns transports)

SseClient upgrades: synchronous initial connect throwing SSEUnavailableException
when the server rejects streaming (never a silently-empty stream); id: frames
tracked; mid-stream drops reconnect with bounded backoff sending Last-Event-ID.

AgentRuntime constructs OrkesAgentClient and exposes getClient() — the same
instance the runtime uses for every /agent/* call (spec R5 acceptance).

Fork point: feature/combine-agentspan @ b261ff7.
Design: idea-12/design-java.md DD4 (interface + 11 ops), DD11 (R2 satisfied by
construction via ApiClient.buildCall — no token accessor added).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ifecycle, RunSettings, CONDUCTOR_* env chain (spec R3/R4/R8/R9)

serve = deploy + serve (R9): each served agent is deployed (idempotent) before
workers start, so a bare serve(agent) is a complete deployment. New
serve(Boolean blocking, ...) overloads — Boolean, not boolean, so the overload
stays strictly more specific than the serve(Object...) drop-in (a primitive
flag made serve(false, agent) ambiguous); blocking=false returns once workers
are polling.

AgentHandle lifecycle (Python parity): send() now delivers {"message": ...}
via respond (the placeholder silently discarded the message); stop() = graceful
control-plane stop + best-effort signal unblock; pause()/resume() via the
standard workflow endpoints (javadoc disambiguates handle.resume un-pause vs
AgentRuntime.resume re-attach); cancel(reason) terminates; getStatus() returns
the server snapshot (AgentStatusResponse — the Java analogue of Python's
AgentStatus dataclass; the plan's AgentStatus enum is terminal-only and cannot
represent a running execution).

Env chain (R3): CONDUCTOR_SERVER_URL → AGENTSPAN_SERVER_URL → default, same
chain for AUTH_KEY/AUTH_SECRET; default host 6767 → 8080 at both resolution
sites; blank values never clobber the chain; injectable env seam for tests.

AgentConfig (R4): +autoStartWorkers/daemonWorkers/streamingEnabled/
livenessEnabled/livenessStallSeconds/livenessCheckIntervalSeconds with spec
defaults + env names and lenient parsing (invalid/empty → default — fixes the
NumberFormatException wart). autoStartWorkers gates worker registration in
startAsync; streamingEnabled + SSEUnavailableException degrade stream() to a
polling-mode AgentStream (getResult polls the real status — never a
silently-empty COMPLETED); liveness knobs land before their S4 reader.
autoRegisterIntegrations skipped (dead flag, design DD8).

RunSettings (R8): builder-style per-run overrides (model/temperature/maxTokens/
reasoningEffort/thinkingBudgetTokens → thinkingConfig; no topP) applied to the
serialized root agentConfig at the choke point with a != null gate (zero values
honored); overloads on run/start/stream + async twins; drop-in Object varargs
extract RunSettings before tool coercion (else it would be swallowed as a tool).

streamAsync now rides AgentClient.streamSse (runtime transport = the exposed
client, R5/T8). conductor-ai gains a mockwebserver test dep pinned to 5.3.2 —
google-adk forces okhttp 5.x onto the test classpath, versions.okHttp (4.x)
NoClassDefFounds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…closed; retire /workers/secrets fetcher; e2e matrix conductor-oss 3.32.0-rc.8 + agentspan 0.4.4 (spec R6/R12)

Credentials now ride the wire contract (conductor-oss PR #1255): declared
secret names are stamped on TaskDef.runtimeMetadata at registration; a capable
host (agentspan > 0.4.2 / conductor-oss >= 3.32.0-rc) resolves them at poll
time and delivers values on the wire-only Task.runtimeMetadata map.

- TaskDef gains List<String> runtimeMetadata (declared names); Task gains
  Map<String,String> runtimeMetadata (delivered values); SerDer templates
  extended so the round-trip covers the new wire fields.
- WorkerManager: the task-def upsert now runs on EVERY register() call — the
  old !isNew early-return skipped it, which is exactly the re-register-wipes-
  the-stamp bug the porting spec calls out. Upsert is PUT-overwrite first
  (mirrors the Python task runner) with a create fallback.
- Dispatch is fail-closed: declared-but-undelivered names fail the task
  FAILED_WITH_TERMINAL_ERROR, naming the missing names and the server
  capability; ambient process env is NEVER read (proven with PATH in tests).
  CredentialContext (thread-local, per-call) unchanged — only the source moved.
- Deleted: WorkerCredentialFetcher (POST /workers/secrets + execution token)
  and the fetch-transport exceptions CredentialAuthException/
  CredentialRateLimitException/CredentialServiceException.
  CredentialNotFoundException stays (public ToolContext.getCredential API).
  T17 grep clean: no workers/secrets, no execution-token handling in main.
- agent-e2e workflow is now a two-leg matrix per idea.md Clarifications:
  conductor-oss server boot JAR 3.32.0-rc.8 (Maven Central) + agentspan
  release JAR bumped 0.4.0 → 0.4.4. Suite2 gains a capability probe
  (register TaskDef with runtimeMetadata, read it back) that skips the
  wire-delivery suite on servers that drop the field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… contract (spec R11/R13)

Liveness (R11): stateful runs enqueue worker tasks under a per-execution
domain — if the owning process stops polling, nothing else ever will and a
wait burns its full timeout on a dead queue. startAsync now attaches a
ServerLivenessMonitor (daemon per config.daemonWorkers) to stateful runs when
livenessEnabled: every livenessCheckIntervalSeconds it fetches the workflow;
a SCHEDULED task with pollCount == 0 older than livenessStallSeconds records
a stall, which AgentHandle.waitForResult surfaces as WorkerStallError on its
next poll iteration. Monitoring stops on first stall, terminal status, or
close; the handle closes the monitor on every wait exit. Transient poll
errors are ignored — liveness never fails a healthy run.

Swarm transfers (R13): {source}_transfer_to_{peer} workers now echo the
hand-off message ({"message": ...}) instead of discarding it into emptyMap;
{name}_check_transfer scans ALL matching tool calls — first wins
(transfer_to + transfer_message from inputParameters.message, tolerating the
"arguments" variant), every non-winning transfer lands in dropped_transfers
(only when > 1) with a warning naming honored and dropped targets. Both
handlers extracted to package-private statics (the inline lambdas were
untestable in isolation); shapes mirror the Python reference byte-for-byte.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… default + verb-contract notes

- CHANGELOG [Unreleased] describes the final agent surface in one place:
  AgentClient interface via OrkesClients.getAgentClient() with the full
  control plane, SSE hardening (SSEUnavailableException, Last-Event-ID
  reconnect), serve = deploy + serve + non-blocking variant, handle lifecycle
  verbs, RunSettings, CONDUCTOR_* env chain + 8080 default, runtimeMetadata
  credentials (fail-closed, fetch path deleted), liveness, swarm hand-off
  notes, and the two-server e2e matrix (conductor-oss 3.32.0-rc.8 +
  agentspan 0.4.4).
- NEW docs/design/secret-injection-contract.md — the doc WorkerManager and
  ToolContext javadoc have referenced all along now exists, describing the
  declare → stamp → deliver → inject contract, the fail-closed rule, what
  replaced the /workers/secrets fetch path, and server capability.
- docs/agents/*: port 6767 → 8080 everywhere; env tables document the
  CONDUCTOR_* → AGENTSPAN_* fallback chain and the new AgentConfig knobs;
  serve section documents deploy-first semantics and the non-blocking form;
  run section documents RunSettings.
- Test-infra defaults follow the SDK default: BaseTest (e2e) and
  ScheduleIntegrationTest fall back to localhost:8080 (were 6767).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
*/
public static ApiClient clientFromEnv() {
return client(
envVar("AGENTSPAN_SERVER_URL", "http://localhost:6767"),

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 shouldn't be here

The workflow declared no permissions block, so jobs got the default
token grant. It only uses the token to download the public agentspan
release JAR, so contents:read is sufficient.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kowser-orkes kowser-orkes changed the title Feature/combine agentspan Agent SDK: merge Agentspan + porting-spec conformance Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants