diff --git a/skills/swarm-orchestration/SKILL.md b/skills/swarm-orchestration/SKILL.md new file mode 100644 index 0000000..7234c67 --- /dev/null +++ b/skills/swarm-orchestration/SKILL.md @@ -0,0 +1,90 @@ +--- +name: Swarm Orchestration +description: Pattern for 1-to-many control of agent instances using tmux and session resumption. +--- + +# 1-to-Many Agent Swarm Orchestration Skill + +## Overview +This skill defines a pattern for an orchestrator agent to spawn, track, and control multiple sub-agent instances (a "swarm") using `tmux` for environment isolation and command-line interfaces supporting session resumption. + +## Prerequisites +- `tmux` installed on the host system. +- An interactive CLI or agent tool that supports: + - Running in a terminal. + - Exposing a unique Session/Conversation ID. + - Resuming sessions via targeting flags (e.g., `-conversation=""`). + +## Procedure + +### 1. Initialize Swarm Environment +Create a `tmux` session with multiple windows or panes, depending on the desired layout. + +**Windows Layout (Clean separation):** +```bash +tmux new-session -d -s -n agent1 +tmux new-window -t -n agent2 +... +``` + +**Grid Layout (All-in-one view):** +```bash +tmux new-window -t -n grid +tmux split-window -t :grid +tmux select-layout -t :grid tiled +``` + +### 2. Spawn Agents +Send execution commands to the specific panes/windows. + +```bash +tmux send-keys -t : "/path/to/cli --args" C-m +``` + +### 3. Capture Session IDs +Extract the unique identifiers generated by the agents to enable future resumption. +- Use `tmux capture-pane -p -t ` to read the screen. +- Look for patterns indicating Conversation IDs or session hashes. +- If not visible, trigger menus or status commands (e.g., `/` commands). + +### 4. Record State +Store the mapping of `Pane/Window -> Session ID` in a persistent file (e.g., `swarm_sessions.md`) so the orchestrator can resume tracking after context breaks or restarts. + +## File-Based Orchestration Framework + +To automate communication, use a file-based message bus. + +### Directory Convention +Create a directory `tmp/swarm/` in the project root: +- `requests/`: For dropping request files. +- `responses/`: For reading results. +- `scripts/`: For the orchestrator script. + +### Payload Formats + +**Request File (`request_.json`):** +```json +{ + "request_id": "req_123", + "target_agent": "grid.0", + "prompt": "Your instruction here", + "response_file": "tmp/swarm/responses/res_123.json" +} +``` + +**Response File (`res_123.json`):** +```json +{ + "request_id": "req_123", + "status": "success", + "output": "... captured terminal output ..." +} +``` + +### Orchestrator Script +A script running in the background polls `requests/`, executes the `tmux send-keys` command, captures the results, and writes the response file. + +Example implementation at: `tmp/swarm/scripts/swarm_orchestrator.py` + +## General Applicability +This pattern applies to any CLI-based tool or agent system that allows isolated instances and session targeting. It is not limited to any specific project or workspace. diff --git a/swarm_framework/README.md b/swarm_framework/README.md new file mode 100644 index 0000000..67c120e --- /dev/null +++ b/swarm_framework/README.md @@ -0,0 +1,126 @@ +# Swarm Orchestration Framework + +This framework allows you to control a swarm of $N$ agents with isolated environments via file operations. + +## Setup + +Run the bootstrap script to create the tmux session, spawn agents, and start the orchestrator: + +```bash +cd swarm_framework +chmod +x bootstrap.sh +./bootstrap.sh +``` +Replace `` with the desired number of agents (default is 4). + +## Isolation +Each agent runs in its own directory: `swarm_framework/agents/agent_N/`. +This isolates their working directory and local state if they store it there. + +## Usage + +1. **Bootstrap**: Run the bootstrap script to create the swarm environment and start the Master Agent. + ```bash + ./bootstrap.sh + ``` + Default is 4 agents if not specified. + This script will start the **Master Agent (jetski-cli)** in the **foreground**. + **Exit** the Master Agent (Ctrl+D or type exit) to stop the swarm and cleanup all resources. + + +2. **Monitor**: Open another terminal and attach to the tmux session to see sub-agents: + ```bash + tmux attach -t jetski_swarm + ``` + +3. **Send Request**: Drop JSON request files into `comms/requests/`. + + +**Format:** +```json +{ + "request_id": "req_123", + "target_agent": "agent_1", + "prompt": "Hello", + "response_file": "swarm_framework/comms/responses/res_123.json" +} +``` +Note: The `target_agent` should be the window name (e.g., `agent_1`, `agent_2`). + +## Control Actions + +You can control the swarm lifecycle by sending requests with `type: "control"`. + +**Actions:** +- `create`: Spawn a new agent window and directory. +- `kill`: Remove an agent window. +- `clear`: Clear agent screen (visual). +- `resume`: Resume a session by ID. + +**Examples:** + +**Create Agent:** +```json +{ + "request_id": "req_create", + "type": "control", + "action": "create", + "target_agent": "agent_new", + "response_file": "swarm_framework/comms/responses/res_create.json" +} +``` + +**Kill Agent:** +```json +{ + "request_id": "req_kill", + "type": "control", + "action": "kill", + "target_agent": "agent_new", + "response_file": "swarm_framework/comms/responses/res_kill.json" +} +``` + +**Clear Screen:** +```json +{ + "request_id": "req_clear", + "type": "control", + "action": "clear", + "target_agent": "agent_1", + "response_file": "swarm_framework/comms/responses/res_clear.json" +} +``` + +**Resume Session:** +```json +{ + "request_id": "req_resume", + "type": "control", + "action": "resume", + "target_agent": "agent_1", + "conversation_id": "CONV_ID_HERE", + "response_file": "swarm_framework/comms/responses/res_resume.json" +} +``` + + +## Cleanup + +To clear the CLI screen for a specific agent (visually cleaning up the window): + +```bash +./clear_pane.sh +``` +Example: `./clear_pane.sh agent_1` + +Note: This preserves the session ID and conversation history; it only clears the visual terminal display. + +## Components +- `bootstrap.sh`: Initializes the environment. +- `orchestrator.py`: The background polling script. +- `clear_pane.sh`: Script to clear agent screen. +- `SKILL.md`: The skill definition. +- `comms/`: Communication directories. +- `agents/`: Isolated working directories for agents. + diff --git a/swarm_framework/SKILL.md b/swarm_framework/SKILL.md new file mode 100644 index 0000000..a9f3501 --- /dev/null +++ b/swarm_framework/SKILL.md @@ -0,0 +1,72 @@ +--- +name: Swarm Orchestration +description: Pattern for 1-to-many control of agent instances using tmux and session resumption. +--- + +# 1-to-Many Agent Swarm Orchestration Skill + +## Overview +This skill defines a pattern for an orchestrator agent to spawn, track, and control multiple sub-agent instances (a "swarm") using `tmux` for environment isolation and command-line interfaces supporting session resumption. + +## Prerequisites +- `tmux` installed on the host system. +- An interactive CLI or agent tool that supports: + - Running in a terminal. + - Exposing a unique Session/Conversation ID. + - Resuming sessions via targeting flags (e.g., `-conversation=""`). + +## Procedure + +### 1. Initialize Swarm Environment +Create a `tmux` session with multiple windows or panes, depending on the desired layout. + +**Windows Layout (Clean separation & Isolation):** +```bash +tmux new-session -d -s -n agent_1 -c /path/to/isolated/dir1 +tmux new-window -t -n agent_2 -c /path/to/isolated/dir2 +... +``` +Using `-c` with `tmux new-window` allows isolating the working directory of each agent. + +### 2. Spawn Agents +Send execution commands to the specific windows. + +```bash +tmux send-keys -t : "/path/to/cli --args" C-m +``` + +### 3. Record State +Store the mapping of `Window -> Session ID` if session resumption is needed. + +## File-Based Orchestration Framework + +To automate communication, use a file-based message bus. + +### Directory Convention +Create a directory `swarm_framework/` in the project root: +- `comms/requests/`: For dropping request files. +- `comms/responses/`: For reading results. +- `orchestrator.py`: The polling script. +- `bootstrap.sh`: The setup script. +- `agents/`: Contains isolated directories for each agent. + +### Payload Formats + +**Request File (`request_.json`):** +```json +{ + "request_id": "req_123", + "target_agent": "agent_1", + "prompt": "Your instruction here", + "response_file": "swarm_framework/comms/responses/res_123.json" +} +``` + +**Response File (`res_123.json`):** +```json +{ + "request_id": "req_123", + "status": "success", + "output": "... captured terminal output ..." +} +``` diff --git a/swarm_framework/agents/agent_3/hello.txt b/swarm_framework/agents/agent_3/hello.txt new file mode 100644 index 0000000..41fb21c --- /dev/null +++ b/swarm_framework/agents/agent_3/hello.txt @@ -0,0 +1 @@ +Hello from Agent 3! I am ready for tasks. diff --git a/swarm_framework/agents/master/SKILL.md b/swarm_framework/agents/master/SKILL.md new file mode 100644 index 0000000..7765482 --- /dev/null +++ b/swarm_framework/agents/master/SKILL.md @@ -0,0 +1,74 @@ +# Swarm Communication Skill + +## Overview +This skill allows you to communicate with and control other agents in the swarm using a file-based messaging system. + +## Communication Protocol +You communicate by writing JSON files to the requests directory and reading response files. + +### 1. Sending a Request +To send a message to a sub-agent, create a JSON file in the following location: +`../../comms/requests/req_.json` + +**JSON Schema:** +```json +{ + "request_id": "req_123", + "type": "prompt", + "target_agent": "agent_1", + "prompt": "Your instructions to the agent here.", + "response_file": "../../comms/responses/res_123.json" +} +``` + +* `request_id`: A unique identifier for the request. +* `type`: Must be `"prompt"` for sending instructions. +* `target_agent`: The name of the target window (e.g., `"agent_1"`, `"agent_2"`). +* `prompt`: The text message or command you want the agent to execute. +* `response_file`: The relative path where the Orchestrator will write the response. + +### 2. Reading a Response +The background Orchestrator will pick up your request, send it to the agent, and write the response to the specified `response_file`. +You should poll for the existence of the response file and read its content. + +**Response Schema:** +```json +{ + "request_id": "req_123", + "status": "success", + "output": "The raw terminal output from the agent." +} +``` + +### 3. Control Commands +You can also send control commands to create or kill agents. + +**Create Agent Example:** +```json +{ + "request_id": "req_create", + "type": "control", + "action": "create", + "target_agent": "agent_new", + "conversation_id": "optional_existing_conv_id", + "response_file": "../../comms/responses/res_create.json" +} +``` + +**Kill Agent Example:** +```json +{ + "request_id": "req_kill", + "type": "control", + "action": "kill", + "target_agent": "agent_1", + "response_file": "../../comms/responses/res_kill.json" +} +``` + +## Available Agents +You can see active agents by listing the windows in the `tmux` session, or assume `agent_1`, `agent_2`, etc., are available based on the setup. + +## Best Practices +- Always use unique `request_id`s. +- Clean up response files after reading them if you want to keep the directory tidy. diff --git a/swarm_framework/bootstrap.sh b/swarm_framework/bootstrap.sh new file mode 100755 index 0000000..955defa --- /dev/null +++ b/swarm_framework/bootstrap.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# Configuration +SESSION_NAME="jetski_swarm" +CLI_PATH="/google/bin/releases/jetski-devs/tools/cli" +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +ORCHESTRATOR_PATH="$SCRIPT_DIR/orchestrator.py" + +# Number of agents +N=${1:-4} + +cleanup() { + echo "Enforcing smooth start (cleaning up previous state)..." + tmux kill-session -t $SESSION_NAME 2>/dev/null + rm -f "$SCRIPT_DIR/comms/requests/"*.json + echo "Cleanup complete." +} + +# Smooth Start: Ensure clean state +cleanup + +echo "Bootstrapping swarm in background..." + +# Create session with Master Agent window +mkdir -p "$SCRIPT_DIR/agents/master" +tmux new-session -d -s $SESSION_NAME -n master -c "$SCRIPT_DIR/agents/master" +tmux send-keys -t $SESSION_NAME:master "$CLI_PATH -cli=true" C-m + +# Wait a few seconds for Master Agent to initialize, then inject skill instruction +sleep 5 +tmux send-keys -t $SESSION_NAME:master "Please read the file 'SKILL.md' in your current directory to understand how to control the swarm." C-m + +# Create Orchestrator window +tmux new-window -t $SESSION_NAME -n orchestrator -c "$SCRIPT_DIR" +# Run orchestrator with unbuffered output and redirection +tmux send-keys -t $SESSION_NAME:orchestrator "python3 -u $ORCHESTRATOR_PATH > orchestrator.log 2>&1" C-m + +# Create Grid window for sub-agents +tmux new-window -t $SESSION_NAME -n grid -c "$SCRIPT_DIR" + +# Add sub-agents to Grid window as panes +for i in $(seq 1 $N); do + mkdir -p "$SCRIPT_DIR/agents/agent_$i" + if [ $i -eq 1 ]; then + tmux send-keys -t $SESSION_NAME:grid "$CLI_PATH -cli=true" C-m + else + tmux split-window -t $SESSION_NAME:grid -c "$SCRIPT_DIR/agents/agent_$i" + tmux send-keys -t $SESSION_NAME:grid "$CLI_PATH -cli=true" C-m + fi +done + +# Arrange Grid window in tiles +tmux select-layout -t $SESSION_NAME:grid tiled + +# Wait for agents to settle and handle any prompts +echo "Aligning agents..." +python3 "$SCRIPT_DIR/../tmp/swarm/scripts/swarm_auto_aligner.py" + +tmux select-window -t $SESSION_NAME:master +echo "Setup complete." +echo "To use the Master Agent (Default Window), run:" +echo " tmux attach -t $SESSION_NAME" +echo "Or:" +echo " tmux attach -t $SESSION_NAME:master" +echo "" +echo "To use the Grid of Sub-Agents, run:" +echo " tmux attach -t $SESSION_NAME:grid" +echo "" +echo "To see the Orchestrator logs, attach to its window:" +echo " tmux attach -t $SESSION_NAME:orchestrator" +echo "Or check the file: $SCRIPT_DIR/orchestrator.log" +echo "" +echo "To view all windows:" +echo " tmux attach -t $SESSION_NAME" +echo "" +echo "To stop the swarm and cleanup:" +echo " tmux kill-session -t $SESSION_NAME" diff --git a/swarm_framework/clear_pane.sh b/swarm_framework/clear_pane.sh new file mode 100755 index 0000000..ac2cbba --- /dev/null +++ b/swarm_framework/clear_pane.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Script to clear the CLI screen for a specific agent in the swarm. +# This sends Ctrl+L to the target pane. + +SESSION_NAME="jetski_swarm" +TARGET=$1 # e.g., agent_1 + +if [ -z "$TARGET" ]; then + echo "Usage: $0 " + echo "Example: $0 agent_1" + exit 1 +fi + +# Send Ctrl+L +tmux send-keys -t $SESSION_NAME:$TARGET C-l +echo "Sent Clear Screen command (Ctrl+L) to $TARGET" diff --git a/swarm_framework/comms/responses/res_create_test.json b/swarm_framework/comms/responses/res_create_test.json new file mode 100644 index 0000000..8136a23 --- /dev/null +++ b/swarm_framework/comms/responses/res_create_test.json @@ -0,0 +1,5 @@ +{ + "request_id": "req_create_test", + "status": "success", + "output": "Created agent agent_test_c in window and directory." +} \ No newline at end of file diff --git a/swarm_framework/comms/responses/res_demo_1.json b/swarm_framework/comms/responses/res_demo_1.json new file mode 100644 index 0000000..3065500 --- /dev/null +++ b/swarm_framework/comms/responses/res_demo_1.json @@ -0,0 +1,5 @@ +{ + "request_id": "req_demo_1", + "status": "success", + "output": " hub/swarm_framework/agents/agent_1\n Model: Gemini Next\n Conversation: 9807f47f-fd0e-4293-bbec-758148efc5bb\n\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n > pwd \u2584\n \u2588\n \u25b8 Thought for 1s, 661 tokens \u2588\n The user has sent a request `pwd`. \u2588\n \u2588\n \u25cf Bash(pwd) (ctrl+o to expand) \u2580\nBash command\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n pwd\n\nDo you want to proceed?\n> 1. Yes\n 2. Yes, and always allow in this conversation for commands that start with 'pw\n 3. No\n\n \u2191/\u2193 navigate \u00b7 Tab amend\nEsc to cancel Gemini Next\n\n" +} \ No newline at end of file diff --git a/swarm_framework/comms/responses/res_demo_2.json b/swarm_framework/comms/responses/res_demo_2.json new file mode 100644 index 0000000..041b410 --- /dev/null +++ b/swarm_framework/comms/responses/res_demo_2.json @@ -0,0 +1,5 @@ +{ + "request_id": "req_demo_2", + "status": "success", + "output": " Workspace: ~/Projects/PersonalProject/cortex-\n hub/swarm_framework/agents/agent_1\n Model: Gemini Next\n Conversation: 9807f47f-fd0e-4293-bbec-758148efc5bb\n\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n > pwd \u2584\n \u2588\n \u25b8 Thought for 1s, 661 tokens \u2588\n The user has sent a request `pwd`. \u2588\n \u2588\n \u25cf Bash(pwd) (ctrl+o to expand) \u2588\n \u2588\n \u25b8 Thought Process \u2588\n The command `pwd` returned \u2588\n `/usr/local/google/home/jerxie/Projects/PersonalProje...\t\t\t\u2588\n The current working directory is: \u2588\n /usr/local/google/home/jerxie/Projects/PersonalProject/cortex- \u2588\n hub/swarm_framework/agents/agent_1 \u2580\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n >\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ? for shortcuts Gemini Next\n\n" +} \ No newline at end of file diff --git a/swarm_framework/comms/responses/res_kill_test.json b/swarm_framework/comms/responses/res_kill_test.json new file mode 100644 index 0000000..9541acc --- /dev/null +++ b/swarm_framework/comms/responses/res_kill_test.json @@ -0,0 +1,5 @@ +{ + "request_id": "req_kill_test", + "status": "success", + "output": "Killed agent agent_test_c (window)." +} \ No newline at end of file diff --git a/swarm_framework/comms/responses/res_resume_test.json b/swarm_framework/comms/responses/res_resume_test.json new file mode 100644 index 0000000..e07493b --- /dev/null +++ b/swarm_framework/comms/responses/res_resume_test.json @@ -0,0 +1,5 @@ +{ + "request_id": "req_resume_test", + "status": "success", + "output": "Created agent agent_resumed in window and directory. Resumed conversation 1192304b-dc36-4a6e-9ed3-fdc109d179a2." +} \ No newline at end of file diff --git a/swarm_framework/comms/responses/res_test_hang.json b/swarm_framework/comms/responses/res_test_hang.json new file mode 100644 index 0000000..bee01c0 --- /dev/null +++ b/swarm_framework/comms/responses/res_test_hang.json @@ -0,0 +1,5 @@ +{ + "request_id": "req_test_hang", + "status": "success", + "output": "\n \u25cf ListDir(/usr/local/google/home/jerxie/Projects/PersonalProject/corte...)\n (ctrl+o to expand)\n\n \u25b8 Thought for 1s, 100 tokens\n Okay, I see a `comms` directory in \t\t\n `/usr/local/google/home/jerxie/Projects/Perso...\n\n \u25cf ListDir(/usr/local/google/home/jerxie/Projects/PersonalProject/corte...) \u2584\n (ctrl+o to expand) \u2580\nFile access\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /usr/local/google/home/jerxie/Projects/PersonalProject/cortex-hub/swarm_fr...\n Reason: outside workspace\n\nAllow access to this file?\n> 1. Yes, allow access\n 2. Yes, and always allow non-workspace access\n 3. No, deny access\n\n \u2191/\u2193 navigate\nEsc to cancel Gemini Next\n\n" +} \ No newline at end of file diff --git a/swarm_framework/comms/responses/res_test_n.json b/swarm_framework/comms/responses/res_test_n.json new file mode 100644 index 0000000..186becf --- /dev/null +++ b/swarm_framework/comms/responses/res_test_n.json @@ -0,0 +1,5 @@ +{ + "request_id": "req_test_n", + "status": "success", + "output": " \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n > hello from agent 3 in isolated env\n hello from agent 3 in isolated env\n\n \u25b8 Thought for 1s, 403 tokens\n The user repeated the message twice this time. It seems like a test or a\n loop or...\n\n \u25cf ListDir(/usr/local/google/home/jerxie/Projects/PersonalProject/corte...) \u2584\n (ctrl+o to expand) \u2580\nFile access\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /usr/local/google/home/jerxie/Projects/PersonalProject/cortex-hub/swarm_fr...\n Reason: outside workspace\n\nAllow access to this file?\n> 1. Yes, allow access\n 2. Yes, and always allow non-workspace access\n 3. No, deny access\n\n \u2191/\u2193 navigate\nEsc to cancel Gemini Next\n\n" +} \ No newline at end of file diff --git a/swarm_framework/comms/responses/res_test_n2.json b/swarm_framework/comms/responses/res_test_n2.json new file mode 100644 index 0000000..4ed4858 --- /dev/null +++ b/swarm_framework/comms/responses/res_test_n2.json @@ -0,0 +1,5 @@ +{ + "request_id": "req_test_n2", + "status": "success", + "output": " > hello again from agent 3\n\n \u25b8 Thought for 1s, 309 tokens\t\n The user is repeating the message again. This might be a test of my\n persistence ...\n\n \u25cf Create(/usr/local/google/home/jerxie/Projects/PersonalProject/corte...)\n (ctrl+o to expand)\n\n \u25b8 Thought Process\n I have created the file. Now I will respond to the user.\n Hello again!\n\n I have created a file named hello.txt in my directory ( \t\t\t\n /usr/local/google/home/jerxie/Projects/PersonalProject/cortex- \t\t\n hub/swarm_framework/agents/agent_3/hello.txt ) to confirm that I can write\n to my workspace. \u2584\n \u2588\n I am ready for any tasks you have for me! \u2580\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n >\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ? for shortcuts Gemini Next\n\n" +} \ No newline at end of file diff --git a/swarm_framework/comms/responses/res_test_new.json b/swarm_framework/comms/responses/res_test_new.json new file mode 100644 index 0000000..8936cec --- /dev/null +++ b/swarm_framework/comms/responses/res_test_new.json @@ -0,0 +1,5 @@ +{ + "request_id": "req_test_new", + "status": "success", + "output": " \u25cf\n ListDir(/usr/local/google/home/jerxie\n /Projects/PersonalProject/corte...) \u2584\n (ctrl+o to expand) \t\u2580\n \u28fd Generating...\n \u25b8 hello from new framework\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n >\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Press up to edit queued messages\n\n" +} \ No newline at end of file diff --git a/swarm_framework/orchestrator.py b/swarm_framework/orchestrator.py new file mode 100644 index 0000000..18e19a1 --- /dev/null +++ b/swarm_framework/orchestrator.py @@ -0,0 +1,225 @@ +import os +import time +import json +import subprocess + +# Self-contained paths relative to this script +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +REQUESTS_DIR = os.path.join(BASE_DIR, "comms", "requests") +RESPONSES_DIR = os.path.join(BASE_DIR, "comms", "responses") +SESSION_NAME = "jetski_swarm" +CLI_PATH = "/google/bin/releases/jetski-devs/tools/cli" + +# State tracking +# { target: { "last_cmd_time": timestamp, "last_output_time": timestamp, "last_output": str } } +AGENT_STATE = {} +HANGING_THRESHOLD = 30 # seconds of no output change while busy +BUSY_THRESHOLD = 300 # seconds max execution time + +def get_active_agents(): + try: + cmd = f"tmux list-windows -t {SESSION_NAME} -F '#{{window_name}}'" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, check=True) + windows = result.stdout.strip().split('\n') + + agents = [] + for w in windows: + if w in ["master", "orchestrator"]: + continue + if w == "grid": + # List panes + cmd = f"tmux list-panes -t {SESSION_NAME}:grid -F '#P'" + res = subprocess.run(cmd, shell=True, capture_output=True, text=True, check=True) + panes = res.stdout.strip().split('\n') + agents.extend([f"grid.{p}" for p in panes]) + else: + agents.append(w) + return agents + except Exception as e: + print(f"Error listing agents: {e}") + return [] + +def check_hanging(): + agents = get_active_agents() + now = time.time() + + for agent in agents: + state = AGENT_STATE.setdefault(agent, { + "last_cmd_time": 0, + "last_output_time": now, + "last_output": "" + }) + + # Check if we recently sent a command + is_busy = (now - state["last_cmd_time"]) < BUSY_THRESHOLD + + print(f"Debug [check_hanging]: Agent: {agent}, is_busy: {is_busy}, now: {now}, last_cmd_time: {state['last_cmd_time']}") + + if is_busy: + # Capture output + try: + cap_cmd = f"tmux capture-pane -p -t {SESSION_NAME}:{agent}" + result = subprocess.run(cap_cmd, shell=True, capture_output=True, text=True, check=True) + current_output = result.stdout + + print(f"Debug [check_hanging]: Agent: {agent}, current_output len: {len(current_output)}, prev_output len: {len(state['last_output'])}") + + if current_output != state["last_output"]: + state["last_output"] = current_output + state["last_output_time"] = now + print(f"Debug [check_hanging]: Agent: {agent}, output changed.") + else: + # No change + silence_duration = now - state["last_output_time"] + print(f"Debug [check_hanging]: Agent: {agent}, silence_duration: {silence_duration}") + if silence_duration > HANGING_THRESHOLD: + print(f"Detected hanging agent: {agent}") + notify_master(agent, current_output) + # Reset to avoid spamming + state["last_cmd_time"] = 0 + + except Exception as e: + print(f"Error checking agent {agent}: {e}") + +def notify_master(agent, last_output): + print(f"Notifying master about hanging agent {agent}...") + # Extract last few lines of output + lines = last_output.split('\n') + last_snippet = "\n".join(lines[-5:]) if len(lines) > 5 else last_output + + message = f"\n[System] Warning: Agent {agent} seems to be hanging.\nLast output snippet:\n---\n{last_snippet}\n---\nWhat should we do?" + + try: + # Send keys to master pane + # We need to escape quotes in message + escaped_msg = message.replace('"', '\\"') + cmd = f"tmux send-keys -t {SESSION_NAME}:master \"{escaped_msg}\" C-m" + subprocess.run(cmd, shell=True, check=True) + except Exception as e: + print(f"Failed to notify master: {e}") + +def process_request(file_path): + try: + with open(file_path, 'r') as f: + req = json.load(f) + + req_id = req.get("request_id") + req_type = req.get("type", "prompt") + target = req.get("target_agent") + resp_file = req.get("response_file") + + # Mapping for Grid layout + if target.startswith("agent_"): + try: + agent_num = int(target.split("_")[1]) + target = f"grid.{agent_num - 1}" + print(f"Mapped {req.get('target_agent')} to {target}") + except ValueError: + pass + + print(f"Processing {req_id} (Type: {req_type}) for {target}...") + + output = "" + status = "success" + + if req_type == "prompt": + prompt = req.get("prompt") + + # Update state + state = AGENT_STATE.setdefault(target, {}) + state["last_cmd_time"] = time.time() + + # Send keys + cmd = f"tmux send-keys -t {SESSION_NAME}:{target} \"{prompt}\" C-m" + subprocess.run(cmd, shell=True, check=True) + + # Wait for agent to process + time.sleep(5) + + # Capture pane for response + cap_cmd = f"tmux capture-pane -p -t {SESSION_NAME}:{target}" + result = subprocess.run(cap_cmd, shell=True, capture_output=True, text=True, check=True) + output = result.stdout + + elif req_type == "control": + action = req.get("action") + print(f"Control Action: {action}") + + if action == "create": + agent_dir = os.path.join(BASE_DIR, "agents", target) + os.makedirs(agent_dir, exist_ok=True) + + conv_id = req.get("conversation_id") + cli_cmd = f"{CLI_PATH} -cli=true" + if conv_id: + cli_cmd += f" -conversation={conv_id}" + + # Create tmux window + cmd = f"tmux new-window -t {SESSION_NAME} -n {target} -c {agent_dir} \"{cli_cmd}\"" + subprocess.run(cmd, shell=True, check=True) + output = f"Created agent {target} in window and directory." + if conv_id: + output += f" Resumed conversation {conv_id}." + + elif action == "kill": + cmd = f"tmux kill-window -t {SESSION_NAME}:{target}" + subprocess.run(cmd, shell=True, check=True) + output = f"Killed agent {target} (window)." + + elif action == "clear": + cmd = f"tmux send-keys -t {SESSION_NAME}:{target} C-l" + subprocess.run(cmd, shell=True, check=True) + output = f"Sent clear screen to {target}." + + elif action == "resume": + conv_id = req.get("conversation_id") + cmd = f"tmux send-keys -t {SESSION_NAME}:{target} \"{CLI_PATH} -conversation={conv_id}\" C-m" + subprocess.run(cmd, shell=True, check=True) + output = f"Resumed conversation {conv_id} in {target}." + else: + status = "error" + output = f"Unknown control action: {action}" + + else: + status = "error" + output = f"Unknown request type: {req_type}" + + # Write response + resp_data = { + "request_id": req_id, + "status": status, + "output": output + } + + # Ensure target file directory exists + os.makedirs(os.path.dirname(resp_file), exist_ok=True) + + with open(resp_file, 'w') as f: + json.dump(resp_data, f, indent=2) + + # Delete request + os.remove(file_path) + print(f"Completed {req_id}") + + except Exception as e: + print(f"Error processing {file_path}: {e}") + +def main(): + print(f"Swarm Orchestrator started polling in {REQUESTS_DIR}...") + while True: + try: + # Check hanging agents + check_hanging() + + # Check requests + files = os.listdir(REQUESTS_DIR) + for file in files: + if file.endswith(".json"): + file_path = os.path.join(REQUESTS_DIR, file) + process_request(file_path) + except Exception as e: + print(f"Polling error: {e}") + time.sleep(2) + +if __name__ == "__main__": + main()