Skip to content

Commit 3c0e05d

Browse files
committed
feat: consolidate agent control-plane onto AgentClient; fix worker credential delivery
Refactors the agent runtime's server access onto the standard client stack and fixes worker credential resolution to the server's runtimeMetadata contract. AgentClient (interface + Orkes impl) - Add `conductor.client.agent_client.AgentClient` (ABC) + `OrkesAgentClient` following the WorkflowClient/OrkesWorkflowClient pattern, built on the shared ApiClient (sync) / AsyncApiClient (async) so JWT mint / X-Authorization / TTL refresh / 401-retry are reused, not re-implemented. SSE reuses the ApiClient token via a new public `get_authentication_headers()`. - `OrkesClients.get_agent_client()` returns the new client. - Delete the duplicate transport (`client/ai/agent_api_client.py`) and the DX wrapper (`ai/agents/runtime/http_client.py`); AgentRuntime uses the one client for all /agent/* calls (start/deploy/compile/status/execution/respond/stop/ signal/SSE), sync and async. AgentRuntime / Configuration - `AgentRuntime(configuration: Configuration = None, *, settings: AgentConfig = None)`: server config comes solely from the standard Configuration; AgentConfig carries runtime behaviour only. Remove the custom `to_conductor_configuration()` bridge and the private log-level poke. - `Configuration` host resolution falls back CONDUCTOR_SERVER_URL -> AGENTSPAN_SERVER_URL. Worker credentials via runtimeMetadata (conductor-oss PR #1255 contract) - Add `Task.runtime_metadata` and `TaskDef.runtime_metadata` model fields. - Stamp `TaskDef.runtimeMetadata` from a tool/agent's declared credentials at registration so the SDK's overwrite-registration no longer wipes the value the server compiles. - Worker dispatch (+ langchain/langgraph/claude_agent_sdk wrappers) reads the host-resolved secrets off `Task.runtimeMetadata` instead of the retired execution-token / POST /workers/secrets path. Retire WorkerCredentialFetcher, `_extract_execution_token`, `_get_credential_fetcher`; `resolve_credentials(task, names)` now reads runtimeMetadata. Remove server auto-start - Delete `runtime/server.py` and the `auto_start_server` / AGENTSPAN_AUTO_START_SERVER config surface; runtime no longer probes or launches a local server. CLI tools spawn-safety - `cli_config._make_cli_tool` builds the `run_command` worker from a module-level `_CliCommandRunner` instance (not a <locals> closure), so registration's spawn-safety probe passes. Tests / docs / examples - New: orkes_agent_client, task/taskdef runtimeMetadata serde, runtime_metadata registration, e2e suite26 (worker credentials + CLI spawn-safety) unit/e2e tests. - Update credential/dispatch/runtime/sse/signals tests to the new contracts; rewrite e2e suite24 for the transport-only client + runtime DX. - Docs + examples updated (GH_TOKEN, Configuration-based AgentRuntime).
1 parent 612821b commit 3c0e05d

55 files changed

Lines changed: 1874 additions & 2817 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/agents/advanced.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,34 @@
1212
## Runtime init and config
1313

1414
`AgentRuntime` is the entry point. Use it as a context manager so workers shut down
15-
cleanly. Config comes from `AgentConfig.from_env()` by default, or pass overrides.
15+
cleanly. Server connection comes from the standard Conductor `Configuration`
16+
the same object every other client uses — which resolves `CONDUCTOR_SERVER_URL`
17+
(falling back to `AGENTSPAN_SERVER_URL`) and `CONDUCTOR_AUTH_KEY`/`CONDUCTOR_AUTH_SECRET`
18+
from the environment when not passed explicitly.
1619

1720
```python
1821
from conductor.ai.agents import AgentRuntime, AgentConfig
22+
from conductor.client.configuration.configuration import Configuration
1923

20-
# From env (AGENTSPAN_SERVER_URL etc.)
24+
# From env (CONDUCTOR_SERVER_URL → AGENTSPAN_SERVER_URL, CONDUCTOR_AUTH_*)
2125
with AgentRuntime() as runtime:
2226
runtime.run(agent, "hi")
2327

24-
# Explicit kwargs
25-
with AgentRuntime(server_url="https://prod:8080/api",
26-
api_key="...") as runtime:
28+
# Explicit Configuration
29+
with AgentRuntime(Configuration(server_api_url="https://prod:8080/api")) as runtime:
2730
...
2831

29-
# Or an AgentConfig
30-
config = AgentConfig.from_env()
31-
config.auto_start_server = False
32-
with AgentRuntime(config=config) as runtime:
32+
# Runtime behaviour knobs (workers, streaming, log level) via AgentConfig
33+
settings = AgentConfig.from_env()
34+
settings.log_level = "DEBUG"
35+
with AgentRuntime(settings=settings) as runtime:
3336
...
3437
```
3538

3639
`AgentConfig` is a dataclass; `from_env()` reads the `AGENTSPAN_*` environment
3740
variables (full list in [Getting started](getting-started.md#environment-variables)).
38-
The Conductor `Configuration` object underneath is built from `server_url` and the
39-
auth fields (`api_key`, or `auth_key`/`auth_secret`).
41+
It carries runtime *behaviour* settings only — server connection always comes from
42+
the `Configuration`.
4043

4144
### Module-level convenience functions
4245

@@ -45,7 +48,7 @@ For one-off scripts, top-level functions use a shared singleton runtime:
4548
```python
4649
import conductor.ai.agents as ag
4750

48-
ag.configure(server_url="https://prod:8080/api", auto_start_server=False) # before first run
51+
ag.configure(server_url="https://prod:8080/api") # before first run
4952
result = ag.run(agent, "Hello!")
5053
ag.shutdown() # explicit cleanup; not required for simple scripts
5154
```

docs/agents/api-reference.md

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ agents](framework-agents.md), and [Advanced](advanced.md).
1919

2020
## AgentRuntime
2121

22-
`AgentRuntime(*, server_url=None, api_key=None, api_secret=None, config=None)`
22+
`AgentRuntime(configuration=None, *, settings=None)``configuration` is the
23+
standard Conductor `Configuration` (host + auth; defaults to `Configuration()`,
24+
which resolves `CONDUCTOR_SERVER_URL``AGENTSPAN_SERVER_URL` and
25+
`CONDUCTOR_AUTH_*` from the environment); `settings` is an optional `AgentConfig`
26+
with runtime behaviour knobs (its connection fields are ignored).
2327

2428
Context manager (sync and async: `with` / `async with`).
2529

@@ -269,43 +273,46 @@ override. Pass instances via `Agent(callbacks=[...])`; they chain in list order.
269273

270274
## AgentClient
271275

272-
The control-plane client (formerly `AgentHttpClient`, alias kept). Reach it via
273-
`runtime.client`, or construct standalone:
274-
`AgentClient(server_url="", api_key="", auth_key="", auth_secret="", *, runtime=None)`.
276+
The `/agent/*` control-plane client. `AgentClient` is an interface
277+
(`conductor.client.agent_client`) implemented by `OrkesAgentClient`
278+
(`conductor.client.orkes.orkes_agent_client`), following the same pattern as
279+
`WorkflowClient`/`OrkesWorkflowClient` and built on the shared `ApiClient`
280+
token machinery. Reach it via `runtime.client` or
281+
`OrkesClients(configuration).get_agent_client()`. Every method has an `*_async`
282+
counterpart.
275283

276284
| Method | Signature | Purpose |
277285
|---|---|---|
278-
| `run` / `run_async` | `(agent, prompt=None, *, media=None, session_id=None, idempotency_key=None, timeout=None, context=None, static_plan=None) -> AgentResult` | Compile + start + poll (no local workers) |
279-
| `start` / `start_async` | same args | `-> AgentHandle` |
280-
| `deploy` / `deploy_async` | `(*agents) -> list[DeploymentInfo]` | Compile + register |
281-
| `schedule` | `(agent, schedules) -> DeploymentInfo` | Deploy + reconcile cron schedules |
282-
| `get_status` | `(execution_id) -> dict` | |
283-
| `respond` | `(execution_id, body) -> None` | |
284-
| `stop` | `(execution_id) -> None` | |
285-
| `signal` | `(execution_id, message) -> None` | |
286-
| `stream_sse` | `(execution_id) -> AsyncIterator[dict]` | |
287-
| `schedules` (property) | `-> SchedulerClient` | |
288-
| `close` | `() -> None` (async) | |
289-
290-
Lower-level endpoint methods (`start_agent`, `deploy_agent`, `compile_agent`) are also
291-
available. The raw transport behind them is `conductor.client.ai.AgentApiClient`
292-
(build one with `OrkesClients.get_agent_client()`); `AgentClient` composes it and
293-
keeps the agent-level conveniences.
286+
| `start_agent` | `(payload) -> dict` | POST /agent/start |
287+
| `deploy_agent` | `(payload) -> dict` | POST /agent/deploy |
288+
| `compile_agent` | `(payload) -> dict` | POST /agent/compile |
289+
| `get_status` | `(execution_id) -> dict` | GET /agent/{id}/status |
290+
| `get_execution` | `(execution_id) -> dict` | GET /agent/execution/{id} |
291+
| `list_executions` | `(params=None) -> dict` | GET /agent/executions |
292+
| `respond` | `(execution_id, body) -> None` | POST /agent/{id}/respond |
293+
| `stop` | `(execution_id) -> None` | POST /agent/{id}/stop |
294+
| `signal` | `(execution_id, message) -> None` | POST /agent/{id}/signal |
295+
| `stream_sse` | `(execution_id, last_event_id=None) -> Iterator[dict]` | GET /agent/stream/{id} (SSE) |
296+
| `schedules` (property) | `-> SchedulerClient` | Cron schedule lifecycle |
297+
| `close` / `close_async` | `() -> None` | Release transport resources |
294298

295299
## Config and credentials
296300

297301
`AgentConfig` (dataclass) fields: `server_url="http://localhost:8080/api"`,
298302
`api_key=None`, `auth_key=None`, `auth_secret=None`, `llm_retry_count=3`,
299303
`worker_poll_interval_ms=100`, `worker_thread_count=1`, `auto_start_workers=True`,
300-
`auto_start_server=True`, `daemon_workers=True`, `auto_register_integrations=False`,
304+
`daemon_workers=True`, `auto_register_integrations=False`,
301305
`streaming_enabled=True`, `secret_strict_mode=False`, `log_level="INFO"`. Classmethod
302306
`AgentConfig.from_env()` reads the `AGENTSPAN_*` variables (see [Getting
303307
started](getting-started.md#environment-variables)). Property `api_secret` aliases
304-
`auth_secret`.
308+
`auth_secret`. `AgentRuntime` uses it only for runtime *behaviour* knobs — server
309+
connection always comes from the Conductor `Configuration`.
305310

306311
`get_secret(name) -> str` — read a credential inside a `@tool(credentials=[...])`
307-
function. `resolve_credentials(input_data, names) -> dict` — for external workers.
308-
Errors: `CredentialNotFoundError`, `CredentialAuthError`, `CredentialRateLimitError`,
312+
function. `resolve_credentials(task, names) -> dict` — for external workers; reads
313+
the host-resolved values the server delivers on `Task.runtimeMetadata` (declared via
314+
the tool/agent `credentials`, resolved at poll time). Errors:
315+
`CredentialNotFoundError`, `CredentialAuthError`, `CredentialRateLimitError`,
309316
`CredentialServiceError`.
310317

311318
`ClaudeCode(model_name="", permission_mode=PermissionMode.ACCEPT_EDITS)` with

docs/agents/getting-started.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ the output if you prefer.
6060
| `AGENTSPAN_WORKER_POLL_INTERVAL` | `100` | Worker poll interval (ms) |
6161
| `AGENTSPAN_WORKER_THREADS` | `1` | Worker thread count |
6262
| `AGENTSPAN_AUTO_START_WORKERS` | `true` | Auto-start local tool workers |
63-
| `AGENTSPAN_AUTO_START_SERVER` | `true` | Auto-start a local server if none is reachable |
6463
| `AGENTSPAN_DAEMON_WORKERS` | `true` | Run workers as daemon threads |
6564
| `AGENTSPAN_INTEGRATIONS_AUTO_REGISTER` | `false` | Auto-register provider integrations |
6665
| `AGENTSPAN_STREAMING_ENABLED` | `true` | Enable SSE streaming |

e2e/conftest.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@
1515
MODEL = os.environ.get("AGENTSPAN_LLM_MODEL", "openai/gpt-4o-mini")
1616

1717

18-
# ── Prevent runtime from auto-starting a second server ──────────────────
19-
20-
os.environ["AGENTSPAN_AUTO_START_SERVER"] = "false"
21-
22-
2318
# ── Markers ─────────────────────────────────────────────────────────────
2419

2520

e2e/test_suite24_agent_client.py

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
"""Suite 24: AgentClient — control-plane run + schedule surface.
1+
"""Suite 24: AgentClient (transport) + runtime run/start + schedule surface.
22
3-
Verifies the control-plane :class:`AgentClient` (formerly ``AgentHttpClient``)
4-
exposed via ``runtime.client``:
3+
``runtime.client`` is the transport :class:`AgentClient`
4+
(``conductor.client.orkes.orkes_agent_client.OrkesAgentClient``) — the
5+
``/agent/*`` control-plane endpoints built on the shared ``ApiClient``. The
6+
run/start DX lives on the runtime; the schedule lifecycle is reachable from both
7+
``runtime.schedules_client()`` and ``runtime.client.schedules``.
58
6-
- ``run`` on an LLM-only agent (no local tools) reaches status COMPLETED.
7-
Control-plane only: no local tool workers are registered/polled.
8-
- ``schedule(agent, [Schedule(...)])`` deploys + reconciles; the schedule then
9-
shows up in ``list_for_agent``. A counterfactual ``reconcile([])`` purges it.
10-
- The runtime's schedule surface (``runtime.schedules_client()``) and the
11-
client's (``runtime.client.schedules``) are the *same* instance.
9+
Verifies:
10+
- ``runtime.run`` on an LLM-only agent (no local tools) reaches COMPLETED.
11+
- ``runtime.start`` returns a handle that joins to COMPLETED.
12+
- ``schedules.reconcile(agent, [Schedule(...)])`` upserts and lists; a
13+
counterfactual ``reconcile([])`` purges it.
14+
- ``runtime.client`` is the runtime's own transport client, and both schedule
15+
accessors return working ``SchedulerClient``s.
1216
13-
No LLM is used for validation — assertions are on workflow status / schedule
14-
structure only (per CLAUDE.md rule 1). The scheduled "agent" target is a bare
15-
no-op Conductor workflow so no LLM is invoked for the schedule tests.
17+
The scheduled "agent" target is a bare no-op Conductor workflow so no LLM is
18+
invoked for the schedule tests.
1619
1720
Targets the live Agentspan server (``AGENTSPAN_SERVER_URL``). The schedule
1821
tests are skipped automatically if the server's Conductor lacks the scheduler
@@ -63,24 +66,22 @@ def test_run_llm_only_agent_completes(self, runtime, model):
6366
instructions="You are a calculator. Reply with only the number.",
6467
)
6568

66-
result = runtime.client.run(agent, "What is 2 + 2? Reply with only the number.")
69+
result = runtime.run(agent, "What is 2 + 2? Reply with only the number.")
6770

6871
assert result.status == Status.COMPLETED, (
6972
f"expected COMPLETED, got {result.status} (error={result.error})"
7073
)
7174
assert result.execution_id
72-
# No local tool workers were started for this control-plane run.
73-
assert runtime._workers_started is False
7475

7576
def test_start_returns_handle_then_joins(self, runtime, model):
76-
"""AgentClient.start returns a handle that joins to a COMPLETED result."""
77+
"""runtime.start returns a handle that joins to a COMPLETED result."""
7778
agent = Agent(
7879
name=f"e2e_client_start_{uuid.uuid4().hex[:8]}",
7980
model=model,
8081
instructions="Reply with the single word: ok",
8182
)
8283

83-
handle = runtime.client.start(agent, "Say ok")
84+
handle = runtime.start(agent, "Say ok")
8485
assert handle.execution_id
8586
result = handle.join(timeout=120)
8687
assert result.status == Status.COMPLETED
@@ -145,16 +146,22 @@ def test_schedule_then_list_then_purge(self, runtime, noop_agent_name):
145146
assert not schedules.get_all_schedules(workflow_name=noop_agent_name)
146147

147148

148-
# ── structural consistency: runtime + client share one schedule surface ──
149+
# ── structural consistency: transport client + schedule accessors ────────
149150

150151

151152
class TestScheduleSurfaceConsistency:
152-
def test_runtime_and_client_share_schedule_client(self, runtime):
153-
"""runtime.schedules_client() and runtime.client.schedules are identical."""
154-
from_runtime = runtime.schedules_client()
155-
from_client = runtime.client.schedules
156-
assert from_runtime is from_client
157-
158-
def test_client_is_bound_to_runtime(self, runtime):
159-
"""runtime.client is the runtime's own control-plane client (not a copy)."""
160-
assert runtime.client is runtime._http
153+
def test_both_schedule_accessors_are_scheduler_clients(self, runtime):
154+
"""Both runtime.schedules_client() and runtime.client.schedules return a
155+
working SchedulerClient (the transport client builds its own from the same
156+
Configuration; they are no longer required to be the same instance)."""
157+
from conductor.client.scheduler_client import SchedulerClient
158+
159+
assert isinstance(runtime.schedules_client(), SchedulerClient)
160+
assert isinstance(runtime.client.schedules, SchedulerClient)
161+
162+
def test_client_is_the_runtime_agent_client(self, runtime):
163+
"""runtime.client is the runtime's own transport AgentClient (not a copy)."""
164+
from conductor.client.agent_client import AgentClient
165+
166+
assert runtime.client is runtime._agent_client
167+
assert isinstance(runtime.client, AgentClient)

0 commit comments

Comments
 (0)