Skip to content

Add Conductor-Agents to Rust SDK - #10

Merged
propp-orkes merged 80 commits into
mainfrom
CCOR-13389-propp
Sep 25, 2026
Merged

propp-orkes merged 80 commits into
mainfrom
CCOR-13389-propp

Conversation

@propp-orkes

@propp-orkes propp-orkes commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Brings the Rust SDK's Agents feature (agents Cargo feature) to comprehensive parity with
python-sdk's conductor.ai.agents, plus a handful of core (non-agents) improvements found and
fixed along the way. Delivered in 8 waves, tracked in detail in
docs/agents/development-waves.md — that file is the
authoritative, itemized record of what shipped, what was deliberately left out (and why), and
what's still open; this description summarizes it.

What's included

Core agent composition & runtime (Waves 1–4)

  • New composition types: Guardrail/RegexGuardrail/LlmGuardrail, TerminationCondition,
    SwarmTransition, CallbackHandler, ConversationMemory, Credentials.
  • Wired into AgentDef: guardrails, termination, router/swarm/plan-execute strategies,
    callbacks, memory, structured output_type.
  • Credentials delivery: declared credential names stamped onto TaskDef.runtime_metadata at
    registration, #[tool] macro support for a &Credentials parameter, per-tool/per-agent
    credential scoping.
  • AgentRuntime (compile/deploy/start/run/serve/resume), AgentHandle
    (join/stream/approve/reject/respond), AgentEvent/AgentStream (SSE), AgentStatus/
    AgentResult.

Framework adapters (Wave 5)

  • FrameworkAgent trait + fallible conversion to AgentDef.
  • async-openai adapter (OpenAI Agents SDK tool shape).
  • Claude Agent SDK subprocess/stream-json transport.
  • GraphAgentDef — a typed, Rust-native graph-declaration API for the LangGraph-shaped need
    (not a literal LangGraph port; see "Decisions" below for why).

Worker infrastructure (Wave 6, benefits every TaskHandler user, not just agents)

  • Lease-extension heartbeat (lease_extend_enabled/lease_extend_threshold) so long-running
    tasks don't get reassigned mid-execution.
  • TaskHandler::verify_workers_started — confirms every registered worker actually reached its
    first real poll within a timeout, catching an early panic or a starved task before it becomes
    a silent "nobody's listening" stall.

