Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 15 additions & 20 deletions examples/agents/63b_serve.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
"""Serve — keep tool workers running as a persistent service.

Demonstrates:
- runtime.serve() to register Python workers and block until interrupted
- runtime.serve() to register agents + Python workers and block until interrupted
- Serving multiple agents in a single process
- Decoupled from deploy: workers only, no workflow registration
- The deploy/serve/run separation: this process owns the workers for a
pre-deployed agent while another process runs it by name

serve() registers the Python tool functions (tools, custom guardrails,
callbacks, handoff checks) as Conductor workers and starts polling for
tasks. The workflow must already exist on the server (from a prior
deploy() or run() call, possibly in a different process).
serve() = deploy() + serve: it registers each agent on the server (compiles
the workflow and task definitions), registers the local Python tool functions
(tools, custom guardrails, callbacks, handoff checks) as Conductor workers,
and starts polling for tasks. It blocks until Ctrl+C / SIGTERM.

Start this in a long-running process (systemd, Docker, k8s pod).
Press Ctrl+C to stop.
Run this in one terminal and run the matching run-by-name client
(63c_run_by_name.py) in another. Without a process serving the workers the
run-by-name client schedules tool tasks that nothing ever polls, so it hangs.

python 63b_serve.py

Requirements:
- Conductor server running
- Agents already deployed (run 63_deploy.py first)
- CONDUCTOR_SERVER_URL=http://localhost:8080/api in .env or environment
- CONDUCTOR_AGENT_LLM_MODEL=openai/gpt-4o-mini in .env or environment
"""
Expand Down Expand Up @@ -70,14 +71,8 @@ def check_status(service: str) -> str:

if __name__ == "__main__":
with AgentRuntime() as runtime:
result = runtime.run(ops_bot, "Check the status of the API gateway.")
result.print_result()

# Production pattern:
# 1. Deploy once during CI/CD:
# runtime.deploy(doc_assistant, ops_bot)
# CLI alternative:
# runtime.deploy(agent) from a release script
#
# 2. In a separate long-lived worker process:
# runtime.serve(doc_assistant, ops_bot)
# serve() registers doc_assistant/ops_bot on the server, then serves
# their tool workers and blocks until Ctrl+C. Run this in one terminal
# so that 63c_run_by_name.py (and any other run-by-name client) finds
# a live process polling the agents' task queues.
runtime.serve(doc_assistant, ops_bot)
25 changes: 14 additions & 11 deletions examples/agents/63d_serve_from_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@
collects all module-level Agent instances. This avoids the need to
explicitly list every agent when serving a large codebase.

serve() registers each agent on the server (compile + register, like
deploy()) and then serves its Python tool workers, blocking until Ctrl+C.
Run this in one terminal and run the matching run-by-name client
(63e_run_monitoring.py) in another; without a serving process the client
schedules tool tasks that nothing ever polls, so it hangs.

python 63d_serve_from_package.py

Requirements:
- Conductor server running
- CONDUCTOR_SERVER_URL=http://localhost:8080/api in .env or environment
- A Python package with Agent instances at module level
- CONDUCTOR_AGENT_LLM_MODEL=openai/gpt-4o-mini in .env or environment
"""

from conductor.ai.agents import Agent, AgentRuntime, discover_agents, tool
Expand Down Expand Up @@ -51,14 +57,11 @@ def health_check() -> str:

if __name__ == "__main__":
with AgentRuntime() as runtime:
result = runtime.run(monitoring_agent, "Is everything healthy? Run a full check.")
result.print_result()

# Production pattern:
# 1. Deploy once during CI/CD:
# runtime.deploy(monitoring_agent, *discover_agents(["myapp.agents"]))
# CLI alternative:
# runtime.deploy(agent) from a release script
# serve() registers monitoring on the server, serves its tool workers,
# and blocks until Ctrl+C. Run this in one terminal so that
# 63e_run_monitoring.py (and any other run-by-name client) finds a live
# process polling the monitoring agent's task queue.
#
# 2. In a separate long-lived worker process:
# runtime.serve(monitoring_agent, packages=["myapp.agents"])
# Add packages=["myapp.agents"] to auto-discover and serve every
# module-level Agent in that package alongside the explicit agent.
runtime.serve(monitoring_agent)
6 changes: 3 additions & 3 deletions examples/agents/run_all_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@
# Long-running by design or interactive — would legitimately never terminate.
DAEMON_SKIP = {
"12_long_running.py": "long-running by design",
"63b_serve.py": "serve() worker daemon (blocks forever)",
"63d_serve_from_package.py": "serve() worker daemon (blocks forever)",
"63e_run_monitoring.py": "live monitoring loop",
"63b_serve.py": "serve() worker daemon (blocks forever; serves 63c's agents)",
"63d_serve_from_package.py": "serve() worker daemon (blocks forever; serves 63e's agent)",
"63e_run_monitoring.py": "run-by-name client; needs 63d_serve_from_package.py serving in another terminal",
"77_kafka_consumer_agent.py": "kafka consumer daemon (+ needs Kafka)",
"79_agent_message_bus.py": "message-bus daemon",
"80_live_dashboard.py": "interactive live dashboard",
Expand Down
135 changes: 135 additions & 0 deletions tests/unit/ai/test_example_63_serve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""Regression tests for examples 63b/63d: serve the workers that 63c/63e need.

Issue #487: ``63c_run_by_name.py`` and ``63e_run_monitoring.py`` execute a
pre-deployed agent *by name* (``runtime.run("doc_assistant", ...)`` /
``runtime.run("monitoring", ...)``). A run-by-name client never sees the
agent object, so it cannot start the agent's tool workers. Some other
process must be serving them. ``63b_serve.py`` and ``63d_serve_from_package.py``
are the examples meant to be that process, but their executable bodies called
``runtime.run(<agent object>, ...)`` instead of ``runtime.serve(...)`` — so no
shipped example ever served workers and the run-by-name clients hung forever.

These tests pin the example *executable bodies* (the ``__main__`` block) to the
serving behaviour, using only the stdlib ``ast`` module so they fail on the
original, un-fixed examples. They do not import the examples (which would
execute their module-level code and require a live server).
"""

