Skip to content

Commit 2735470

Browse files
Kowserclaude
andcommitted
S3: SchedulerClient is the get/save/list source of truth; AgentScheduleClient 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>
1 parent 83dae07 commit 2735470

7 files changed

Lines changed: 168 additions & 176 deletions

File tree

e2e/test_suite21_scheduling.py

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@
3131
from conductor.ai.agents.schedule import (
3232
Schedule,
3333
ScheduleNameConflict,
34-
ScheduleNotFound,
3534
)
3635
from conductor.ai.agents.schedule.client import ScheduleClient
36+
from conductor.client.ai.schedule import _from_workflow_schedule
3737

3838
pytestmark = [pytest.mark.e2e]
3939

@@ -152,13 +152,13 @@ def test_creates_schedules(self, schedule_client, agent_name):
152152
Schedule(name="weekly", cron="0 0 9 * * MON"),
153153
],
154154
)
155-
infos = schedule_client.list_for_agent(agent_name)
156-
by_short = {i.short_name: i for i in infos}
157-
assert set(by_short) == {"daily", "weekly"}
158-
assert by_short["daily"].name == f"{agent_name}-daily"
159-
assert by_short["daily"].cron == "0 0 9 * * ?"
160-
assert by_short["daily"].input == {"k": 1}
161-
assert by_short["daily"].agent == agent_name
155+
scheds = schedule_client.get_all_schedules(workflow_name=agent_name)
156+
by_wire = {s.name: s for s in scheds}
157+
assert set(by_wire) == {f"{agent_name}-daily", f"{agent_name}-weekly"}
158+
daily = by_wire[f"{agent_name}-daily"]
159+
assert daily.cron_expression == "0 0 9 * * ?"
160+
assert daily.start_workflow_request.input == {"k": 1}
161+
assert daily.start_workflow_request.name == agent_name
162162

163163
def test_upsert_and_prune(self, schedule_client, agent_name):
164164
schedule_client.reconcile(
@@ -176,21 +176,21 @@ def test_upsert_and_prune(self, schedule_client, agent_name):
176176
Schedule(name="c", cron="0 0 17 * * ?"),
177177
],
178178
)
179-
infos = {i.short_name: i for i in schedule_client.list_for_agent(agent_name)}
180-
assert set(infos) == {"a", "c"}
181-
assert infos["a"].cron == "0 0 9 * * ?"
179+
scheds = {s.name: s for s in schedule_client.get_all_schedules(workflow_name=agent_name)}
180+
assert set(scheds) == {f"{agent_name}-a", f"{agent_name}-c"}
181+
assert scheds[f"{agent_name}-a"].cron_expression == "0 0 9 * * ?"
182182

183183
def test_empty_list_purges(self, schedule_client, agent_name):
184184
schedule_client.reconcile(agent_name, [Schedule(name="x", cron="0 * * * * ?")])
185-
assert len(schedule_client.list_for_agent(agent_name)) == 1
185+
assert len(schedule_client.get_all_schedules(workflow_name=agent_name)) == 1
186186
schedule_client.reconcile(agent_name, [])
187-
assert schedule_client.list_for_agent(agent_name) == []
187+
assert not schedule_client.get_all_schedules(workflow_name=agent_name)
188188

189189
def test_none_preserves(self, schedule_client, agent_name):
190190
schedule_client.reconcile(agent_name, [Schedule(name="x", cron="0 * * * * ?")])
191191
schedule_client.reconcile(agent_name, None)
192-
infos = schedule_client.list_for_agent(agent_name)
193-
assert [i.short_name for i in infos] == ["x"]
192+
scheds = schedule_client.get_all_schedules(workflow_name=agent_name)
193+
assert [s.name for s in scheds] == [f"{agent_name}-x"]
194194

195195
def test_duplicate_name_raises_before_io(self, schedule_client, agent_name):
196196
with pytest.raises(ScheduleNameConflict):
@@ -202,45 +202,44 @@ def test_duplicate_name_raises_before_io(self, schedule_client, agent_name):
202202
],
203203
)
204204
# And nothing landed on the server.
205-
assert schedule_client.list_for_agent(agent_name) == []
205+
assert not schedule_client.get_all_schedules(workflow_name=agent_name)
206206

207207

208208
class TestPauseResume:
209209
def test_pause_then_resume(self, schedule_client, agent_name):
210210
schedule_client.reconcile(agent_name, [Schedule(name="p", cron="0 0 9 * * ?")])
211211
wire = f"{agent_name}-p"
212212

213-
info = schedule_client.get(wire)
214-
assert info.paused is False
213+
ws = schedule_client.get_schedule(wire)
214+
assert not ws.paused
215215

216216
schedule_client.pause(wire)
217-
assert schedule_client.get(wire).paused is True
217+
assert schedule_client.get_schedule(wire).paused is True
218218

219219
schedule_client.resume(wire)
220-
assert schedule_client.get(wire).paused is False
220+
assert not schedule_client.get_schedule(wire).paused
221221

222222
def test_paused_on_create_preserves_state(self, schedule_client, agent_name):
223223
"""Spec §10 Q3: paused-on-create still records the schedule cleanly."""
224224
schedule_client.reconcile(
225225
agent_name, [Schedule(name="silent", cron="0 0 9 * * ?", paused=True)]
226226
)
227-
info = schedule_client.get(f"{agent_name}-silent")
228-
assert info.paused is True
227+
ws = schedule_client.get_schedule(f"{agent_name}-silent")
228+
assert ws.paused is True
229229

