Agents talking to agents

Find out how to get agents to talk to each other -- simply, efficiently, and reliably

Communication is the backbone of organization. This is true of ants, of people, and, of course, of agents. The reason communication is so important is that it allows individuals to organize into groups with a shared purpose and to operate much more efficiently. An organization is much more (and more effective) than the sum of the individual parts. (Just look at any political or corporate entity and you'll see what I mean.) There's even a word -- synergism -- that describes this property of organizations. And it all begins with communication.

With this thought in mind, let's take a look at three mechanisms agents might use to communicate.

Communication mechanisms

Communication is just as important in the realm of software as it is in the realm of animal life. Whether the entities in question are threads, subroutines, processes, or agents, they usually find themselves in the position of needing to share data (or communicate) with others of their kind.

Consequently, the field of computer science has produced a number of mechanisms to enable communication. I've selected three of the most common. I'll describe them, present their strengths and weaknesses, and then explain why I eventually selected the mechanism I did.

The procedure call mechanism

The first mechanism is known as the procedure call mechanism. It came into being at almost the same time as the first modern programming languages. It's simple and effective. It works like this:

The procedure call mechanism

Assume entity A needs the services of entity B in order to carry out its purpose, and B in turn needs the services of entity C in order to fulfill its obligation to A.

Entity A begins by asking entity B to do something. It then waits for entity B to do it. Entity B works for a while and then realizes it needs something from entity C. It asks entity C for what it needs and waits for entity C to fulfill the request. As programmers we see this occur in all its glory whenever we call a procedure (or subroutine or function or method).

The procedure call mechanism is fast and (because of its sequential nature) makes it easy to follow a program's flow of execution. However, it's also inherently synchronous and difficult to parallelize.

Java's remote method invocation (RMI) system is based on the procedure call mechanism.

The callback mechanism

Now we'll shift to the other end of the spectrum and look at the callback mechanism. It works like this:

The callback mechanism

This scenario is similar to what we saw with the procedure call mechanism; however, instead of asking for something and then waiting for it, entities A and B ask for something and then continue on with their tasks. When entities B and C have done whatever they were asked to do, they call back the original caller and give it what it asked for. The original caller must stop what it's doing and deal with the callback. If you've ever written windowing code, you've used callbacks before.

The callback mechanism is useful because it permits truly asynchronous processing. However, it's also complicated, and makes the program's flow of execution more difficult to follow.

Java's GUI toolkits (AWT and Swing) are based on the callback mechanism.

The mailbox mechanism

Somewhere between these two extremes lies the mailbox mechanism. It works like this:

The mailbox mechanism

Here, entity A asks entity B to do something and tells entity B to put the finished result in its mailbox. Entity A then goes about its business, checking the mailbox periodically to see if entity B is finished. Or entity A may simply stop and wait for entity B to finish its part, as in the procedure call mechanism. Entities B and C interact in the same way.

The mailbox mechanism is more difficult to implement than either of the other two mechanisms; however, it allows for asynchronous processing while at the same time avoiding the problem of the confusing flow of execution. These two factors turn out to be very important in distributed systems.

And the winner is...

For our agent communications, I've decided to go with the mailbox mechanism. Why mailboxes? First, the mailbox method avoids hiding the flow of execution. In a distributed system, with processing often spread across a number of machines in a network, tracing the flow of execution is already difficult. Just trying to debug a system like that can make you rethink your decision to become a developer! Callbacks only make it more difficult.

Second, agents are, by their very nature, autonomous, independent, distributed software entities. In a network environment, with its inherent delays, agents can't afford to wait around for something they need at some future point.

While there are other reasons why I believe the mailbox mechanism is the right choice for the job, I think the previous two are the most compelling.

Before I move on, let's take a look at how the mailbox mechanism will work within our agent environment.

First, the sending agent creates a message containing the arguments it wishes to pass to the recipient agent. Next, it contacts the recipient agent and passes it the message. The recipient agent then provides the sending agent with a token called a message response, which it uses to peek into the mailbox to see if its request has been answered. Once it has, the sending agent uses the message response to retrieve the information.

Handling the communication

Agents rely on two APIs provided with the Java class library to handle messages: the Reflection API and the Serialization API.

The Java Reflection API allows a Java application to inspect the methods (and the fields) a class provides, and determine what arguments the methods require. Using this information, a Java application can compare the message to the available methods a class provides to locate one that can handle the message. The JavaBeans framework uses this mechanism to tie events fired by one object to the event handlers defined on another object. We'll use it to locate a message handler on an agent.

Java's Serialization API provides a seamless, robust, reliable mechanism for turning Java objects and primitive types (int, char, long, for example) into a stream of data, sending it through a pipe, and re-creating the originals on the other end. The API handles all the details of byte ordering, multiple references, and a host of other problems. We'll use it to take care of moving the data between agent hosts.

The agent context

We've established the foundation of an agent communication system. I've explained how agents communicate. The only remaining issue is who an agent communicates with.

Let's step back a few paces and look at the agent environment one more time.

Two months ago, you learned that agents lived in agent hosts. Agent hosts take care of the details involved in moving agents around, and they provide them with access to the resources and services that enable agents to do their job. However, agent hosts have to provide these resources and services without giving agents carte-blanche access to the innards of the application (via a reference to the agent host itself, for example). One way to manage these two goals is by providing the agent with a reference to another class that provides only limited access to specific resources. I call such a class an agent context.

Every agent has an agent context -- a window into the environment in which it lives. One of the resources the agent context provides is access to an agent registry. The agent registry is like the yellow pages -- a listing of services and the agents that provides those services.

The details

Last month I presented the Agent class. We must now modify it so that subclasses can access the agent context.

abstract public class Agent { /** * The agent context. * * The agent context provides an agent with an interface into the * environment in which it exists. The agent host sets the agent * context when the agent is transferred. * */

private transient AgentContext agentcontext = null;

/** * Gets an agent's agent context. * */

protected final AgentContext getAgentContext() { return agentcontext; }

. . . }

An agent context specific to an agent host is created whenever an agent is transferred. That's why the agentcontext field is marked transient. The AgentContext class looks like this:

class AgentContext { /** * The agent host. * */

private AgentHost agenthost = null;

/** * Constructs the agent context. * */

AgentContext(AgentHost agenthost) { this.agenthost = agenthost; }

/** * Send a message. * * Returns the agent response for the message. * */

public MessageResponse sendMessage(AgentIdentity agentidentity, Message message) { return new MessageResponse(agenthost, agentidentity, message); } }

An agent calls the sendMessage method to send a message to the agent specified by the AgentIdentity instance. The Message class encapsulates a message. It captures the message name and the list of parameters (the message) to pass to the recipient.

public final class Message implements Serializable { /** * The name. * */

String strName = null;

/** * The parameter array. * */

Object [] rgobject = null;

/** * Constructs a message. * */

public Message(String strName, Object [] rgobject) { this.strName = strName; this.rgobject = rgobject; } }

Most of the message-passing activity occurs in the MessageResponse class, which creates a new thread within the Java virtual machine where this activity takes place. The sendMessage call appears to return immediately to the caller.

public final class MessageResponse implements Serializable { private boolean boolFinished = false;

private Object objResponse = null;

MessageResponse(final AgentHost agenthost, final AgentIdentity agentidentity, final Message message) { Threadthread = new Thread() { public void run() { Response response = null;

response = agenthost.sendMessage(agentidentity, message);

objResponse = response.objResponse;

boolFinished = true; } };

thread.start(); }

/** * Gets the response. * */

public Object getResponse() { return objResponse; } }

The agent host actually takes care of delivering the message to the agent by using the Reflection API to look up a method that matches the supplied name and invoking the matching method with the parameter array. The agent host returns the result. Here's the method that must be added to the AgentHostImplementation class from last month:

class AgentHostImplementation extends UnicastRemoteObject implements AgentHost { . . .

/** * Sends a message. * */

public Object sendMessage(AgentIdentity agentidentity, Message message) { Agent agent = (Agent)hashtableAgents.get(agentidentity);

Method [] rgmethod = agent.getClass().getMethods();

for (int i = 0; i < rgmethod.length; i++) { if (rgmethod[i].getName().equals("handle" + message.strName)) { rgmethod[i].invoke(agent, message.rgobject);

break; } } } . . . }

For an agent to receive a particular message, it must define a method named handle plus the message name. For example, to receive a message named "GoHome", an agent must define a handleGoHome method that takes the proper arguments -- in this case, perhaps a string containing the hostname of the agent's home.

The good news

Those of you who've been following this series of columns on agent technology may have been wondering when you'd get your hands on live source code. I'm happy to say, the wait is over. The source code (and compiled class files) are available on my Web site. Because some of the source code has been taken from one of my commercial products (don't worry, it's all legal), I decided it would be best if I made the code available from my site rather than from JavaWorld's. Surf on over and pick up you're copy of Ki for JavaWorld!

Conclusion

We're almost to the end of our journey. I've taught you how to make agents move and how to make them talk. Pretty practical stuff. Next month we'll get to the heart, or rather, the brains of the matter. I'll show you how to smarten up your agents by adding a bit of artificial intelligence to the mix. See you then!

Todd Sundsted has been writing programs since computers became available in convenient desktop models. Though originally interested in building distributed object applications in C++, Todd moved on to the Java programming language when it became the obvious choice for that sort of thing. In addition to writing, Todd is president of Etcee, which offers Java-centric training, mentoring, consulting, and development.
1 2 Page 1
Page 1 of 2