Docs (Wave 7) — 14 docs checked against actual capability (not rewritten from assumption);
two turned out to be real code gaps and were implemented rather than just flagged: Workflow
Message Queue (WorkflowClient::send_message, PULL_WORKFLOW_MESSAGES task support) and a
TestWorkflowRequest/TaskMock wire-format fix (verified against the server's actual Java DTO).

Parity gaps found in a full-source review, all resolved (Wave 8)

  • Worker liveness / stall detection: server-side stall detection in AgentHandle::join
    (ConductorError::WorkerStall, StallPolicy::Warn/Raise) plus the local startup check
    above.
  • AgentRuntime::resume — reattach to an existing execution after a process restart.
  • A core, LLM-free slice of the agent testing/eval framework (mock_run/ScriptedEvent +
    expect(result) assertions).
  • Missing TaskType variants (GenerateImage/GenerateAudio/LlmSearchEmbeddings) with
    python-matching defaults.
  • ServiceRegistryClient — full HTTP/gRPC service registry + circuit-breaker management (14
    methods, verified against python's generated REST client).
  • AgentRuntime::deploy_with_schedules — upsert/prune an agent's cron schedules in the same
    deploy() call (found and fixed two real wire-format bugs in the schedule models along the
    way).
  • AiOrchestrator — typed convenience layer for registering LLM/vector-DB integrations
    (add_ai_integration/add_vector_store/test_prompt_template/get_token_used).
  • Claude model alias resolution ("opus" → "claude-opus-4-6", etc.).
  • AgentEvent fixed to match the real server SSE contract. This was the most consequential
    finding: 2 of the previously-shipped 5 event variants (Message/Progress) matched no real
    server event at all, and 2 more (Waiting/Error) had the wrong field shape entirely.
    Re-derived from the actual server source (AgentSSEEvent.java/AgentEventListener.java), not
    python — the corrected 12-variant set now covers 3 event kinds python's own SDK doesn't even
    know about (ContextCondensed/SubagentStart/SubagentStop).

Also enabled clippy::pedantic + clippy::restriction across the crate, fixing or
consciously triaging every finding (many turns of work, not part of the agents effort per se,
but needed to keep CI green while iterating).

Parity confidence

Yes, with specific, itemized exceptions — no blanket "100% parity" claim:

  • Framework adapters are intentionally narrower than python's. LangChain: no adapter written
    — the dominant Rust LLM crate (rig/rig-agent) can't be adapted via FrameworkAgent (its
    tool discovery is async/prompt-dependent, no plain model-string getter); worth revisiting as a
    passthrough adapter if a concrete user asks. GPTAssistantAgent: not ported — the OpenAI
    Assistants API it wraps was fully sunset by OpenAI on 2026-08-26, so a port would ship a
    feature that fails on every call.
  • Testing/eval framework only covers the core, LLM-free slice. Record/replay, LLM-judge
    semantic assertions, per-strategy structural validators, and the LLM-backed correctness eval
    runner are not ported (each needs infrastructure — an LLM client, a recording format — this
    crate doesn't have yet).
  • AgentEvent has no forward-compatible catch-all variant — a future 13th server event kind
    would error out AgentStream::next rather than degrade gracefully. Pre-existing property of
    this design, documented, not fixed in this PR.
  • A few multiprocessing/GIL-specific python mechanisms (worker_isolation.py,
    _worker_entries.py, the pre-harmonization metrics collector) are confirmed N/A — rust's
    worker model has no equivalent problem to solve.

Full itemized list, including every "why not" with its reasoning on the record, is in
docs/agents/development-waves.md.

Verification

  • Playback-verified against real recorded LLM interactions: ported one Rust example per
    scenario from conductor-oss/conductor PR #1614's shared cross-SDK llm-recordings/ (93
    recordings across 19 scenarios) and replayed each end-to-end against a real server. 92/93 play
    back correctly — the one gap is an externally-unfixable recording artifact (a GitHub response
    ETag scoped to the original recording session), not a bug here. Found and fixed 5 real bugs
    this way that unit tests alone hadn't caught.
  • 625 unit tests, plus wiremock-backed integration tests for every new HTTP client
    (ServiceRegistryClient, AiOrchestrator, agent liveness/stall detection, schedule
    reconciliation, worker-startup verification).
  • clippy::pedantic/clippy::restriction clean, cargo fmt clean, rustdoc clean
    (-D warnings), license headers clean, MSRV (1.85) clean.
  • CI: all green except the pre-existing "Integration Tests (Enterprise)" flake against the
    shared remote dev server (sdkdev.orkesconductor.io) — confirmed transient/environmental
    across multiple pushes (different test fails each time, none touching files this PR changed),
    not addressable from this repo.

propp-orkes and others added 30 commits September 8, 2026 13:07
Guardrail, TerminationCondition, SwarmTransition, CallbackHandler,
ConversationMemory, and Credentials, ported from python-sdk's
conductor.ai.agents. Wired into agents::mod and the crate's top-level
re-exports; not yet consumed by AgentDef/AgentConfigSerializer (Wave 2).

Also fixes a clippy::drain_collect warning in TaskHandler::stop, and adds
the CredentialNotFound error variant plus the exhaustive-match arm it
required in events/exception.rs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Guardrail: full field + builder + AgentConfigSerializer wire support,
including a new GuardrailCheck::guardrail_type_fields() method so each
concrete guardrail type owns its own wire discriminant/fields.

TerminationCondition: field + with_termination() builder + tests on
AgentDef; serializer wiring still outstanding.

Also includes incidental cargo-fmt reformatting of callback.rs,
credentials.rs, memory.rs, and termination.rs picked up from a
misdirected worktree run (no logic changes).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Completes Agents parity Wave 2: every composition type added in Wave 1
(Guardrail, TerminationCondition, SwarmTransition, CallbackHandler,
ConversationMemory) is now wired into AgentDef and AgentConfigSerializer,
and all three previously-deferred Strategy variants are unlocked:

- Router: requires a router sub-agent (with_router), serializes recursively.
- Swarm: requires swarm_transitions (with_swarm_transition), serializes
  under the "handoffs" wire key for cross-SDK compatibility; OnCondition's
  unserializable closure mirrors python's taskName-reference convention.
- PlanExecute: requires a planner (with_planner), with optional fallback/
  fallback_max_turns/planner_context/synthesize.
- CallbackHandler: registered via with_callback but deliberately not
  serialized (no AgentRuntime yet to give it a real task-name reference).
- ConversationMemory: wired with_memory + full message/tool-call
  serialization matching python's wire shape.

requires_deferred_composition() is removed since nothing is deferred
anymore; with_strategy() now validates Router/PlanExecute's required
composition field directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…810e-97fd167641bc:baseline]

