Skip to content

Commit c44e7fa

Browse files
Merge pull request #439 from conductor-oss/examples/verification-fixes
fix(examples): fix 9 broken examples + worker-tool registry lookup (companion to #438)
2 parents b581a5e + 7258f97 commit c44e7fa

11 files changed

Lines changed: 171 additions & 34 deletions

File tree

examples/agentic_workflows/llm_chat.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,14 @@ def create_chat_workflow(executor) -> ConductorWorkflow:
8787
messages=[
8888
ChatMessage(
8989
role="system",
90-
message="You are an expert in science. Think of a random scientific "
91-
"discovery and create a short, interesting question about it.",
90+
message="You are an expert in science.",
91+
),
92+
# The server requires a non-empty user message — a system-only
93+
# conversation fails validation before the first task runs.
94+
ChatMessage(
95+
role="user",
96+
message="Think of a random scientific discovery and create a "
97+
"short, interesting question about it.",
9298
),
9399
],
94100
temperature=0.7,
@@ -121,9 +127,16 @@ def create_chat_workflow(executor) -> ConductorWorkflow:
121127
ChatMessage(
122128
role="system",
123129
message=(
124-
"You are an expert in science. Given the context below, "
130+
"You are an expert in science. Given the context provided, "
125131
"generate a follow-up question to dive deeper into the topic. "
126-
"Do not repeat previous questions.\n\n"
132+
"Do not repeat previous questions."
133+
),
134+
),
135+
# The server requires a non-empty user message — a system-only
136+
# conversation fails validation before the task runs.
137+
ChatMessage(
138+
role="user",
139+
message=(
127140
"Context:\n${chat_complete_ref.output.result}\n\n"
128141
"Previous questions:\n"
129142
"${collect_history_ref.input.history}"
@@ -198,6 +211,16 @@ def main():
198211
printed_tasks.add(ref)
199212
time.sleep(2)
200213

214+
# is_completed() is true for any terminal state — verify the workflow
215+
# actually COMPLETED rather than FAILED/TERMINATED/TIMED_OUT.
216+
result = workflow_client.get_workflow(workflow_id=workflow_id, include_tasks=False)
217+
if result.status != "COMPLETED":
218+
print("=" * 70)
219+
print(f"Workflow ended {result.status}: {result.reason_for_incompletion}")
220+
print(f"Full execution: {api_config.ui_host}/execution/{workflow_id}")
221+
print("=" * 70)
222+
raise SystemExit(1)
223+
201224
print("=" * 70)
202225
print("Conversation complete.")
203226
print(f"Full execution: {api_config.ui_host}/execution/{workflow_id}")

examples/agents/16i_credentials_langchain.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,22 @@
2323
from settings import settings
2424

2525

26+
# Tool callables must live at module level: worker processes are spawned on
27+
# macOS/Windows and re-import this module, so a tool defined inside a factory
28+
# function ("<locals>") cannot be resolved by qualified name (SpawnSafetyError).
29+
def check_github_token() -> str:
30+
"""Check if GitHub token is available in the environment."""
31+
token = os.environ.get("GITHUB_TOKEN", "")
32+
if token:
33+
return f"GitHub token available (starts with {token[:4]}...)"
34+
return "GitHub token is NOT available"
35+
36+
2637
def create_langchain_agent():
2738
"""Create a LangChain agent with a tool that uses GITHUB_TOKEN."""
2839
from langchain.agents import create_agent
2940
from langchain_core.tools import tool as lc_tool
3041

31-
@lc_tool
32-
def check_github_token() -> str:
33-
"""Check if GitHub token is available in the environment."""
34-
token = os.environ.get("GITHUB_TOKEN", "")
35-
if token:
36-
return f"GitHub token available (starts with {token[:4]}...)"
37-
return "GitHub token is NOT available"
38-
3942
model_str = settings.llm_model
4043
# create_agent accepts "provider:model" format (e.g. "openai:gpt-4o")
4144
if "/" in model_str:
@@ -44,7 +47,7 @@ def check_github_token() -> str:
4447

4548
agent = create_agent(
4649
model_str,
47-
tools=[check_github_token],
50+
tools=[lc_tool(check_github_token)],
4851
system_prompt="You are a helpful assistant. Use tools when asked.",
4952
)
5053
return agent

examples/agents/16j_credentials_openai_sdk.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,27 @@
2222
from conductor.ai.agents import AgentRuntime
2323

2424

25+
# Tool callables must live at module level: worker processes are spawned on
26+
# macOS/Windows and re-import this module, so a tool defined inside a factory
27+
# function ("<locals>") cannot be resolved by qualified name (SpawnSafetyError).
28+
# Keep the plain function importable and apply @function_tool at agent
29+
# construction, so the module global is not rebound to a FunctionTool.
30+
def check_github_auth() -> str:
31+
"""Check if GitHub authentication is available."""
32+
token = os.environ.get("GITHUB_TOKEN", "")
33+
if token:
34+
return f"GitHub token is set (starts with {token[:4]}...)"
35+
return "GitHub token is NOT set"
36+
37+
2538
def create_openai_agent():
2639
"""Create an OpenAI Agent SDK agent with a credential-aware tool."""
2740
from agents import Agent, function_tool
2841

29-
@function_tool
30-
def check_github_auth() -> str:
31-
"""Check if GitHub authentication is available."""
32-
token = os.environ.get("GITHUB_TOKEN", "")
33-
if token:
34-
return f"GitHub token is set (starts with {token[:4]}...)"
35-
return "GitHub token is NOT set"
36-
3742
agent = Agent(
3843
name="github_checker",
3944
instructions="You check GitHub authentication status. Use the tool when asked.",
40-
tools=[check_github_auth],
45+
tools=[function_tool(check_github_auth)],
4146
)
4247
return agent
4348

examples/agents/94_openai_runner_tools.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ class Weather(BaseModel):
4949
conditions: str = Field(description="The weather conditions")
5050

5151

52-
@function_tool
52+
# Keep the plain function importable at module level and apply
53+
# @function_tool at Agent construction: worker processes are spawned on
54+
# macOS/Windows and re-import this module by qualified name, which fails
55+
# when the decorator has rebound the module global to a FunctionTool.
5356
def get_weather(city: Annotated[str, "The city to get the weather for"]) -> Weather:
5457
"""Get the current weather information for a specified city."""
5558
print("[debug] get_weather called")
@@ -59,7 +62,7 @@ def get_weather(city: Annotated[str, "The city to get the weather for"]) -> Weat
5962
agent = Agent(
6063
name="weather_agent",
6164
instructions="You are a helpful agent.",
62-
tools=[get_weather],
65+
tools=[function_tool(get_weather)],
6366
)
6467

6568

examples/agents/kitchen_sink.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,21 +157,18 @@
157157

158158

159159
@agent(name="tech_classifier", model=settings.llm_model)
160-
def tech_classifier(prompt: str) -> str:
160+
def tech_classifier() -> str:
161161
"""Classifies tech articles."""
162-
pass
163162

164163

165164
@agent(name="business_classifier", model=settings.llm_model)
166-
def business_classifier(prompt: str) -> str:
165+
def business_classifier() -> str:
167166
"""Classifies business articles."""
168-
pass
169167

170168

171169
@agent(name="creative_classifier", model=settings.llm_model)
172-
def creative_classifier(prompt: str) -> str:
170+
def creative_classifier() -> str:
173171
"""Classifies creative articles."""
174-
pass
175172

176173

177174
intake_router = Agent(

examples/agents/run_all_examples.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@
6464
}
6565
# Require external infra / providers not available in this environment.
6666
INFRA_SKIP = {
67+
"30_skills_dg_review.py": "needs the dg skill cloned to ~/.claude/skills/dg",
68+
"32_skills_multi_agent.py": "needs the dg skill cloned to ~/.claude/skills/dg",
69+
"75_wait_for_message.py": "needs the workflow messages API (not in conductor-oss 3.32.0-rc.8)",
70+
"82_fan_out_fan_in.py": "needs the workflow messages API (not in conductor-oss 3.32.0-rc.8)",
71+
"83_stateful_resume.py": "needs the workflow messages API (not in conductor-oss 3.32.0-rc.8)",
72+
"84_deterministic_stop.py": "needs the workflow messages API (not in conductor-oss 3.32.0-rc.8)",
6773
"04_mcp_weather.py": "needs an MCP server",
6874
"04_http_and_mcp_tools.py": "needs an MCP server",
6975
"16f_credentials_mcp_tool.py": "needs an MCP server",

examples/helloworld/greetings_worker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def greet(name: str) -> str:
2121
poll_timeout=100, # Default poll timeout (ms)
2222
lease_extend_enabled=False # Fast tasks don't need lease extension
2323
)
24-
def greet(name: str) -> str:
24+
def greet_sync(name: str) -> str:
2525
"""
2626
Synchronous worker - automatically runs in thread pool to avoid blocking.
2727
Good for legacy code or simple CPU-bound tasks.

examples/user_example/user_workers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from conductor.client.context import get_task_context
1010
from conductor.client.worker.worker_task import worker_task
11-
from examples.user_example.models import User
11+
from user_example.models import User
1212

1313

1414
@worker_task(

examples/worker_example.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import logging
3131
import os
3232
import shutil
33+
import tempfile
3334
import time
3435
from typing import Union
3536

@@ -313,7 +314,7 @@ def main():
313314
api_config = Configuration()
314315

315316
# Metrics configuration - HTTP mode (recommended)
316-
metrics_dir = os.path.join('/Users/viren/', 'conductor_metrics')
317+
metrics_dir = os.path.join(tempfile.gettempdir(), 'conductor_metrics')
317318

318319
# Clean up any stale metrics data from previous runs
319320
if os.path.exists(metrics_dir):

src/conductor/ai/agents/tool.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,36 @@ def agent_tool(
11591159
# ── Utilities ───────────────────────────────────────────────────────────
11601160

11611161

1162+
def _entry_resolves_to(entry_func: Any, original: Callable[..., Any],
1163+
func: Callable[..., Any]) -> bool:
1164+
"""True if a ``_decorated_functions`` entry refers to *original*/*func*.
1165+
1166+
A plain identity check is not enough: once a ``@worker_task`` function is
1167+
used as an agent tool, ``ToolRegistry.register_tool_workers`` re-registers
1168+
the same task name with a spawn-safe ``ToolWorkerEntry`` wrapper, which
1169+
carries the original either directly (``fn_direct``) or by qualified name
1170+
(``fn_ref``). Walk the ``__wrapped__`` chain and those carriers.
1171+
"""
1172+
obj = entry_func
1173+
for _ in range(8): # bounded — wrapper chains are shallow
1174+
if obj is None:
1175+
return False
1176+
if obj is original or obj is func:
1177+
return True
1178+
fn_direct = getattr(obj, "fn_direct", None)
1179+
if fn_direct is not None and (fn_direct is original or fn_direct is func):
1180+
return True
1181+
fn_ref = getattr(obj, "fn_ref", None)
1182+
if (
1183+
fn_ref is not None
1184+
and getattr(fn_ref, "module", None) == getattr(original, "__module__", None)
1185+
and getattr(fn_ref, "qualname", None) == getattr(original, "__qualname__", None)
1186+
):
1187+
return True
1188+
obj = getattr(obj, "__wrapped__", None)
1189+
return False
1190+
1191+
11621192
def _try_worker_task(func: Callable[..., Any]) -> Optional[ToolDef]:
11631193
"""Try to build a :class:`ToolDef` from a ``@worker_task``-decorated function.
11641194
@@ -1174,7 +1204,7 @@ def _try_worker_task(func: Callable[..., Any]) -> Optional[ToolDef]:
11741204
original = getattr(func, "__wrapped__", func)
11751205

11761206
for (task_name, _domain), entry in _decorated_functions.items():
1177-
if entry["func"] is original or entry["func"] is func:
1207+
if _entry_resolves_to(entry["func"], original, func):
11781208
from conductor.ai.agents._internal.schema_utils import schema_from_function
11791209

11801210
description = inspect.getdoc(original) or ""

0 commit comments

Comments
 (0)