Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/build-tui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ permissions:
contents: read

env:
AGENTSWARM_CLI_VERSION: 1.4.37-rc.1
AGENTSWARM_CLI_VERSION: 1.4.38

jobs:
prepare:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/live-run-mode-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: VRSEN/agentswarm-cli
ref: v1.4.37-rc.1
ref: v1.4.38
path: agentswarm-cli
- uses: oven-sh/setup-bun@v2
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-mac.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: VRSEN/agentswarm-cli
ref: v1.4.37-rc.1
ref: v1.4.38
path: agentswarm-cli
- uses: oven-sh/setup-bun@v2
with:
Expand Down
6 changes: 6 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
__pycache__/
*.py[cod]
*$py.class
node_modules/
.venv/
dist/
188 changes: 94 additions & 94 deletions package-lock.json

Large diffs are not rendered by default.

33 changes: 18 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vrsen/openswarm",
"version": "1.0.1-rc.7",
"version": "1.0.1-rc.8",
"description": "An open-source multi-agent AI team built on Agency Swarm",
"license": "MIT",
"publishConfig": {
Expand Down Expand Up @@ -32,13 +32,16 @@
"patches/",
"pyproject.toml",
"package.json",
"package-lock.json"
"package-lock.json",
"!**/__pycache__/**",
"!**/*.pyc",
"!node_modules/**"
],
"scripts": {
"postinstall": "node -e \"const fs=require('fs');const path=require('path');const cp=require('child_process');const pkg=__dirname;const patchTarget=path.join(pkg,'node_modules','dom-to-pptx');const patchCli=path.join(pkg,'node_modules','patch-package','index.js');if(fs.existsSync(patchTarget)&&fs.existsSync(patchCli)){cp.execFileSync(process.execPath,[patchCli],{cwd:pkg,stdio:'inherit'});}try{fs.chmodSync(path.join(pkg,'bin','openswarm'),0o755)}catch(e){}\""
},
"dependencies": {
"@vrsen/agentswarm": "1.4.37-rc.1",
"@vrsen/agentswarm": "1.4.38",
"dom-to-pptx": "1.1.5",
"patch-package": "^8.0.1",
"playwright": "^1.59.1",
Expand All @@ -49,18 +52,18 @@
"sharp": "^0.33.0"
},
"optionalDependencies": {
"@vrsen/openswarm-cli-darwin-arm64": "1.0.1-rc.7",
"@vrsen/openswarm-cli-darwin-x64": "1.0.1-rc.7",
"@vrsen/openswarm-cli-darwin-x64-baseline": "1.0.1-rc.7",
"@vrsen/openswarm-cli-linux-arm64": "1.0.1-rc.7",
"@vrsen/openswarm-cli-linux-arm64-musl": "1.0.1-rc.7",
"@vrsen/openswarm-cli-linux-x64": "1.0.1-rc.7",
"@vrsen/openswarm-cli-linux-x64-baseline": "1.0.1-rc.7",
"@vrsen/openswarm-cli-linux-x64-baseline-musl": "1.0.1-rc.7",
"@vrsen/openswarm-cli-linux-x64-musl": "1.0.1-rc.7",
"@vrsen/openswarm-cli-windows-arm64": "1.0.1-rc.7",
"@vrsen/openswarm-cli-windows-x64": "1.0.1-rc.7",
"@vrsen/openswarm-cli-windows-x64-baseline": "1.0.1-rc.7"
"@vrsen/openswarm-cli-darwin-arm64": "1.0.1-rc.8",
"@vrsen/openswarm-cli-darwin-x64": "1.0.1-rc.8",
"@vrsen/openswarm-cli-darwin-x64-baseline": "1.0.1-rc.8",
"@vrsen/openswarm-cli-linux-arm64": "1.0.1-rc.8",
"@vrsen/openswarm-cli-linux-arm64-musl": "1.0.1-rc.8",
"@vrsen/openswarm-cli-linux-x64": "1.0.1-rc.8",
"@vrsen/openswarm-cli-linux-x64-baseline": "1.0.1-rc.8",
"@vrsen/openswarm-cli-linux-x64-baseline-musl": "1.0.1-rc.8",
"@vrsen/openswarm-cli-linux-x64-musl": "1.0.1-rc.8",
"@vrsen/openswarm-cli-windows-arm64": "1.0.1-rc.8",
"@vrsen/openswarm-cli-windows-x64": "1.0.1-rc.8",
"@vrsen/openswarm-cli-windows-x64-baseline": "1.0.1-rc.8"
},
"engines": {
"node": ">=18.0.0",
Expand Down
111 changes: 87 additions & 24 deletions patches/patch_agency_swarm_dual_comms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,38 @@
from typing import Any


def _validate_communication_tool_class(tool_class: type, send_message_class: type, handoff_class: type) -> None:
if not issubclass(tool_class, (send_message_class, handoff_class)):
raise TypeError(
f"Invalid communication tool class: {tool_class.__name__}. "
"Expected a SendMessage or Handoff subclass."
)


def _add_tool_class_for_pair(
mapping: dict[tuple[str, str], list[type]],
default_tool_pairs: set[tuple[str, str]],
pair_key: tuple[str, str],
tool_class: type | None,
send_message_class: type,
handoff_class: type,
) -> None:
if tool_class is None:
return
_validate_communication_tool_class(tool_class, send_message_class, handoff_class)
if pair_key in default_tool_pairs and issubclass(tool_class, send_message_class):
raise ValueError(
f"Duplicate communication tool class detected for {pair_key[0]} -> {pair_key[1]}: "
f"{tool_class.__name__}. Each SendMessage tool for a pair can only be defined once."
)
classes = mapping.setdefault(pair_key, [])
if issubclass(tool_class, send_message_class):
for existing_tool_class in classes:
if issubclass(existing_tool_class, send_message_class):
raise ValueError(
f"Duplicate communication tool class detected for {pair_key[0]} -> {pair_key[1]}: "
f"{tool_class.__name__}. Each SendMessage tool for a pair can only be defined once."
)
if tool_class in classes:
raise ValueError(
f"Duplicate communication tool class detected for {pair_key[0]} -> {pair_key[1]}: "
Expand All @@ -30,6 +54,26 @@ def _add_tool_class_for_pair(
classes.append(tool_class)


def _add_default_tool_pair(
custom_mapping: dict[tuple[str, str], list[type]],
default_tool_pairs: set[tuple[str, str]],
pair_key: tuple[str, str],
send_message_class: type,
) -> None:
if pair_key in default_tool_pairs:
raise ValueError(
f"Duplicate communication flow detected: {pair_key[0]} -> {pair_key[1]}. "
"Each default agent-to-agent communication can only be defined once."
)
for tool_class in custom_mapping.get(pair_key, []):
if issubclass(tool_class, send_message_class):
raise ValueError(
f"Duplicate communication tool class detected for {pair_key[0]} -> {pair_key[1]}: "
f"{tool_class.__name__}. Each SendMessage tool for a pair can only be defined once."
)
default_tool_pairs.add(pair_key)


def apply_dual_comms_patch() -> None:
import warnings

Expand All @@ -44,9 +88,10 @@ def apply_dual_comms_patch() -> None:

def parse_agent_flows_patched(
agency: Any, communication_flows: list[Any]
) -> tuple[list[tuple[Agent, Agent]], dict[tuple[str, str], list[type]]]:
) -> tuple[list[tuple[Agent, Agent]], dict[tuple[str, str], list[type]], set[tuple[str, str]]]:
basic_flows: list[tuple[Agent, Agent]] = []
tool_class_mapping: dict[tuple[str, str], list[type]] = {}
default_tool_pairs: set[tuple[str, str]] = set()
seen_flows: set[tuple[str, str]] = set()

chain_flows = AgentFlow.get_and_clear_chain_flows()
Expand All @@ -61,13 +106,10 @@ def parse_agent_flows_patched(

if isinstance(first, Agent) and isinstance(second, Agent):
flow_key = (first.name, second.name)
if flow_key in seen_flows:
raise ValueError(
f"Duplicate communication flow detected: {first.name} -> {second.name}. "
"Each agent-to-agent communication can only be defined once."
)
seen_flows.add(flow_key)
basic_flows.append((first, second))
if flow_key not in seen_flows:
seen_flows.add(flow_key)
basic_flows.append((first, second))
_add_default_tool_pair(tool_class_mapping, default_tool_pairs, flow_key, SendMessage)

elif isinstance(first, AgentFlow) and (isinstance(second, type) or second is None):
tool_class = second
Expand All @@ -84,12 +126,19 @@ def parse_agent_flows_patched(
seen_flows.add(flow_key)
basic_flows.append((sender, receiver))
elif tool_class is None:
raise ValueError(
f"Duplicate communication flow detected: {sender.name} -> {receiver.name}. "
"Each agent-to-agent communication can only be defined once unless adding "
"a distinct tool class."
_add_default_tool_pair(tool_class_mapping, default_tool_pairs, flow_key, SendMessage)
continue
if tool_class is None:
_add_default_tool_pair(tool_class_mapping, default_tool_pairs, flow_key, SendMessage)
else:
_add_tool_class_for_pair(
tool_class_mapping,
default_tool_pairs,
flow_key,
tool_class,
SendMessage,
Handoff,
)
_add_tool_class_for_pair(tool_class_mapping, flow_key, tool_class)
else:
raise TypeError(
f"Invalid communication flow entry: {flow_entry}. "
Expand All @@ -105,6 +154,8 @@ def parse_agent_flows_patched(
# The agency factory reconstructs flows from _communication_tool_classes,
# which stores lists of types per pair. Accept both a single class and a list.
tool_classes = tool_class if isinstance(tool_class, (list, tuple)) else [tool_class]
if not tool_classes:
raise ValueError("Communication flow tool class list cannot be empty.")
for tc in tool_classes:
if not isinstance(tc, type):
raise TypeError(f"Invalid tool class in communication flow: {tc}. Expected a class type.")
Expand All @@ -115,12 +166,12 @@ def parse_agent_flows_patched(
basic_flows.append((sender, receiver))

for tc in tool_classes:
_add_tool_class_for_pair(tool_class_mapping, flow_key, tc)
_add_tool_class_for_pair(tool_class_mapping, default_tool_pairs, flow_key, tc, SendMessage, Handoff)

else:
raise ValueError(f"Invalid communication flow entry: {flow_entry}. Expected 2 or 3 elements.")

return basic_flows, tool_class_mapping
return basic_flows, tool_class_mapping, default_tool_pairs

def configure_agents_patched(agency: Any, defined_communication_flows: list[tuple[Agent, Agent]]) -> None:
setup_mod.logger.info("Configuring agents...")
Expand All @@ -137,15 +188,26 @@ def configure_agents_patched(agency: Any, defined_communication_flows: list[tupl
allowed_recipients = communication_map.get(agent_name, [])

if allowed_recipients:
if not agent_instance.supports_outbound_communication:
joined = ", ".join(allowed_recipients)
raise ValueError(
f"Agent '{agent_name}' cannot be the sender in communication_flows. "
f"It can receive delegated work, but it cannot delegate to: {joined}."
)
setup_mod.logger.debug(f"Agent '{agent_name}' can send messages to: {allowed_recipients}")
for recipient_name in allowed_recipients:
recipient_agent = agency.agents[recipient_name]
pair_key = (agent_name, recipient_name)
configured = agency._communication_tool_classes.get(pair_key, [])
tool_classes = list(configured) if configured else [agency.send_message_tool_class or SendMessage]

try:
for effective_tool_class in tool_classes:
tool_classes: list[type] = []
if pair_key in agency._default_communication_tool_pairs:
tool_classes.append(agency.send_message_tool_class or SendMessage)
tool_classes.extend(configured)
if not tool_classes:
tool_classes.append(agency.send_message_tool_class or SendMessage)

for effective_tool_class in tool_classes:
try:
if isinstance(effective_tool_class, Handoff) or (
isinstance(effective_tool_class, type) and issubclass(effective_tool_class, Handoff)
):
Expand All @@ -162,6 +224,7 @@ def configure_agents_patched(agency: Any, defined_communication_flows: list[tupl
setup_mod._warned_deprecated_send_message_handoff = True

handoff_instance = effective_tool_class().create_handoff(recipient_agent=recipient_agent)
handoff_instance._agency_swarm_tool_class = effective_tool_class
runtime_state.handoffs.append(handoff_instance)
setup_mod.logger.debug(f"Added Handoff for {agent_name} -> {recipient_name}")
else:
Expand All @@ -174,11 +237,11 @@ def configure_agents_patched(agency: Any, defined_communication_flows: list[tupl
send_message_tool_class=chosen_tool_class,
runtime_state=runtime_state,
)
except Exception as e:
setup_mod.logger.error(
f"Error registering subagent '{recipient_name}' for sender '{agent_name}': {e}",
exc_info=True,
)
except Exception as e:
setup_mod.logger.error(
f"Error registering subagent '{recipient_name}' for sender '{agent_name}': {e}",
exc_info=True,
)
else:
setup_mod.logger.debug(f"Agent '{agent_name}' has no explicitly defined outgoing communication paths.")

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[project]
name = "open-swarm"
description = "An open-source multi-agent AI team built on Agency Swarm and the OpenAI Agents SDK"
version = "1.0.1-rc.7"
version = "1.0.1-rc.8"
license = { text = "MIT" }
keywords = ["agency-swarm", "openai", "multi-agent", "open-source", "openswarm"]
requires-python = ">=3.12"
dependencies = [
"agency-swarm[fastapi,jupyter,litellm]>=1.9.8",
"agency-swarm[fastapi,jupyter,litellm]>=1.10.0",
"questionary>=2.0.0",
"python-dotenv",
"rich",
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
agency-swarm[fastapi,jupyter,litellm]>=1.9.7
agency-swarm[fastapi,jupyter,litellm]>=1.10.0
questionary>=2.0.0
fastapi
uvicorn
Expand Down
27 changes: 20 additions & 7 deletions scripts/smoke-run-mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ def install_openswarm_tui_binary(package_dir: pathlib.Path, binary: pathlib.Path
return target


def create_local_openswarm_project(package_dir: pathlib.Path, root: pathlib.Path) -> pathlib.Path:
target = root / "openswarm"
def create_local_openswarm_project(package_dir: pathlib.Path, root: pathlib.Path, name: str = "openswarm") -> pathlib.Path:
target = root / name
ignore = shutil.ignore_patterns(
".git",
".venv",
Expand Down Expand Up @@ -176,6 +176,14 @@ def compact(text: str) -> str:
return re.sub(r"\s+", "", text)


def is_run_mode_ready(plain: str) -> bool:
packed = compact(plain).lower()
has_current_footer = "run·swarmdefault" in packed or "runswarmdefault" in packed
has_command_ui = "tabagents" in packed and "ctrl+pcommands" in packed
has_legacy_footer = "agencyswarmdefault" in packed
return has_command_ui and (has_current_footer or has_legacy_footer)


def terminate(process: subprocess.Popen[bytes]) -> None:
if process.poll() is None:
try:
Expand Down Expand Up @@ -306,7 +314,7 @@ def run_tui_smoke(
write(master_fd, "\r")
sent_confirm = True

run_mode_ready = "AgencySwarmDefault" in compact_plain and "ctrl+pcommands" in compact_plain
run_mode_ready = is_run_mode_ready(plain)

if check in {"agents", "all"} and not sent_agents_command and run_mode_ready:
write(master_fd, "/agents\r")
Expand Down Expand Up @@ -398,7 +406,9 @@ def run_tui_smoke(
if selected_models_command and not verified_models:
models_picker = compact(plain[models_picker_start:]).lower()
if "selectmodel" in models_picker and (
"agencyswarmdefault" in models_picker or "manageproviderauth" in models_picker
"swarmdefault" in models_picker
or "agencyswarmdefault" in models_picker
or "manageproviderauth" in models_picker
):
verified_models = True
write(master_fd, "\x1b")
Expand Down Expand Up @@ -479,6 +489,7 @@ def main() -> int:
raise RuntimeError("OPENAI_API_KEY is required for the live prompt smoke test")
auth_key = api_key or "dummy-openai-key-for-agent-roster-smoke"
root = pathlib.Path(tempfile.mkdtemp(prefix="openswarm-run-mode-smoke-"))
state_root = root / "openswarm-state"
env = os.environ.copy()
env.update(
{
Expand All @@ -491,6 +502,7 @@ def main() -> int:
"XDG_CONFIG_HOME": str(root / "xdg-config"),
"XDG_CACHE_HOME": str(root / "xdg-cache"),
"XDG_STATE_HOME": str(root / "xdg-state"),
"OPENSWARM_STATE_ROOT": str(state_root),
"OPENCODE_DISABLE_AUTOUPDATE": "true",
"OPENCODE_DISABLE_MODELS_FETCH": "true",
}
Expand All @@ -517,10 +529,11 @@ def main() -> int:
),
encoding="utf-8",
)
project_dir = create_local_openswarm_project(package_dir, generic_dir)
state_root.mkdir(parents=True, exist_ok=True)
project_dir = create_local_openswarm_project(package_dir, state_root, "project")
plain = run_tui_smoke(launcher, package_dir, project_dir, root, env, args.check, args.prompt, args.expect, args.timeout)
if "Agency Swarm Default" not in plain:
raise RuntimeError("Smoke response was seen, but Agency Swarm Run mode was not detected")
if not is_run_mode_ready(plain):
raise RuntimeError("Smoke response was seen, but OpenSwarm Run mode and command UI were not detected")
if args.check in {"agents", "all"}:
print(f"OpenSwarm /agents smoke passed with {EXPECTED_AGENT_COUNT} agents visible")
if args.check in {"models", "all"}:
Expand Down
Loading