Conductor-Original-Branch: CCOR-13389-propp
Conductor-Original-Head: e6e6947
…bb74-292f08938b1f:baseline]

Conductor-Original-Branch: CCOR-13389-propp
Conductor-Original-Head: 7cecbcf
AgentHandle::stream() and AgentStream::new() were built by separate
Wave 4 subtasks (agent-handle, agent-stream-transport) against
different assumptions: stream() called AgentStream::new(client,
execution_id) synchronously, but AgentStream::new takes a raw
reqwest::Response. Make stream() async and have it open the SSE
response via AgentClient::stream() first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wave 4 (runtime): agent-runtime-types, agent-runtime-core,
agent-handle, agent-stream-transport.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TerminationCondition doctest imported it via the private
agents::termination module (E0603); the module is private, only
re-exported as agents::TerminationCondition.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…b30e-3386f825439d:baseline]

Conductor-Original-Branch: CCOR-13389-propp
Conductor-Original-Head: cd5da7d
The def-output-type and serializer-output-type code_parallel subtasks
built the OutputType wiring against different assumed builder shapes:
def.rs's with_output_type(class_name, schema) two-arg builder vs. a
test in serializer.rs calling it with a single OutputType value (only
caught by cargo test, not cargo build, since #[cfg(test)] code isn't
compiled by build). Fix the test call site and check off the
output_type line in development-waves.md, which the same run left
unchecked despite implementing it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…b91c-9595c7444afd:baseline]

Conductor-Original-Branch: CCOR-13389-propp
Conductor-Original-Head: ae59c59
propp-orkes and others added 2 commits September 17, 2026 13:03
…heck

Verifies every registered worker's polling task actually reached and
completed a first real poll within a timeout, catching an early panic
during setup or a spawned task that never reaches the network call --
the rust-native analog of python's fork()-failure check, since
tokio::spawn has no equivalent silent-failure mode to guard against.

Built on a new TaskRunner::poll_attempt_count() counter plus each
worker's JoinHandle::is_finished(). Not agent-specific (lives on the
core TaskHandler) and not auto-wired into AgentRuntime::serve/resume
(new task_handler() accessor lets callers opt in explicitly).

python's own LocalLivenessCheck turned out to be unreachable dead code
-- never called from runtime.py/worker_manager.py, no config field, no
test -- so this is a from-scratch rust-native design for the same
underlying problem, not a port of working python behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
OpenAI's Assistants API (client.beta.assistants.*/threads.*, what
GPTAssistantAgent and its _AssistantCall wrap) was sunset on
2026-08-26 with no extension -- every endpoint it calls now errors
unconditionally. Porting it would ship a feature that never works.
No architectural gap either way: it was always just a native Agent
with one custom tool, not a framework-adapter case needing new
machinery.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread DEBUGGING.md Outdated
Comment on lines +1 to +11
# Debugging Incidents

Start with safe evidence: workflow ID, task reference name, status, retry count, and
`reason_for_incompletion`. Confirm server reachability and authentication before changing
application code.

| Symptom | First check |
|---|---|
| Connection error | `CONDUCTOR_SERVER_URL` includes `/api` and the server is healthy (`curl $CONDUCTOR_SERVER_URL/../health`). |
| Task remains `SCHEDULED` | A worker is polling the exact task type (and `domain`, if set). |
| Authentication failure | `CONDUCTOR_AUTH_KEY`/`CONDUCTOR_AUTH_SECRET` target the active server. |

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.

