Feature/scheduler client dx#419
Open
kowser-orkes wants to merge 33 commits into
Open
Conversation
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>
…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>
…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>
…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>
Codecov Report✅ All modified and coverable lines are covered by tests. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.