from __future__ import annotations

import ast
from pathlib import Path

EXAMPLES = Path(__file__).resolve().parents[3] / "examples" / "agents"


def _main_block_calls(path: Path) -> list[ast.Call]:
"""Return every ``runtime.<method>(...)`` call inside the ``__main__`` block.

Only calls whose receiver is the ``AgentRuntime`` context-manager binding
(``with AgentRuntime() as runtime:``) are returned, so module-level helper
calls in the example are ignored.
"""
tree = ast.parse(path.read_text(encoding="utf-8"))

main_body: list[ast.stmt] = []
for node in tree.body:
if not isinstance(node, ast.If):
continue
test = node.test
# ``if __name__ == "__main__":``
if (
isinstance(test, ast.Compare)
and isinstance(test.left, ast.Name)
and test.left.id == "__name__"
and len(test.comparators) == 1
and isinstance(test.comparators[0], ast.Constant)
and test.comparators[0].value == "__main__"
):
main_body = node.body
break

runtime_names: set[str] = set()
for node in ast.walk(ast.Module(body=main_body, type_ignores=[])):
if isinstance(node, ast.With):
for item in node.items:
ctx = item.context_expr
if (
isinstance(ctx, ast.Call)
and isinstance(ctx.func, ast.Name)
and ctx.func.id == "AgentRuntime"
and isinstance(item.optional_vars, ast.Name)
):
runtime_names.add(item.optional_vars.id)

calls: list[ast.Call] = []
for node in ast.walk(ast.Module(body=main_body, type_ignores=[])):
if (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and isinstance(node.func.value, ast.Name)
and node.func.value.id in runtime_names
):
calls.append(node)
return calls


def _method_name(call: ast.Call) -> str:
assert isinstance(call.func, ast.Attribute)
return call.func.attr


def _positional_names(call: ast.Call) -> list[str]:
return [a.id for a in call.args if isinstance(a, ast.Name)]


def test_63b_serves_doc_assistant_and_ops_bot():
calls = _main_block_calls(EXAMPLES / "63b_serve.py")
serve_calls = [c for c in calls if _method_name(c) == "serve"]
assert serve_calls, (
"63b_serve.py must call runtime.serve(...) in its __main__ block so a "
"process serves the workers that 63c_run_by_name.py needs; it currently "
"does not serve any agent."
)
served = set()
for call in serve_calls:
served.update(_positional_names(call))
assert {"doc_assistant", "ops_bot"} <= served, (
f"63b must serve both agents by name so 63c can run 'doc_assistant'; "
f"got served={sorted(served)}"
)


def test_63b_does_not_run_an_agent_object():
calls = _main_block_calls(EXAMPLES / "63b_serve.py")
run_calls = [c for c in calls if _method_name(c) == "run"]
assert not run_calls, (
"63b_serve.py is the serving daemon for 63c; its __main__ block must "
"not execute an agent with runtime.run(<agent>, ...)."
)


def test_63d_serves_monitoring_agent():
calls = _main_block_calls(EXAMPLES / "63d_serve_from_package.py")
serve_calls = [c for c in calls if _method_name(c) == "serve"]
assert serve_calls, (
"63d_serve_from_package.py must call runtime.serve(...) in its __main__ "
"block so a process serves the workers that 63e_run_monitoring.py needs; "
"it currently does not serve any agent."
)
served = set()
for call in serve_calls:
served.update(_positional_names(call))
assert "monitoring_agent" in served, (
f"63d must serve the monitoring agent so 63e can run 'monitoring'; "
f"got served={sorted(served)}"
)


def test_63d_does_not_run_an_agent_object():
calls = _main_block_calls(EXAMPLES / "63d_serve_from_package.py")
run_calls = [c for c in calls if _method_name(c) == "run"]
assert not run_calls, (
"63d_serve_from_package.py is the serving daemon for 63e; its __main__ "
"block must not execute an agent with runtime.run(<agent>, ...)."
)