[Draft] Agent SDK Parity - #10
propp-orkes wants to merge 75 commits into
Conversation
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
…-13389-propp-a99bc3b9
…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
These 10 examples/*_probe.rs files were one-off manual verification scripts used while building the Agents feature (bare println! diagnostics against a live/mock server, no assertions, no doc comments) -- distinct from the sdk_playback_* examples, which are real regression-style examples. Zero references to them exist anywhere else in the repo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… gaps
Closes the two remaining substantive items tracked in
docs/agents/development-waves.md:
- Wave 6: WorkerConfig gains lease_extend_enabled/lease_extend_threshold
(default off, 0.8), and TaskRunner spawns a per-task heartbeat loop
alongside worker::execute() that sends TaskResult{extend_lease: true}
at that fraction of responseTimeoutSeconds, aborted as soon as
execution finishes. Ports python-sdk's LeaseManager design, with one
deliberate difference: a spawned tokio task per execution instead of a
shared background-thread manager, since tokio tasks are cheap enough
here that the coordination python needs isn't worth replicating.
- Wave 7: added SCHEMA_CLIENT.md (documents the already-implemented
SchemaClient) and LEASE_EXTENSION.md (the new feature above), and
corrected docs/agents/README.md, examples.md, and rust-sdk-design.md's
"nothing implemented yet" status headers, which were no longer true
now that Waves 1-5 have shipped.
The remaining Wave 7 item (a longer list of unrelated doc files, each
needing its own "does rust-sdk actually have this" check) is left open
as a separate, larger follow-up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…he way Checked each of the 14 remaining docs/agents/development-waves.md Wave 7 doc items against actual rust-sdk capability before writing (matching the SCHEMA_CLIENT.md/LEASE_EXTENSION.md precedent). Twelve were pure doc gaps and got new root-level docs (CORE_QUICKSTART, API_MAP, CONNECTION_AUTHENTICATION, SERVER_SETUP, WORKFLOW_LIFECYCLE, SCHEDULES_EVENTS, DEPLOYMENT_SCALING, RELIABILITY, OBSERVABILITY, DEBUGGING, SECURITY, UPGRADING). Two were real code gaps: - Workflow Message Queue: WorkflowClient::send_message didn't exist, and WorkflowTask had no way to construct a PULL_WORKFLOW_MESSAGES task (TaskType is a closed enum with no generic-string escape hatch). Added send_message (verified against the server's actual WorkflowMessageQueueResource.java endpoint), TaskType:: PullWorkflowMessages, and WorkflowTask::pull_workflow_messages/ .non_blocking(). Documented in the new WORKFLOW_MESSAGE_QUEUE.md. - TestWorkflowRequest's wire format didn't match the server's actual WorkflowTestRequest/TaskMock model (confirmed against WorkflowTestRequest.java directly): task_ref_to_mock_output was Map<String, Map<String, Value>> instead of Map<String, List<TaskMock>>, so retry-sequence and non-COMPLETED-status mocking was never actually possible; workflow_input serialized as "workflowInput" instead of "input"; and correlation_id/task_to_domain/priority/ external_input_payload_storage_path/sub_workflow_test_request were missing entirely. Fixed to match the server exactly, added the TaskMock type, and added TaskResultStatus::Canceled (the actual server enum has 5 values, not the 4 this crate had). with_mock_output's existing call sites, including the example, need no changes -- it now appends to a sequence instead of overwriting a flat map. Documented in the new WORKFLOW_TESTING.md. Verified: cargo fmt --check, both CI clippy commands (-D warnings), lib tests (580 passed), doc tests, the new wiremock-based workflow_message_queue_tests, and cargo doc --document-private-items. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a Wave 8 checklist to docs/agents/development-waves.md tracking the real parity gaps found in a full python-sdk vs rust-sdk source review (worker stall detection, AgentRuntime::resume, the agent testing/eval framework, missing TaskType variants, ServiceRegistryClient, and several smaller items), then implements the first and highest-impact one. Ports the server-side half of python-sdk's runtime/_liveness.py (ServerLivenessMonitor): AgentHandle::join now periodically fetches the full workflow and detects any task stuck SCHEDULED with zero polls past a stall threshold (default 30s, checked every 10s) -- the "task queued forever because no worker is polling" failure mode. Exposed via AgentHandle::join_with_options(stall_seconds, check_interval, policy), with join() using sensible defaults and StallPolicy::Warn (log and keep waiting). StallPolicy::Raise returns the new ConductorError::WorkerStall instead. Deliberately narrower than python's version, documented as such in src/agents/liveness.rs: python scopes the check to the execution's own worker domain (a random UUID assigned per stateful-agent execution), but rust's AgentRuntime has no equivalent per-execution domain concept at all -- register_agent_workers registers workers keyed only by task type name, shared across every concurrent execution. This crate's check is workflow-scoped instead (any stalled task in this execution's workflow), which is strictly more general but can occasionally flag a stall unrelated to this handle's own local tool workers. LocalLivenessCheck (OS-process-alive check) and WorkerRestarter (SIGKILL + process-supervisor respawn) aren't ported -- both depend on python's one-process-per-worker model, which this crate's tokio-task-per-worker model has no equivalent of. Verified: cargo fmt --check, both CI clippy commands (-D warnings), lib tests (584 passed, +4 net from 5 new liveness unit tests minus the now-dead poll_until_terminal helper this replaced), doc tests, the new wiremock-based agent_liveness_tests (3 tests), and cargo doc --document-private-items. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Re-attach to an execution this runtime didn't itself start -- typically after a process restart, since the execution is durable on the server regardless of which process started it. Registers the agent's local tool workers and starts them polling, then returns an AgentHandle bound to the given execution_id. Simpler than python-sdk's AgentRuntime.resume: python extracts the execution's per-run worker domain from its taskToDomain mapping and re-registers workers scoped to that domain, because each stateful python execution gets a random per-run domain. This crate's worker registration has no equivalent per-execution domain concept at all (confirmed while implementing Wave 8's liveness-detection item) -- workers are always registered by task-type name alone, shared across every concurrent execution of the same agent -- so resume() just reuses serve()'s own registration/start logic directly, with no domain extraction step needed. Also fixed serve()'s doc comment along the way: it claimed to block for the runners' whole lifetime, but TaskHandler::start() only blocks long enough to register and spawn them, then returns -- discovered while checking whether resume() could reuse the same start-up path. Verified: cargo fmt --check, both CI clippy commands (-D warnings), lib tests (585 passed, the new wiremock-based resume test runs in 0.27s -- deliberately not calling shutdown(), since the #[tokio::test] runtime dropping already aborts the spawned poller, avoiding TaskHandler::stop()'s 30s graceful-drain wait for a test with no real in-flight work to drain), doc tests, examples build, and cargo doc --document-private-items. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…done The previous commit implemented and shipped the ServerLivenessMonitor equivalent (AgentHandle::join's stall detection) but never updated its checklist entry. Also records the WorkerRestarter decision (N/A, already documented in liveness.rs) as resolved rather than open. LocalLivenessCheck remains genuinely open -- it needs its own design, not a literal port, since rust has no per-worker OS process to check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports the core, LLM-free slice of python-sdk's conductor.ai.agents.testing package into a new src/agents/testing.rs: mock_run/ScriptedEvent (deterministic execution scripting -- auto-executes a matching tool's real handler unless an explicit scripted result is given) and the fluent expect(result) assertion API (completed/failed/no_errors/ used_tool/used_tool_with_args/did_not_use_tool/output_contains). Required extending AgentResult with a new tool_calls field (a proper Vec<ToolCallRecord> instead of python's untyped dict-of-dicts) -- empty on a result built from a live /status poll (still no extraction path there, same honesty as every other field result.rs's module doc already lists as deliberately unpopulated), real on one mock_run builds directly from the script. ScriptedEvent only covers tool-call/tool-result/done/error, narrower than python's 10-variant MockEvent (also THINKING/HANDOFF/ GUARDRAIL_PASS/GUARDRAIL_FAIL/MESSAGE/WAITING): confirmed rust's real AgentEvent/SSE parsing only recognizes 5 wire event kinds today, so those extra MockEvent kinds have nothing real to script against yet. Recorded as its own new Wave 8 follow-up, since it surfaced while scoping this item, not something to silently paper over. Also fixes the checklist bookkeeping error from two commits ago: the liveness ServerLivenessMonitor item was implemented but never marked done. Still not ported, tracked as separate Wave 8 follow-ups: record/replay, LLM-judge semantic assertions (needs an LLM client this crate doesn't have), per-strategy structural validators, the LLM-backed correctness eval runner. The pytest plugin is N/A as designed. Verified: cargo fmt --check, both CI clippy commands (-D warnings) -- including working through a real must_use_candidate/must_use_candidate mismatch (clippy wants #[must_use] on every chainable assertion method, but a fluent chain's whole point is that the final call's return value is legitimately discardable; resolved with a documented, targeted #[expect(clippy::must_use_candidate)] rather than fighting the lint at every call site) -- lib tests (592 passed, +7 new), doc tests, examples build, and cargo doc --document-private-items. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…8, item 4) TaskType is a closed enum with no generic-string escape hatch, so these were literally impossible to construct before -- same shape as the PullWorkflowMessages fix from Wave 7. Adds: - TaskType::LlmSearchEmbeddings/GenerateImage/GenerateAudio. - WorkflowTask::llm_search_embeddings/generate_image/generate_audio constructors, baking in python-sdk's exact default values (1024x1024, n=1, outputFormat="png" for images) rather than guessing, per each task's python source file. - Optional-field builders: with_dimensions, with_n, with_image_dimensions, with_size, with_style, with_weight, with_output_format, with_text, with_voice, with_speed, with_response_format, with_prompt. Reused the existing with_namespace/with_max_results/with_embedding_model builders where the field is already shared with llm_search_index. Verified: cargo fmt --check, both CI clippy commands (-D warnings), lib tests (596 passed, +4 new), doc tests, examples build, license headers, and cargo doc --document-private-items. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports python's service_registry_client.py: registry CRUD, circuit breaker open/close/status, method management, and proto file storage. Verified the REST mapping against python's generated resource API rather than guessing. Required adding raw-bytes get/post support to ApiClient since proto file contents aren't JSON. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
….io data Closes the Wave 8 checklist item: GraphAgentDef stays the LangGraph answer (no adopted alternative crate). For LangChain, the doc's old "no dominant Rust equivalent" premise no longer holds -- rig/rig-agent is now dominant by download count -- but its async, prompt-dependent tool discovery and opaque ModelHandle don't fit FrameworkAgent's synchronous extraction contract, so no adapter is written. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports python's deploy(agent, schedules=...) tri-state reconcile semantics (leave alone / purge all / upsert+prune) as a new deploy_with_schedules method, backed by a new agents::schedule module built on the existing SchedulerClient. While mapping fields against the real WorkflowSchedule server DTO, found and fixed missing description/pausedReason/nextRunTime fields and a description/ updatedTime wire-name mismatch in the schedule models. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports python's AIOrchestrator/LLMProvider/VectorDB/IntegrationConfig as a thin typed wrapper over the existing IntegrationClient/ PromptClient, so callers don't have to hand-build IntegrationUpdate/ IntegrationApiUpdate payloads to register an AI model or vector store. Drops two dead parameters python itself never uses (prompt_test_workflow_name, test_prompt_template's unused max_tokens) rather than porting inert state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
inventory was declared for a never-built #[worker] auto-discovery macro and had zero real uses anywhere in the repo. Decided against wiring it up: Rust callers already register workers/agents/tools at a well-typed call site, and inventory::submit! collection has a real failure mode (silently dropped registrations across binary/linker boundaries) that explicit registration doesn't share. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports python's claude_code.py alias table (opus/sonnet/haiku -> full model ids) as resolve_claude_code_model, exposed via a new with_model_alias builder method rather than changing with_model's existing literal-passthrough behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Investigated by reading the real server source directly (AgentSSEEvent.java, AgentEventListener.java, AgentHumanTask.java) rather than trusting python's EventType. Found the previous 5-variant enum had two variants (Message/Progress) matching no real server event at all, and two more (Waiting/Error) with wrong field shapes. Replaced with the verified real 12-variant set, which also covers three event kinds (ContextCondensed/SubagentStart/SubagentStop) python's own EventType is missing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…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>
| # 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. | |
There was a problem hiding this comment.
Let's remove all of the .md files.
We should have at-most 1 .md file explaining how to use agents
|
@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
left a comment
There was a problem hiding this comment.
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?
| /// `AgentRuntime::run` only starts an execution and polls it — unlike python-sdk's `run()`, it | ||
| /// does not also register/poll local tool workers (`AgentRuntime::serve`) for the duration of | ||
| /// the call. Every playback example that uses a client-side tool has to do that wiring itself: | ||
| /// spawn `serve` on a second runtime instance pointed at the same server, `run` to completion, | ||
| /// then abort the spawned poller. Confirmed against python-sdk's `runtime.py::run`, which calls | ||
| /// `self._prepare_workers(...)` internally right after starting — a real, currently-undocumented | ||
| /// parity gap in this crate's `AgentRuntime::run`, not something specific to these examples. | ||
| // This shared file is included by every `sdk_playback_*` example; the function is actually |
There was a problem hiding this comment.
can this comment be compressed down? also the rust doc comments shouldn't be mentioning another sdk because the reader shouldn't have to cross-reference the rust sdk with an sdk for a language they might not even know.
| //! Python's hook methods are plain `def` (synchronous) — invoked from a sync chaining helper. | ||
| //! This crate's [`Worker`](crate::worker::Worker) trait, the closest existing precedent for "a | ||
| //! trait a caller implements and this crate stores as a boxed trait object," is instead defined | ||
| //! with `#[async_trait]` (`async-trait` is already a workspace dependency — see `Cargo.toml`) | ||
| //! precisely because handlers registered into an async runtime may need to do async work of | ||
| //! their own (write to a metrics store, call an audit-log service, etc.) without blocking the | ||
| //! executor thread. `CallbackHandler` follows that same convention rather than python's | ||
| //! synchronous one: matching `Worker`'s established async-trait shape in this codebase takes | ||
| //! priority over matching python's sync methods verbatim, since the wire/behavioral contract | ||
| //! (six named hooks, `Option` return, chain-until-non-empty semantics) is what parity actually | ||
| //! requires — *how* a Rust caller is allowed to implement a hook body is not. |
There was a problem hiding this comment.
likewise here with the Python mention. there's more of these python-focused docstring comments in this PR. for comments like this one around design: could they go into a separate .md file describing the design? and for the smaller docstring comments mentioning Python: can we check if they are necessary or if there's a more general way to phrase it?
| let mut command = Command::new(CLAUDE_BINARY); | ||
| command | ||
| .args(&args) | ||
| .stdin(Stdio::null()) |
There was a problem hiding this comment.
I think we'll need a stdin if there are structured follow up questions or if the user doesn't set bypass permissions and then claude code requests permission.
| pub fn with_model(mut self, model: impl Into<String>) -> Self { | ||
| self.model = Some(model.into()); | ||
| self | ||
| } |
There was a problem hiding this comment.
this should do a check that the model starts with "openai/" and if it doesn't then prepend it with that.
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.
| // spawned tokio task (cheap here, unlike an OS thread) rather than a shared | ||
| // background-thread manager. | ||
| let heartbeat_handle = Self::maybe_spawn_lease_heartbeat(task_client, &task, config); | ||
|
|
||
| // Execute the worker - pass reference to avoid clone in worker trait | ||
| let exec_result = worker.execute(&task).await; |
There was a problem hiding this comment.
a panic in the future itself would keep the heartbeat open indefinitely. I'm not entirely sure how likely that would be to happen. the tokio joinhandle on drop just detaches rustdoc so in a panic it would stay open.
Summary
Brings the Rust SDK's Agents feature (
agentsCargo feature) to comprehensive parity withpython-sdk's
conductor.ai.agents, plus a handful of core (non-agents) improvements found andfixed along the way. Delivered in 8 waves, tracked in detail in
docs/agents/development-waves.md— that file is theauthoritative, 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)
Guardrail/RegexGuardrail/LlmGuardrail,TerminationCondition,SwarmTransition,CallbackHandler,ConversationMemory,Credentials.AgentDef: guardrails, termination, router/swarm/plan-execute strategies,callbacks, memory, structured
output_type.TaskDef.runtime_metadataatregistration,
#[tool]macro support for a&Credentialsparameter, per-tool/per-agentcredential scoping.
AgentRuntime(compile/deploy/start/run/serve/resume),AgentHandle(
join/stream/approve/reject/respond),AgentEvent/AgentStream(SSE),AgentStatus/AgentResult.Framework adapters (Wave 5)
FrameworkAgenttrait + fallible conversion toAgentDef.async-openaiadapter (OpenAI Agents SDK tool shape).stream-jsontransport.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
TaskHandleruser, not just agents)lease_extend_enabled/lease_extend_threshold) so long-runningtasks don't get reassigned mid-execution.
TaskHandler::verify_workers_started— confirms every registered worker actually reached itsfirst 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_MESSAGEStask support) and aTestWorkflowRequest/TaskMockwire-format fix (verified against the server's actual Java DTO).Parity gaps found in a full-source review, all resolved (Wave 8)
AgentHandle::join(
ConductorError::WorkerStall,StallPolicy::Warn/Raise) plus the local startup checkabove.
AgentRuntime::resume— reattach to an existing execution after a process restart.mock_run/ScriptedEvent+expect(result)assertions).TaskTypevariants (GenerateImage/GenerateAudio/LlmSearchEmbeddings) withpython-matching defaults.
ServiceRegistryClient— full HTTP/gRPC service registry + circuit-breaker management (14methods, verified against python's generated REST client).
AgentRuntime::deploy_with_schedules— upsert/prune an agent's cron schedules in the samedeploy()call (found and fixed two real wire-format bugs in the schedule models along theway).
AiOrchestrator— typed convenience layer for registering LLM/vector-DB integrations(
add_ai_integration/add_vector_store/test_prompt_template/get_token_used)."opus"→"claude-opus-4-6", etc.).AgentEventfixed to match the real server SSE contract. This was the most consequentialfinding: 2 of the previously-shipped 5 event variants (
Message/Progress) matched no realserver 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), notpython — 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::restrictionacross the crate, fixing orconsciously 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:
— the dominant Rust LLM crate (
rig/rig-agent) can't be adapted viaFrameworkAgent(itstool 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 OpenAIAssistants API it wraps was fully sunset by OpenAI on 2026-08-26, so a port would ship a
feature that fails on every call.
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).
AgentEventhas no forward-compatible catch-all variant — a future 13th server event kindwould error out
AgentStream::nextrather than degrade gracefully. Pre-existing property ofthis design, documented, not fixed in this PR.
worker_isolation.py,_worker_entries.py, the pre-harmonization metrics collector) are confirmed N/A — rust'sworker 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
scenario from
conductor-oss/conductorPR #1614's shared cross-SDKllm-recordings/(93recordings 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.
(
ServiceRegistryClient,AiOrchestrator, agent liveness/stall detection, schedulereconciliation, worker-startup verification).
clippy::pedantic/clippy::restrictionclean,cargo fmtclean, rustdoc clean(
-D warnings), license headers clean, MSRV (1.85) clean.shared remote dev server (
sdkdev.orkesconductor.io) — confirmed transient/environmentalacross multiple pushes (different test fails each time, none touching files this PR changed),
not addressable from this repo.