Let's remove all of the .md files.

We should have at-most 1 .md file explaining how to use agents

@NicholasDCole

Copy link
Copy Markdown
Contributor

@propp-orkes , can you bring in github action against common action to prove that playback is working in CI? Should be able to reference feature branch conductor-oss/conductor#1614

@dfont-orkes dfont-orkes left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this isn't a full review of the PR. if you're able to can you help organize some of the longer docstring comments into design docs where it makes sense?

Comment thread examples/support/mod.rs Outdated
Comment thread src/agents/callback.rs Outdated
Comment thread src/agents/claude_agent_sdk.rs Outdated
Comment thread src/agents/framework_openai.rs Outdated
Merge the 7 design docs (README, python-sdk-reference, rust-sdk-design,
parity-plan, examples, framework-support, secrets-and-credentials) and
the top-level Agent_Parity_checklist.txt into one implementation-focused
README, now that the feature has shipped and the old planning docs'
python/java-sdk comparisons are no longer needed. Update every doc
cross-reference in source comments and the other top-level docs to point
at the merged file.
Cut every doc comment (///, //!) that was explaining design rationale,
porting history, or cross-referencing python-sdk/java-sdk/docs/agents/
files, since the feature has already shipped and none of that context
is useful for someone reading the code today. Kept a short description
per item plus any genuinely load-bearing usage note (fail-closed
behavior, panics, valid ranges, thread-safety), and left every doctest
code example untouched.
…dings

Runs every sdk_playback_* example against a Conductor server built from
conductor-oss/conductor#1614 (LLM recording/playback support, not yet
merged — pinned to a commit) in mock-LLM mode, then verifies each shared
recording was actually replayed via that PR's check-playback action.
Non-blocking for now: two of the shared recordings fail LLM-request
matching because the mcp-testkit test-tool server's tool definitions have
drifted since they were captured, a known upstream gap rather than an SDK
regression.
…ocally

The prior comment blamed mcp-testkit tool-definition drift for 04_http_and_mcp_tools
and one 16e_credentials_http_tool recording. Re-running the full suite with the
Python version actually pinned in this job (3.12, not the local default 3.14)
shows 04_http_and_mcp_tools fully passes — the earlier failure was this session's
own test-setup mistake, not a real gap. The one genuine, permanent gap is
16e_credentials_http_tool's 2nd recording, unplayable because GitHub scopes its
response ETag to the requesting identity/token.
cargo clippy --all-features -D warnings was failing on two doc comments
missing backticks around ZeroMQ/UUIDv4 (jupyter_executor.rs,
semantic_memory.rs) — both were in module-level //! headers on private
modules, so fixed by demoting them (see below) rather than just adding
backticks.

Every submodule under src/agents/ is a private `mod`, reachable only
through `pub use` re-exports in mod.rs; only the crate's top-level
`pub mod agents` is genuinely public. A //! or /// on anything not
actually reachable from outside the crate was never doing real
public-API-documentation work, so this converts those to plain //
comments: every private module's //! header, and every /// on a
private/non-reexported item, private method, or test-only helper.
Kept /// wherever it's on a genuinely re-exported item (or a pub
member of one), and always preserved `# Errors`/`# Panics` sections
on anything staying public, since this crate runs clippy::pedantic at
-D warnings (missing_errors_doc/missing_panics_doc/doc_markdown).
Left every existing doctest untouched, including guardrail.rs's
module-doc example.

