Skip to content

[Draft] Agent SDK Parity - #10

Draft
propp-orkes wants to merge 75 commits into
mainfrom
CCOR-13389-propp
Draft

propp-orkes wants to merge 75 commits into
mainfrom
CCOR-13389-propp

Conversation

@propp-orkes

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

Copy link
Copy Markdown

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 17 commits September 16, 2026 13:36
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>
Comment thread DEBUGGING.md
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

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

@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 on lines +9 to +16
/// `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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread src/agents/callback.rs Outdated
Comment on lines +32 to +42
//! 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment on lines +56 to +59
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}

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 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.
Comment thread src/worker/task_runner.rs
// 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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