Skip to content

Commit 17765cf

Browse files
committed
docs and fixes
1 parent 2ff6284 commit 17765cf

7 files changed

Lines changed: 1171 additions & 22 deletions

File tree

README.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,20 @@ If you find [Conductor](https://github.com/conductor-oss/conductor) useful, plea
3434

3535
## Start Conductor Server
3636

37-
If you don't already have a Conductor server running, pick one:
37+
#### Conductor CLI
38+
```shell
39+
# Installs conductor cli
40+
npm install -g @conductor-oss/conductor-cli
41+
42+
# Start the open source conductor server on port 8080
43+
conductor server start
44+
# see conductor server --help for all the available commands
45+
```
46+
3847

39-
**Docker Compose (recommended, includes UI):**
48+
Alternatively, if you want to use docker
49+
50+
**Docker Compose**
4051

4152
```shell
4253
docker run -p 8080:8080 conductoross/conductor:latest
@@ -48,15 +59,6 @@ The UI will be available at `http://localhost:8080` and the API at `http://local
4859
curl -sSL https://raw.githubusercontent.com/conductor-oss/conductor/main/conductor_server.sh | sh
4960
```
5061

51-
**Conductor CLI**
52-
```shell
53-
# Installs conductor cli
54-
npm install -g @conductor-oss/conductor-cli
55-
56-
# Start the open source conductor server
57-
conductor server start
58-
# see conductor server --help for all the available commands
59-
```
6062

6163
## Install the SDK
6264

SDK_UPDATE_v1.md

Lines changed: 326 additions & 0 deletions
Large diffs are not rendered by default.

docs/design/AGENT_SDK_PORTING_SPEC.md

Lines changed: 708 additions & 0 deletions
Large diffs are not rendered by default.

examples/agents/settings.py

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

2222
@dataclass
2323
class Settings:
24-
llm_model: str = "anthropic/claude-sonnet-4-6"
24+
llm_model: str = "openai/gpt-4o"
2525
secondary_llm_model: str = "openai/gpt-4o"
2626

2727
@classmethod

src/conductor/ai/agents/runtime/_worker_entries.py

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -492,22 +492,62 @@ async def __call__(self, result: str = "", iteration: int = 0) -> object:
492492

493493

494494
class CheckTransferEntry:
495-
"""Detect transfer tool calls (was ``check_transfer_worker``; stateless)."""
495+
"""Detect transfer tool calls (was ``check_transfer_worker``; stateless).
496+
497+
Selection is first-wins: when the LLM emits multiple transfer calls in one
498+
turn only the first is honored (the swarm loop can only hand off to one
499+
agent). The others are surfaced as ``dropped_transfers`` so the intent is
500+
visible in the task output instead of silently discarded — the transfer
501+
tool descriptions instruct the model to call at most one per turn.
502+
503+
``transfer_message`` carries the transfer tool's ``message`` argument (the
504+
hand-off note for the receiving agent); the server compiler records it in
505+
the conversation as ``[agent -> target]: <message>``.
506+
"""
496507

497508
async def __call__(self, tool_calls: object = None, _unused: str = "") -> object:
509+
transfers = []
498510
for tc in tool_calls or []:
499-
name = tc.get("name", "")
500-
if "_transfer_to_" in name:
501-
return {"is_transfer": True, "transfer_to": name.split("_transfer_to_", 1)[1]}
502-
return {"is_transfer": False, "transfer_to": ""}
511+
name = tc.get("name", "") or ""
512+
if "_transfer_to_" not in name:
513+
continue
514+
params = tc.get("inputParameters") or {}
515+
message = params.get("message")
516+
transfers.append(
517+
{
518+
"transfer_to": name.split("_transfer_to_", 1)[1],
519+
"message": "" if message is None else str(message),
520+
}
521+
)
522+
if not transfers:
523+
return {"is_transfer": False, "transfer_to": "", "transfer_message": ""}
524+
first = transfers[0]
525+
out = {
526+
"is_transfer": True,
527+
"transfer_to": first["transfer_to"],
528+
"transfer_message": first["message"],
529+
}
530+
if len(transfers) > 1:
531+
import logging
532+
533+
logging.getLogger(__name__).warning(
534+
"Multiple transfer calls in one turn; honoring '%s', dropping %s",
535+
first["transfer_to"],
536+
[t["transfer_to"] for t in transfers[1:]],
537+
)
538+
out["dropped_transfers"] = transfers[1:]
539+
return out
503540

504541

505542
class TransferNoopEntry:
506543
"""No-op transfer tool (was the nested ``transfer_worker``; handoff is
507-
detected by check_transfer from toolCalls output)."""
544+
detected by check_transfer from toolCalls output). Echoes the hand-off
545+
``message`` so it is visible in the task output / UI."""
508546

509-
async def __call__(self) -> object:
510-
return {}
547+
async def __call__(self, message: str = "") -> object:
548+
if not message:
549+
return {}
550+
return {"message": message}
511551

512552

513553
class TransferUnreachableEntry:

src/conductor/ai/agents/runtime/runtime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1557,7 +1557,7 @@ def _register_hybrid_transfer_workers(
15571557
for sub in agent.agents:
15581558
tool_name = f"{agent.name}_transfer_to_{sub.name}"
15591559
transfer_worker = TransferNoopEntry()
1560-
transfer_worker.__annotations__ = {"return": object}
1560+
transfer_worker.__annotations__ = {"message": str, "return": object}
15611561
probe_spawn_safety(transfer_worker, tool_name, group="system")
15621562
worker_task(
15631563
task_definition_name=tool_name,
@@ -1685,7 +1685,7 @@ def _register_swarm_transfer_workers(
16851685
transfer_worker.__annotations__ = {"return": str}
16861686
else:
16871687
transfer_worker = TransferNoopEntry()
1688-
transfer_worker.__annotations__ = {"return": object}
1688+
transfer_worker.__annotations__ = {"message": str, "return": object}
16891689
probe_spawn_safety(transfer_worker, tool_name, group="system")
16901690
worker_task(
16911691
task_definition_name=tool_name,

tests/unit/ai/test_worker_entries.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,82 @@ def test_transfer_entries_pickle(self):
329329
)
330330

331331
assert asyncio.run(pickle.loads(pickle.dumps(TransferNoopEntry()))()) == {}
332+
# Echoes the hand-off message so it is visible in the task output.
333+
assert asyncio.run(pickle.loads(pickle.dumps(TransferNoopEntry()))(message="do X")) == {
334+
"message": "do X"
335+
}
332336
msg = asyncio.run(pickle.loads(pickle.dumps(TransferUnreachableEntry("a_transfer_to_b")))())
333337
assert "a_transfer_to_b is not available" in msg
334338

339+
def test_check_transfer_extracts_message(self):
340+
import asyncio
341+
342+
from conductor.ai.agents.runtime._worker_entries import CheckTransferEntry
343+
344+
entry = pickle.loads(pickle.dumps(CheckTransferEntry()))
345+
out = asyncio.run(
346+
entry(
347+
tool_calls=[
348+
{
349+
"name": "ceo_transfer_to_engineering_lead",
350+
"inputParameters": {
351+
"method": "ceo_transfer_to_engineering_lead",
352+
"message": "Design the REST API",
353+
},
354+
}
355+
]
356+
)
357+
)
358+
assert out == {
359+
"is_transfer": True,
360+
"transfer_to": "engineering_lead",
361+
"transfer_message": "Design the REST API",
362+
}
363+
364+
def test_check_transfer_no_transfer_and_missing_message(self):
365+
import asyncio
366+
367+
from conductor.ai.agents.runtime._worker_entries import CheckTransferEntry
368+
369+
entry = CheckTransferEntry()
370+
assert asyncio.run(entry(tool_calls=None)) == {
371+
"is_transfer": False,
372+
"transfer_to": "",
373+
"transfer_message": "",
374+
}
375+
# Transfer without a message arg (older tool schema) → empty message
376+
out = asyncio.run(
377+
entry(tool_calls=[{"name": "a_transfer_to_b", "inputParameters": {"method": "x"}}])
378+
)
379+
assert out == {"is_transfer": True, "transfer_to": "b", "transfer_message": ""}
380+
381+
def test_check_transfer_multiple_calls_first_wins_and_reports_dropped(self):
382+
import asyncio
383+
384+
from conductor.ai.agents.runtime._worker_entries import CheckTransferEntry
385+
386+
entry = CheckTransferEntry()
387+
out = asyncio.run(
388+
entry(
389+
tool_calls=[
390+
{
391+
"name": "ceo_transfer_to_engineering_lead",
392+
"inputParameters": {"message": "eng task"},
393+
},
394+
{
395+
"name": "ceo_transfer_to_marketing_lead",
396+
"inputParameters": {"message": "mkt task"},
397+
},
398+
]
399+
)
400+
)
401+
assert out["is_transfer"] is True
402+
assert out["transfer_to"] == "engineering_lead"
403+
assert out["transfer_message"] == "eng task"
404+
assert out["dropped_transfers"] == [
405+
{"transfer_to": "marketing_lead", "message": "mkt task"}
406+
]
407+
335408

336409
# ── Framework worker entries (Group C) ───────────────────────────────────
337410

0 commit comments

Comments
 (0)