Verified: cargo clippy --lib and --tests --examples --all-features
-D warnings clean, cargo fmt --check clean, cargo doc
--document-private-items with RUSTDOCFLAGS=-D warnings clean (no
broken intra-doc links from the demoted comments), 625 lib tests and
9 doctests pass (same doctest count as before).
python-sdk was the source of truth while this crate's implementation was
being built; now that it's at parity, most of those references were just
leftover porting-process narrative with no ongoing value (exact python
file/class/method citations, "matching python-sdk's X" asides, "ported
from python's Y" framing) rather than something a reader of this crate
today actually needs. Removed those across agent_client.rs,
ai_orchestrator.rs, service_registry_client.rs, worker_config.rs,
error.rs, ai_integration.rs, workflow_def.rs, task_handler.rs,
task_runner.rs, and examples/support/mod.rs, keeping the substance of
each comment (the actual behavior/rationale) intact. Also fixed one
doc comment in agent_client.rs that had gone stale independently,
claiming AgentStatus/AgentResult didn't exist yet.

Left references alone where they're still doing real work: naming-
compatibility aliases (ConductorClient/OrkesClients, the *_client()
aliases on ConductorClient) genuinely exist for Python SDK familiarity,
not just history; multi-SDK parity statements (Java/Go/Python together,
e.g. metrics bucket definitions) describe an ongoing cross-SDK contract,
not one-off porting notes; and the sdk_playback_* examples' comments
about matching python's exact behavior are load-bearing, since those
examples replay LLM recordings captured from real python-sdk runs.
Left root-level comparison docs (SDK_COMPARISON.md, WORKER_COMPARISON.md,
EXAMPLES_COMPARISON.md, DESIGN.md) and Cargo.toml's dependency-choice
comments untouched -- out of scope for a source-comment cleanup pass.
Same cleanup as the rest of the python-sdk reference pass: these
get_*_client() aliases are still worth documenting as aliases, but
citing Python SDK naming as the reason doesn't add anything a reader
needs today.
…hrasing

Continuation of the python-sdk reference cleanup. Kept the substance of
each comment (metrics stay cross-SDK-consistent by design; TaskContext
still gets the same one-line description) without naming specific
other-language SDKs as the reason.
Comment thread src/worker/task_runner.rs
@NicholasDCole
NicholasDCole marked this pull request as ready for review September 23, 2026 22:42
@NicholasDCole NicholasDCole changed the title [Draft] Agent SDK Parity Add Conductor-Agents to Rust SDK Sep 23, 2026
Comment thread src/models/task.rs Outdated
Comment thread src/worker/task_runner.rs Outdated
Comment thread src/agents/schedule.rs
Comment thread src/agents/schedule.rs Outdated
propp-orkes and others added 3 commits September 24, 2026 11:53
- Task: redact runtime_metadata values in Debug (may hold secrets,
  same as Credentials::Debug already does), and replace the long
  internal-design field doc with a short one pointing at
  Credentials::from_task.
- task_runner.rs: replace the hand-rolled AbortOnDrop with
  tokio_util::task::AbortOnDropHandle, which already does the same
  thing; adds tokio-util as a direct dependency.
- schedule.rs: reconcile() now saves every desired schedule before
  deleting pruned ones, so a transient error partway through doesn't
  risk deleting everything while creating nothing (save_schedule is
  an upsert, so this ordering is strictly safer).
- schedule.rs: documented the wire_name collision risk dfont-orkes
  flagged (non-blocking, matches every other Conductor SDK's wire
  format, not fixable on the Rust side alone).
- Fixed 3 dangling docs/agents/README.md references left over from
  that file's removal in a prior commit.
… SDKs

Splits the single docs/agents/README.md into a python-sdk/java-sdk-style
tree: a short README index, getting-started.md, concepts/ (one file per
topic: agents, tools, guardrails, termination, multi-agent, callbacks,
stateful, deploy-serve-run, streaming-hitl, scheduling), and reference/
(agent-definition, client, runtime). Content is re-derived from the
current src/agents/ source rather than carried over from the old
single-file version, which had drifted (e.g. it still documented a
plan() method on AgentRuntime that no longer exists). Scoped to what
this SDK actually ships today -- no frameworks/ directory, since no
framework adapters exist here, and no references to other SDKs in the
text itself.
Reconciles the closed-and-remerged PR's HTTP-client rework (PUT-with-
GET-fallback for schedule pause/resume, get_text/put_no_body, the
GrantedAccessResponse envelope fix, and the queue-config HashMap shape
fix) with this branch's agents work. Keeps both sides' additions where
they don't overlap (e.g. get_stream stays for agent streaming), takes
main's stricter/fixed test suite for authorization_client_tests.rs and
orkes_client_tests.rs, and re-adds `# Errors` doc sections main's
history lacks so this branch's pedantic clippy gate still passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@propp-orkes
propp-orkes merged commit 6db9437 into main Sep 25, 2026
27 of 28 checks passed
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.

4 participants