230230

231231
class TestDelete:
232232
def test_delete_removes(self, schedule_client, agent_name):
233233
schedule_client.reconcile(agent_name, [Schedule(name="d", cron="0 * * * * ?")])
234234
wire = f"{agent_name}-d"
235235
schedule_client.delete(wire)
236-
assert schedule_client.list_for_agent(agent_name) == []
236+
assert not schedule_client.get_all_schedules(workflow_name=agent_name)
237237

238-
def test_get_after_delete_raises(self, schedule_client, agent_name):
238+
def test_get_after_delete_returns_none(self, schedule_client, agent_name):
239239
schedule_client.reconcile(agent_name, [Schedule(name="g", cron="0 * * * * ?")])
240240
wire = f"{agent_name}-g"
241241
schedule_client.delete(wire)
242-
with pytest.raises(ScheduleNotFound):
243-
schedule_client.get(wire)
242+
assert schedule_client.get_schedule(wire) is None
244243

245244

246245
class TestPreviewNext:
@@ -257,7 +256,7 @@ def test_returns_execution_id_immediately(self, schedule_client, agent_name):
257256
schedule_client.reconcile(
258257
agent_name, [Schedule(name="r", cron="0 0 9 * * ?", input={"trigger": "manual"})]
259258
)
260-
info = schedule_client.get(f"{agent_name}-r")
259+
info = _from_workflow_schedule(schedule_client.get_schedule(f"{agent_name}-r"))
261260

262261
t0 = time.monotonic()
263262
execution_id = schedule_client.run_now(info)

e2e/test_suite24_agent_client.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,21 +129,20 @@ def test_schedule_then_list_then_purge(self, runtime, noop_agent_name):
129129

130130
# Clean slate.
131131
schedules.reconcile(noop_agent_name, [])
132-
assert schedules.list_for_agent(noop_agent_name) == []
132+
assert not schedules.get_all_schedules(workflow_name=noop_agent_name)
133133

134134
# Reconcile a single schedule via the client's schedule surface.
135135
schedules.reconcile(
136136
noop_agent_name,
137137
[Schedule(name="daily", cron="0 0 9 * * ?", input={"k": 1})],
138138
)
139-
infos = {i.short_name: i for i in schedules.list_for_agent(noop_agent_name)}
140-
assert set(infos) == {"daily"}
141-
assert infos["daily"].name == f"{noop_agent_name}-daily"
142-
assert infos["daily"].cron == "0 0 9 * * ?"
139+
by_wire = {s.name: s for s in schedules.get_all_schedules(workflow_name=noop_agent_name)}
140+
assert set(by_wire) == {f"{noop_agent_name}-daily"}
141+
assert by_wire[f"{noop_agent_name}-daily"].cron_expression == "0 0 9 * * ?"
143142

144143
# Counterfactual: reconcile with an empty list purges it.
145144
schedules.reconcile(noop_agent_name, [])
146-
assert schedules.list_for_agent(noop_agent_name) == []
145+
assert not schedules.get_all_schedules(workflow_name=noop_agent_name)
147146

148147

149148
# ── structural consistency: runtime + client share one schedule surface ──

src/conductor/ai/agents/schedule/api.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
from typing import Any, List, Optional
1919

2020
from conductor.ai.agents.schedule.schedule import Schedule, ScheduleInfo
21+
from conductor.client.ai.schedule import (
22+
_get_info,
23+
_list_infos,
24+
_to_save_request,
25+
_translate,
26+
)
2127

2228

2329
def _client(runtime: Optional[Any]) -> Any:
@@ -32,7 +38,7 @@ def list( # noqa: A001 — module-level API mirrors the spec
3238
agent: str, *, runtime: Optional[Any] = None
3339
) -> List[ScheduleInfo]:
3440
"""List all schedules attached to ``agent`` (workflow name)."""
35-
return _client(runtime).list_for_agent(agent)
41+
return _list_infos(_client(runtime), agent)
3642

3743

3844
def get(name: str, *, runtime: Optional[Any] = None) -> ScheduleInfo:
@@ -41,7 +47,7 @@ def get(name: str, *, runtime: Optional[Any] = None) -> ScheduleInfo:
4147
The agent is recovered from the schedule's ``startWorkflowRequest.name``;
4248
``short_name`` on the returned :class:`ScheduleInfo` is the user's original.
4349
"""
44-
return _client(runtime).get(name)
50+
return _get_info(_client(runtime), name)
4551

4652

4753
def pause(name: str, reason: Optional[str] = None, *, runtime: Optional[Any] = None) -> None:
@@ -73,7 +79,7 @@ def run_now(
7379
seconds) — consistent with ``run()``'s completed-workflow result.
7480
"""
7581
client = _client(runtime)
76-
info = client.get(name)
82+
info = _get_info(client, name)
7783
execution_id = client.run_now(info)
7884
if not wait:
7985
return execution_id
@@ -119,4 +125,8 @@ def save(schedule: Schedule, agent: str, *, runtime: Optional[Any] = None) -> No
119125
Useful for ad-hoc creation from the UI / scripts. Most users should
120126
prefer the declarative ``deploy(agent, schedules=[...])`` flow.
121127
"""
122-
_client(runtime).save(schedule, agent)
128+
client = _client(runtime)
129+
try:
130+
client.save_schedule(_to_save_request(schedule, agent))
131+
except Exception as exc: # noqa: BLE001
132+
raise _translate(exc) from exc

0 commit comments

Comments
 (0)