Skip to content

Merge Agentspan agent SDK into conductor-python#416

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

Merge Agentspan agent SDK into conductor-python#416
kowser-orkes wants to merge 36 commits into
mainfrom
feature/combine-agentspan

Conversation

@kowser-orkes

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

Copy link
Copy Markdown

Summary

Folds the Agentspan agent-authoring layer (conductor.ai.agentsAgent, AgentRuntime,
tool, guardrails, handoffs) into conductor-python, so a single package ships both
workflow/worker orchestration (conductor.client.*) and durable AI agents (conductor.ai.*).
Agentspan (conductor-agent-sdk) becomes redundant once this lands.

Changes

  • Copy conductor.ai.agents source from Agentspan; fix an eager langchain import that broke
    without langchain installed
  • Merge Agentspan's deps into pyproject.toml as optional Poetry extras (langchain,
    langgraph, adk, openai, openai-agents, anthropic, claude, combined agents);
    regenerate poetry.lock
  • Migrate the test suite into tests/ai/ (unit/integration/fixtures), registering
    integration/e2e/sse/agent_correctness/semantic markers
  • Migrate examples, dev scripts, and e2e suites into examples/agents, scripts/, e2e/
  • Add an agent-import smoke test + [agents]-extra job to PR CI
  • Add agent docs (docs/agents/*); update README with extras install instructions

Excluded from this pass (per idea.md): conductor.ai.cli, conductor.ai.models, validation/.

Test plan

  • Unit (tests/ai/unit): 1683 passed, 0 failed — hermetic, no server/LLM key needed
  • Integration/e2e: collect cleanly against a live server (133 + 135 items; 3 pre-existing
    collection errors, unrelated to this migration)
  • Live smoke test: AgentRuntime(...).deploy(agent) against a running Conductor server
    registered a real workflow definition, confirmed via /api/metadata/workflow

Known follow-ups (non-blocking)

  • tests/test_kitchen_sink.py (25 hermetic tests) wasn't carried over in the test migration
  • Tool-calling agent tests can hit _pickle.PicklingError under multiprocessing spawn (macOS
    default) — a known class of issue (conductor-python#264) between worker-process spawning and
    Agentspan's dynamic tool registration

@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
see 87 files with indirect coverage changes

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

@kowser-orkes kowser-orkes force-pushed the feature/combine-agentspan branch from 5d7ae95 to ac0a465 Compare July 8, 2026 06:56
@kowser-orkes kowser-orkes changed the title Feature/combine agentspan Merge Agentspan agent SDK into conductor-python Jul 8, 2026
@v1r3n v1r3n removed the request for review from nthmost-orkes July 8, 2026 23:40
Kowser and others added 22 commits July 8, 2026 17:08
Migrates src/conductor/ai/agents (+ conductor.ai package init) from the
Agentspan Python SDK (main-agentspan/sdk/python), excluding cli/ and
models/ per the merge plan.

Source: agentspan/main-agentspan @ sdk/python/src/conductor/ai

Fixes the one eager heavy-framework import found during design review:
frameworks/langchain.py imported langchain_core.callbacks.BaseCallbackHandler
at module scope to use as a base class, which would break a base
conductor-python install with no extras. The class is now built lazily by
_get_callback_handler_class() (memoized) so langchain_core is only
required when a LangChain-backed worker is actually created.
…regenerate poetry.lock

Adds cloudpickle as a core dependency (used broadly by the agent runtime),
and nine agent-framework packages (langchain, langchain-core,
langchain-openai, langgraph, google-adk, openai, openai-agents, anthropic,
claude-code-sdk) as optional deps activated via new [tool.poetry.extras]
groups (per-framework + a convenience "agents" bundle), per idea.md's
explicit ask. No console script added — src/conductor/ai/cli is excluded
from this merge. Carries over the pytest11 entry point
(agentspan-testing -> conductor.ai.agents.testing.pytest_plugin) as
always-on, matching Agentspan's existing behavior.

Floor versions for the langchain family and other framework packages are
set to what Agentspan's own uv.lock proved mutually compatible (e.g.
langchain>=1.2.13), not the original agentspan pyproject.toml floors
(e.g. langchain>=0.3.28) -- those older floors let Poetry's resolver
wander into today's incompatible-with-a-decayed-baseline dependency
space. Poetry also requires a concrete Python upper bound to reconcile
against every dependency's own <4.0.0 cap, so python is now
">=3.10,<4.0" instead of an unbounded ">=3.10" -- this changes no
practically-usable Python version (3.10-3.13 today, headroom through
3.99) and matches the same <4.0.0 convention these packages already use
themselves; it is unrelated to Agentspan's separate (and rejected)
<3.14 near-term cap.

Also adds the one-line `from __future__ import annotations` import to
9 copied files that lacked it, to satisfy this repo's ruff isort
required-imports rule (no per-file-ignore needed -- the copied tree is
otherwise fully compliant, including the banned-relative-imports rule).

Verified:
- poetry check --lock passes
- base install (no extras) + `import conductor.ai.agents` succeeds in a
  bare venv with none of the above packages installed
- `poetry install --extras agents` resolves and installs cleanly, and
  the lazily-built LangChain callback handler class instantiates
  correctly once langchain_core is present
Copies tests/{unit,integration,fixtures} from Agentspan into tests/ai/
(94 files); tests/cli/ (3 files, tested the excluded cli/ module) is not
copied. Registers the integration/e2e/sse/agent_correctness/semantic
markers in [tool.pytest.ini_options] so the default run can exclude them
cleanly.

Also removes src/conductor/ai/__main__.py: its sole purpose was
dispatching `python -m conductor.ai` to conductor.ai.cli:main, which is
out of scope for this merge -- as copied in Stage 1 it was silently
broken (bare `from conductor.ai.cli import main`, no guard). Note
agents/runtime/server.py's own two references to conductor.ai.cli are
NOT affected -- both are already wrapped in try/except and degrade
gracefully (falls back to `agentspan` on $PATH, then a manual-start
message) when the module is absent.

Drops 3 more tests discovered while migrating, for the same
tests-an-excluded-module reason as tests/cli/:
- unit/test_cli_binary.py, unit/test_main_entrypoint.py (test the
  excluded conductor.ai.cli directly)
- unit/validation/ (depends on the excluded top-level validation
  package)

Fixes 5 test files (test_langchain_worker.py,
test_langchain_executor_example.py) that imported the now-lazily-built
AgentspanCallbackHandler directly from
conductor.ai.agents.frameworks.langchain -- updated to call
_get_callback_handler_class() instead, matching the Stage 1 lazy-import
fix.

Fixes 5 test files (test_example_{109,110,111,113,114}_*.py) that locate
their companion example script via a hardcoded parents[N] relative path
-- landing tests one level deeper (tests/ai/unit/ vs source's
tests/unit/) shifted the correct offset from parents[2] to parents[3],
plus an extra "agents" path segment matching where Stage 4 will land
examples (examples/agents/, not examples/ flat).

Verified: full tests/ai collection (1816 tests, including
integration/e2e) has zero import errors. Running with
`-m "not integration and not e2e"`: 1639 passed, 44 failed -- all 44
are the test_example_{109,110,111,113,114}_*.py files whose companion
example scripts don't exist yet (that's Stage 4); expected to go green
once examples/ lands.
Copies examples/ (272 files, all subdirs: adk/, langgraph/, openai/,
claude_agent_sdk/, quickstart/, _configs/, etc.) into examples/agents/ --
nested under a subdir rather than merged into the existing flat
examples/ root, since that root already holds unrelated examples for
the pre-existing conductor.client.ai layer (agentic_workflow.py,
rag_workflow.py, test_ai_examples.py).

Copies scripts/run_examples.sh -> scripts/ (new top-level dir) and
e2e/ (24 files + assets/) -> e2e/ (new top-level dir), both as-is.

Verified:
- All 272 example + 24 e2e files py_compile cleanly.
- The 5 test_example_{109,110,111,113,114}_*.py tests fixed in the
  previous commit now pass for real (44 tests, 0 failures) now that
  their companion example scripts exist at examples/agents/.
- Full tests/ai suite: 1683 passed, 0 failed, 133 deselected
  (integration/e2e), with examples/ in place.
- 21 of 24 e2e files collect cleanly (135 tests). The other 3
  (test_suite4_mcp_tools.py, test_suite5_http_tools.py,
  test_suite11_langgraph.py) have pre-existing collection-time
  fragility unrelated to this migration: two import a local
  `mcp_test_server` support module that isn't vendored anywhere in the
  agentspan monorepo (Agentspan's own CI must provision it separately,
  e.g. from a private mcp-testkit repo), and one constructs a
  ChatOpenAI() client at module scope, which raises
  openai.OpenAIError if OPENAI_API_KEY isn't set. Collecting Agentspan's
  own e2e/ fresh, without that same infrastructure, would hit the
  identical errors -- this is not a regression introduced by the copy.
  e2e stays out of CI per the still-open "e2e test target" item;
  none of this affects tests/ai or the existing Conductor suites, which
  are collected via explicit paths that never traverse e2e/.
Extends the existing single unit-test job (rather than adding a new
job) to match its established pattern: per-step continue-on-error +
a final aggregating "Check test results" gate.

- "Verify agents import without extras installed" runs right after the
  existing base `pip install -e .` step (already extras-free) and
  before any agents-specific install, so it genuinely exercises the
  no-extras case -- this is the regression class the Stage 1 langchain.py
  fix addressed.
- "Install agents extra" + "Run agent tests" run after the three
  existing suites, installing `.[agents]` plus pytest-asyncio.
  pytest-asyncio is required explicitly here: it's declared in
  [tool.poetry.group.dev.dependencies], but this workflow installs via
  plain pip (not `poetry install`), which never sees poetry dependency
  groups -- confirmed by simulating the exact CI command sequence in a
  fresh venv, which reproduced 30 async-test failures
  ("async def functions are not natively supported") until
  pytest-asyncio was installed explicitly. pytest-xdist and
  pytest-rerunfailures (also dev-group-only) are not added since
  nothing in tests/ai depends on either.
- Both new steps feed the existing coverage-file-per-suite +
  `coverage combine` pattern automatically (the combine step already
  globs `.coverage.*`), and both are added to the final
  "Check test results" gate.

Verified by replicating this exact install/test sequence end-to-end in
a throwaway venv: base install + smoke test, all three existing suites,
agents-extra install, agent suite, coverage combine across all 4 files
-- all green (1683 agent tests passed, existing suites unaffected).

e2e/ is intentionally not wired in here (still-open item: what server
these hit once Agentspan's own is discarded).
Copies Agentspan's docs/ (6 files: README, getting-started, writing-agents,
advanced, framework-agents, api-reference) into docs/agents/. Fixes the
two install-instruction lines that referenced the now-discarded package:
docs/agents/README.md's PyPI badge and docs/agents/getting-started.md's
`uv add conductor-agent-sdk` -- both now point at
`pip install 'conductor-python[agents]'`. Left CLI command references
(`agentspan deploy`, `agentspan credentials set`, etc.) untouched --
those describe the separate Go CLI binary, which is its own release
artifact in the agentspan monorepo, not part of this merge or the
Agentspan Python SDK being discarded.

Adds a new "AI Agents" section to the root README (deliberately
separate from the existing "AI & LLM Workflows" section, which covers
the older conductor.client.ai orchestrator layer, not this durable
agent-runtime layer) plus a docs-table entry and an extras-install
callout in "Install the SDK". Mirrors the same addition in
examples/README.md's AI/LLM Workflows area, pointing at the new
examples/agents/README.md catalog rather than duplicating its 270+
entries.
Agentspan no longer runs as a separate server after this merge; agent
execution is served by the standard Conductor server, whose own client
(conductor.client.configuration.Configuration) already defaults to
localhost:8080. Aligns AgentConfig/WorkerCredentialFetcher defaults and
docs with that.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nests the migrated agent tests as an `ai` topic-subdir under Conductor's
pre-existing tests/unit and tests/integration parents (matching the
convention already used for automator/, orkes/, etc.), instead of the old
tests/ai/{unit,integration} structure that stood apart as a parallel
top-level grouping. tests/ai/fixtures is unaffected.

Fixes test_skill.py's FIXTURES path, which assumed fixtures was a sibling
one level up (true under the old layout) and pointed at a
nonexistent directory after the move. Updates the PR CI step and
scripts/run_agent_tests.sh's hardcoded paths to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tests/test_kitchen_sink.py (25 hermetic structural/behavioral tests
against examples/agents/kitchen_sink.py) sat at Agentspan's tests/ root,
outside every directory the original migration copied, so it was never
carried over. Placed under tests/unit/ai so the existing "Run unit
tests" CI step collects it — upstream it only ever ran via bare pytest
(testpaths=["tests"]) and was never a CI gate.

Three changes vs upstream:
- example-import sys.path adjusted for the tests/unit/ai depth
  (examples also moved to examples/agents in the merge)
- test_credential_file_used: ToolDef.secrets no longer exists — the
  field is `credentials`, matching @tool(credentials=[...])
- test_analytics_has_all_advanced_features: dropped `planner is True` —
  planner changed from a boolean to a PLAN_EXECUTE Optional[Agent] slot
  and the example never sets it

The last two assertions fail identically in the Agentspan repo itself —
they rotted upstream precisely because no CI ever ran this file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New agent-e2e.yml workflow restores automated execution to the two
agent test layers that had none after the merge: e2e/ (upstream ran it
on every PR in the Agentspan monorepo CI; the job didn't survive the
migration) and tests/integration/ai (never had CI anywhere).

Instead of Gradle-building the server and Go-building the CLI like
upstream, it downloads both prebuilt from the agentspan v0.4.0 GitHub
release (pinned via AGENTSPAN_VERSION; endpoint surface verified
identical to the SDK's source commit), boots the JAR on :8080, starts
mcp-testkit for the MCP/HTTP tool suites, and runs both layers with
junit output and the migrated HTML report generator.

Nightly + workflow_dispatch, not per-PR: the suites call real LLMs
(OPENAI_API_KEY / ANTHROPIC_API_KEY repo secrets, unavailable to fork
PRs) and take ~25-40 min. Two guards prevent a green-but-empty run:
a hard health gate on the server boot, and a post-run junit check that
executed - skipped > 0 (e2e/conftest.py skips the whole session when
the server is down).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Matches upstream Agentspan's cadence, where the python-e2e job ran on
every PR/push. Fork PRs cannot see the LLM-key repo secrets; for them
the run fails at the silently-empty guard rather than passing
vacuously.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tests/integration/ai goes back to manual-only — matching upstream
Agentspan, which never ran its tests/integration in CI. The per-PR
workflow now runs only the e2e suites against the released server JAR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The AgentspanError hierarchy + _raise_api_error move to
conductor/client/ai/agent_errors.py so the relocated agent API clients can
raise them without importing the agents layer. The old module re-exports the
same class objects, keeping except/isinstance behavior identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Schedule/ScheduleInfo dataclasses, the 4 schedule errors, and the scheduler
bridge (now AgentScheduleClient; ScheduleClient stays as an alias of the same
class) move under conductor/client/ai. OrkesClients gains
get_agent_schedule_client() (lazy import); the runtime and the standalone
AgentClient schedule surfaces build through it. Old
conductor.ai.agents.schedule.* modules re-export the same objects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The /agent/* endpoint methods, SSE streaming, and auth-header logic move to
conductor.client.ai.agent_api_client.AgentApiClient (Configuration-first;
an explicit token= is sent as-is, preserving the api_key-as-bearer semantic).
AgentClient keeps its constructor, convenience surface, and AgentHttpClient
alias, and now composes the transport; its _client attribute proxies to the
transport's httpx client so existing test/mock injection keeps working.
OrkesClients gains get_agent_client(); conductor.client.ai gains exports.

Auth behavior is unchanged in this commit (the JWT-exp mint cache moved
verbatim, as a private copy); consolidation onto Configuration's token
machinery lands separately. Two deliberate edge changes: an empty standalone
server_url now resolves through Configuration's default-host rules instead of
producing relative-URL errors, and the sync token path caches per-instance
instead of process-wide (both unobserved by tests; both superseded next
commit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AgentApiClient now stores minted tokens via Configuration.update_token() and
reuses them until auth_token_ttl_msec elapses — the same rule and the same
cache as every generated client built from that Configuration. This replaces
the private JWT-exp mint cache (and its opaque-token re-mint-per-request
special case: with fixed-TTL renewal a stale token can never be served
indefinitely, so the caching test now pins the TTL behavior instead).
conductor.ai.agents._internal.token_utils is untouched — the framework
adapters' event-push paths still use it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AgentScheduleClient naming (ScheduleClient alias noted), the
OrkesClients.get_agent_client()/get_agent_schedule_client() factories, and
the AgentApiClient transport home; hello_world_schedule example builds via
the factory. Verified live: example fired 5 scheduled executions to
COMPLETED against the v0.4.0 JAR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ync-__call__ detection

Groundwork for making the agent runtime's workers survive the default
'spawn' start method (idea-5 design, Stage 1):

- FunctionRef(module, qualname, unwrap_depth): picklable reference to a
  module-level function, resolved and memoized per process in the spawn
  child. Handles decorator-rebound globals (e.g. @tool's wraps-wrapper)
  via the __wrapped__ chain, mirroring Worker's _ExecuteFunctionReference.
- SpawnSafetyError + probe_spawn_safety(): registration-time pickle probe
  that names the offending worker with actionable remedies instead of an
  opaque PicklingError at TaskHandler.start_processes. Probe groups ship
  disabled; enabled per worker family as each is converted.
- task_handler: async detection now sees callable instances with async
  __call__ (spawn-safe entries are instances, not functions), at both the
  initial process-build and restart paths.

Tests: FunctionRef accept/reject matrix, per-process memoization, a real
cross-process spawn round-trip, probe gating, async detection.
tests/unit: 2509 passed (2489 baseline + 20 new), 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kowser and others added 6 commits July 8, 2026 17:08
…worker closure

Group A of the idea-5 spawn-safety design. Previously make_tool_worker
returned a nested tool_worker closure with a spoofed identity
(__name__/__qualname__ reassigned, __module__ left as _dispatch), which
could not be pickled at Process.start() under the default 'spawn' start
method — the direct cause of the agent-e2e pickle failures
("attribute lookup <tool> on ..._dispatch failed").

- make_tool_worker now returns a picklable ToolWorkerEntry (module-level
  class instance): tool function travels as FunctionRef (module-level fns,
  incl. the @tool wrapper's __wrapped__ hop) or by value (picklable
  callable instances); guardrails, credential names and the framework-
  callable marker are carried as instance state because parent-populated
  _dispatch registries are empty in spawn children.
- The tool execution body moves to module-level run_tool_task(); the
  identity spoof is deleted. Credential-name priority is resolved at
  registration; the workflow-level fallback stays a runtime lookup.
- Skill workers: the make_script_func / make_read_func closures become
  picklable ScriptRunner / SkillFileReader classes (plain-data attrs).
- All three registration paths (native @tool via ToolRegistry, skill
  workers, framework-extracted) now run probe_spawn_safety, failing fast
  at registration with a named offender instead of an opaque
  PicklingError at worker start. Probe group "tools" enabled.
- test fixture: the langchain integration fixture's tool impl moves to
  module level (extracted <locals> functions are legitimately rejected
  by the probe under spawn).

New test proves a full ToolWorkerEntry pickles, crosses a REAL spawn
boundary, and executes a Task with zero parent registry state.
tests/unit: 2510 passed, 1 skipped (default and explicit
CONDUCTOR_MP_START_METHOD=spawn).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…olEntry

The '_make_code_execution_tool.<locals>.execute_code' closure was the
single largest source of agent-e2e pickle failures under 'spawn' (14 of
20: suites 10/14/15 code-exec, stateful and skills paths all route
through it). Both factories now build picklable module-level entries:

- CodeExecutionEntry (code_execution_config): carries the executor by
  value plus allowed_languages/allowed_commands/timeout as plain data;
  the CommandValidator is rebuilt per call. Local/Docker/Serverless
  executors hold no live resources and pickle cleanly; Jupyter pickles
  pre-kernel only (documented).
- ExecutorToolEntry (code_executor.as_tool()): same pattern for the
  public as_tool() API (examples 24, kitchen_sink).
- schema_from_function and the dispatch annotation resolver learn a
  type(func).__call__ fallback so callable-instance tools keep real type
  hints under `from __future__ import annotations` modules.

New tests: CodeExecutionEntry executes real code across a spawn
boundary; as_tool() entries pickle. tests/unit: 2512 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…time closures

Group B of the idea-5 spawn-safety design. Every AgentRuntime._register_*
control worker was an async-def closure ('<locals>' qualname — unpicklable
under 'spawn'): guardrails (combined + single), stop_when, gate, callback,
termination, check_transfer, transfer (hybrid + swarm + unreachable),
router, handoff_check, process_selection. Each becomes a picklable
module-level entry class with async __call__ and plain-data attrs:

- User callables (guardrail funcs, stop_when/gate/router fns, legacy
  callbacks) travel as FunctionRef when module-level, raw otherwise —
  raw only survives fork, and every registration site now runs
  probe_spawn_safety (group "system"), so lambdas fail fast at
  registration with the offender named (design §7 policy).
- CallbackEntry carries handlers (by value) + legacy fn (by ref) and
  rebuilds the _chain_callbacks_for_position chain per call — the chain
  closure itself was unpicklable. _register_callback_worker's signature
  changes accordingly (single caller updated).
- HandoffCheckEntry keeps blocked_counts as instance state (per worker
  process — the same semantics as the closure cell it replaces);
  HandoffConditions travel by value.
- TerminationEntry carries the condition object by value; transfer
  workers become stateless TransferNoopEntry/TransferUnreachableEntry.

Tests: async entry across a real spawn boundary, handoff decision +
pickle round-trips, callback chain rebuild, fail-fast policy (lambda →
SpawnSafetyError at registration). tests/unit: 2518 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Remove `CONDUCTOR_MP_START_METHOD` env var

# Conflicts:
#	METRICS.md
Three findings from running the 20 previously pickle-failing e2e tests
against a live server under default spawn:

- RegexGuardrail / LLMGuardrail built their check functions as __init__
  closures, making every regex/LLM guardrail spawn-unsafe by
  construction (SpawnSafetyError: "Can't pickle local object
  RegexGuardrail.__init__.<locals>._check"). Both now bind methods —
  bound methods pickle with their instance, and all attrs are plain
  data / re.Pattern. Verified: suite8 tool_output_regex_retry green
  under spawn against a live server.
- _worker_entries.py used `from __future__ import annotations`, so entry
  __call__ annotations were strings at runtime. AsyncTaskRunner reads
  parameter types from inspect.signature(execute_function) and feeds
  them to isinstance-based input conversion — strings broke every async
  entry ("isinstance() arg 2 must be a type"). Future-import removed
  (with a warning note) and StopWhenEntry's params annotated with real
  types, matching the explicit __annotations__ dict the old closure
  carried. Verified: suite13 all_callbacks + suite14 stateful stop_when
  green under spawn against a live server.
- e2e suite14 defined its stop_when predicate inside the test method —
  moved to module level per the spawn worker contract (nested defs
  cannot cross the process boundary; the probe rejects them by design).

Also: always-spawn alignment — removed the fork-premised probe test and
fork wording in transport docstrings (CONDUCTOR_MP_START_METHOD was
removed repo-wide in 9a989af; direct transport exists for picklable
instances/bound methods, not as a fork fallback).

tests/unit: 2518 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erEntry

Group C (final) of the idea-5 spawn-safety design — the 9 framework
worker closures (6 langgraph graph-structure workers + 3 passthroughs).

- GraphWorkerEntry: picklable wrapper carrying (factory name, FunctionRef
  to the user node/router function, plain-data args); the langgraph
  make_* factory runs once per worker process inside the entry — its
  nested worker never crosses a process boundary. The LLM/subgraph
  objects were never captured anyway (only their variable NAMES; the
  spawn child rebuilds them by re-importing the node function's module).
- PassthroughWorkerEntry: same deferred-factory pattern for the claude /
  langchain / langgraph passthroughs. Claude options travel as a plain-
  config dict (ClaudeCodeOptions is never picklable as-is —
  debug_stderr defaults to sys.stderr) via new
  claude_options_to_plain_config / make_claude_agent_sdk_worker_from_config;
  hooks / can_use_tool callables and in-process MCP instances raise
  SpawnSafetyError naming the field. Langchain executors / compiled
  langgraph graphs hold live clients and locks — the registration probe
  (group "framework", now enabled) fails fast with an actionable
  message instead of an opaque PicklingError.
- Passthrough registration tests updated to the deferred-factory
  contract (invoke the entry, assert the factory call); the claude test
  uses a real ClaudeCodeOptions instead of a Mock.

Group D note: check_approval_worker and the _tool_registry dispatch
write are retained (test consumers + upstream-parity tracking), not
deleted.

Tests: GraphWorkerEntry across a real spawn boundary; passthrough
payload pickling + fail-fast; claude config extraction/rejection/
round-trip. tests/unit: 2523 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kowser-orkes kowser-orkes force-pushed the feature/combine-agentspan branch from 1aa9995 to 770973e Compare July 9, 2026 00:10
…nsport

The agent-e2e run on 770973e (2 failed / 130 passed) surfaced two
transport gaps, both hit by idiomatic user code:

- suite11 'multiply': langchain's @tool rebinds the module global to a
  StructuredTool holding the original in .func (async: .coroutine) with
  no __wrapped__ chain, so FunctionRef.of gave up and the raw function
  fell to fn_direct — whose reference pickling then found the container
  at the global name ("it's not the same object as …"). FunctionRef now
  records an attr_hop ("func"/"coroutine") taken before the __wrapped__
  walk, so container-held functions travel by reference.

- suite8 'safe_query': ToolWorkerEntry carries tool-attached Guardrail
  objects raw, and Guardrail.func is the function *extracted* from the
  @guardrail wraps-wrapper — pickling it by reference found the wrapper
  instead. Guardrail now travels via __getstate__/__setstate__ routing
  func through wrap_callable/unwrap_callable (agent-level guardrails
  already did this inside GuardrailEntry). Bound-method guardrails
  (RegexGuardrail/LLMGuardrail) and external refs pass through unchanged.

Tests: container-hop matrix (sync/async, real langchain via importorskip,
spawn-child round trip) + Guardrail transport (decorated, tool-attached,
regex regression, external). tests/unit: 2536 passed, 1 skipped. Both
previously-failing e2e tests plus their full suites (19 tests) pass
against a live v0.4.0 server under default spawn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kowser and others added 7 commits July 8, 2026 22:05
…llback, reason param, typed get_schedule

Per-schedule pause/resume verbs are a server-family split: OSS Conductor maps
them PUT-only (conductor-oss#1064), Orkes Conductor GET-only. The generated
client sent GET (Orkes dialect), which fails on OSS/embedded servers. Now:

- scheduler_resource_api.py (HAND-FIX, regeneration-guarded): pause/resume send
  PUT; pause accepts an optional `reason` query param; get_schedule deserializes
  to WorkflowSchedule instead of leaking raw camelCase dicts.
- OrkesSchedulerClient: falls back to the legacy GET dialect on 405 and caches
  the discovered verb per instance (one wasted round-trip max on Orkes servers);
  404 never triggers fallback. get_schedule returns None for Conductor's
  200-with-empty-body missing-schedule responses, honoring the ABC's declared
  Optional[WorkflowSchedule] contract. pause_schedule gains reason= (additive).
- SchedulerClient ABC: fix the broken tuple annotation on get_schedule; add
  reason= to pause_schedule.
- New tests/unit/orkes/test_scheduler_resource_contract.py (13 tests): pins
  paths/verbs/query-params/response_type against spec regeneration, the None
  normalization, reason forwarding, and the 405 fallback + verb cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ayer canonical in schedule.py

SchedulerClient's native get_schedule/save_schedule/get_all_schedules are the
source of truth for reads/writes/lists — deliberately no domain twins. The six
lifecycle operations that add real value beyond the raw endpoints move onto the
ABC as concrete methods implemented over the abstract endpoint methods, so any
implementation gets them for free:

  pause(reason=)/resume/delete/preview_next  — typed-error translation
  run_now(info)                              — via a _start_workflow() hook
                                               (OrkesSchedulerClient implements
                                               it over WorkflowResourceApi)
  reconcile(agent, desired)                  — tri-state declarative diff with
                                               {agent}-{short} wire prefixing

The Schedule/ScheduleInfo mapping helpers (payload mapping, dual-shape reads,
typed-error translation) move from schedule_client.py into
conductor/client/ai/schedule.py next to the models they map, plus new _get_info/
_list_infos read helpers for the module-level schedules.* API. schedule_client.py
re-imports them so the AgentScheduleClient facade and the
conductor.ai.agents.schedule.client re-export chain are byte-compatible.

conductor.client.ai imports inside scheduler_client.py are deliberately lazy —
the module is on every SDK program's import path and must not pull the agent
surface at import time (pinned by a subprocess guard test).

New tests/unit/orkes/test_scheduler_domain_surface.py (17 tests): lifecycle
delegation, typed errors, reconcile tri-state, _get_info mapping, run_now
NotImplementedError default, ai-layer import isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…leClient becomes an inert deprecated shim

The scoped breaking change (user-accepted): schedule client objects no longer
expose the mapped get/save/list_for_agent wrappers — the native methods are the
one API for reads/writes/lists:

  get(wire) -> ScheduleInfo        =>  get_schedule(wire) -> WorkflowSchedule|None
  save(schedule, agent)            =>  save_schedule(SaveScheduleRequest)
  list_for_agent(agent)            =>  get_all_schedules(workflow_name=agent)

Everything else is byte-frozen: the six lifecycle methods, the module-level
schedules.* API (all eight functions keep their signatures and ScheduleInfo
returns — internals rewired onto _get_info/_list_infos over the raw methods),
symbols/constructors/accessors, and all examples.

- AgentScheduleClient rewritten as a pure delegation shim: subclasses
  SchedulerClient, 14 endpoint delegations to the wrapped client,
  _start_workflow via the wrapped workflow client, lifecycle methods inherited,
  zero logic of its own. The raw requests.put pause/resume workaround dies here
  (S1's fixed transport + 405 fallback covers both server dialects). DEPRECATED;
  removal planned for the next major release (get_agent_schedule_client too).
- tests/unit/ai/test_schedule.py: byte-identical except the run_now-wait
  fixture's read stub (get -> get_schedule returning a typed WorkflowSchedule) —
  direct collateral of the accepted break; all 32 tests pass, now exercising the
  shipped SchedulerClient code paths through the shim.
- e2e suite21 (13 call sites) and suite24 (3 call sites) migrate to the
  source-of-truth methods with WorkflowSchedule-field assertions; fixtures and
  constructions unchanged.
- New guard: no schedule client may grow get/save/list_for_agent back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ations, scoped break

- CHANGELOG: Added (lifecycle on SchedulerClient, reason=), Changed (scoped
  breaking get/save/list_for_agent removal with one-line migrations; typed
  get_schedule), Deprecated (AgentScheduleClient/ScheduleClient/
  get_agent_schedule_client), Fixed (PUT-first with 405->GET fallback across
  server families).
- docs/SCHEDULE.md: verb-compatibility note (OSS=PUT, Orkes=GET, transparent
  fallback), pause reason= example (OSS-only storage), new Schedule Lifecycle
  Helpers section (the six typed-error operations; reads/writes/lists stay on
  the native methods).
- docs/agents/{advanced,api-reference,writing-agents}.md: schedule surface
  documented on SchedulerClient; samples migrated off the removed wrappers;
  deprecation guidance for the shim symbols.
- AGENTS.md §1.2: HAND-FIX policy — never regenerate scheduler_resource_api.py
  without re-applying the marked fixes; the contract guard tests fail loudly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User decision supersedes the deprecated-shim compromise: SchedulerClient is
the ONE schedule client. Removed schedule_client.py, the
conductor.ai.agents.schedule.client re-export shim, the ScheduleClient alias,
and OrkesClients.get_agent_schedule_client(). Runtime and control-plane
accessors now hand out get_scheduler_client() (same shared instance).
Tests migrate to a SchedulerClient double; a removal guard keeps the
symbols from creeping back.

Gates: unit 2568 passed / 1 skipped; suite21 11/11 live vs :8085 on a bare
OrkesSchedulerClient fixture; suite24 5/5 (1 flaky rerun).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CHANGELOG Deprecated → Removed (breaking, with migration); drop the
deprecated-shim paragraphs from the agents docs; AgentClient.schedules and
runtime.schedules_client() documented as returning SchedulerClient.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kowser-orkes kowser-orkes force-pushed the feature/combine-agentspan branch from b75172e to cdbd76b Compare July 9, 2026 07:07
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.

2 participants