Chapter 7. The Agent Task Model

Table of Contents

7.1. About this chapter
7.2. Why use the Task Model?
7.3. How the task model is implemented?
7.3.1. Task States
7.3.2. Asynchronous actions
7.3.3. The Agent heartbeat
7.4. The Task Model API
7.4.1. The Task interface
7.4.2. Task State
7.4.3. Events and Listeners
7.4.4. AgentContext
7.4.5. The Directory
7.4.6. Derived task classes
7.5. Programming with tasks
7.5.1. Common tasks
7.5.2. Finding Agents
7.5.3. Messaging
7.5.4. JNDI Groups
7.5.5. Statistics
7.5.6. Other tasks

7.1. About this chapter

This chapter explains the task model which describes how an agent executes various tasks. It behaves differently from typical Java code, therefore requiring further explanation. The following sections explain the reasoning for using this particular task model. The subsequent section explains how these considerations affect implementation of the task model. Additionally, interfaces, classes and models that enable you to get an agent to perform a task or series of tasks are explained.

In some cases, tasks can be executed simultaneously, that is, in no particular order and with more than one executing at a time. While one task is being executed, another can be started, provided it is unrelated to the other task. You can compare this to sending an email, and then making a phone call while you are waiting for the email to be answered.

In other cases, tasks must be executed in a defined order. First, this means that some task cannot start before another is finished. Second, it means that the result of one task may influence which task is executed next. In other words, successful completion of one task may be a pre-condition for starting another one.

7.2. Why use the Task Model?

The reasoning for using the task model is based on three considerations:

  1. Tasks should be executed independent of each other as much as possible. This is referred to as asynchronous processing. The reasoning behind this is that tasks that do not need to wait for each other can be executed faster.

  2. Tasks should be tailored to process messages easily. Virtually all behavior of agents is defined in terms of sending and receiving messages, rather than calling methods.

  3. Tasks should be organizable in a robust way. This means that success or failure of a task can be detected and used as a condition for executing other tasks.

7.3. How the task model is implemented?

Generally speaking, the agent keeps track of which tasks are active and interested in certain events. A task defines what to do with an event, depending on the state of the task. A task may update the agent's internal state, or execute an action like sending a message. After the action has been scheduled, the task remains dormant until the next event arrives. A task may finish. It is then removed from the agent's behavior. Execution of tasks is done either in parallel or serially (scheduled tasks). By default, tasks are executed in parallel. The agent detects the completion of a task and, upon completion, removes it.

When a certain sequence of actions is required, tasks can be organized in a scheduler. In that case, tasks execute one at a time, making it possible to control when (and whether) a certain action will take place. This requires checking the task's state.

7.3.1. Task States

Each task has its own internal task state, which can be used to see if the task has started, is active or has finished. When the task finishes, its state will become either success or failure. You can check these end states to determine which task to execute next or to stop executing tasks altogether.

7.3.2. Asynchronous actions

Communication between agents occurs asynchronously. For example, this means that when some request is made to move an agent to another location, the task that requests the service regains control before the action is actually performed. Of course, we need to be careful not to engage in any activity that assumes that the action has taken place. It follows that the task must check if the move has already been completed.

Because the signal of completion is also sent as a message, any further action needs to be delayed until the expected message comes in. In the meantime, however, other messages could come in that are not expected, and these need to be dealt with. Consequently, some kind of control mechanism is required to process incoming messages.

7.3.3. The Agent heartbeat

Two types of events trigger task execution. The first type of event is an incoming message sent by another agent. This is comparable to the event model of Java. The other type of event is the task simply executing itself of its own free will, whenever it finds time. A task gets control occasionally (once every few milliseconds) in the form of an event during which the agent may perform some action.

This agent clock tick is referred to as the agent's heartbeat. Upon every heartbeat, the agent needs to check what to do next. When the agent releases control, for instance, to send a message, it does not know where to continue when the expected answer comes in, and the expected response might never come.

The agent therefore needs to keep track of its internal state and must remember what it was waiting for. To prevent an agent from entering a never-ending loop it should also check if a timeout has happened on events it was waiting for.

7.4. The Task Model API

The task model can be found in the package tryllian.afc.task. In this package, you will find the building blocks needed to write tasks. It contains the following classes and interfaces.

Table 7.1. Task Model package tryllian.afc.task

Task Core
TaskFramework for agent behavior.
TaskStateState information of a task.
AgentContextInformation about the agent containing the task.
AbstractTaskBase implementation of task.
DefaultTaskTask that schedules subtasks in parallel. Only receives messages if they are part of a conversation it is registered to.
ReactiveTaskTask that schedules subtasks in parallel. Receives all incoming messages.
Scheduling
TaskSchedulerTask that schedules subtasks in a specified order.
SchedulingRuleDetermines which task is executed next in a TaskScheduler.
DefaultSchedulingRuleDefault rule contains one task for success and one for failure of the previous task.
TaskGroupTask that allows you to execute a number of tasks in parallel. It succeeds when all tasks have succeeded, and fails if one task fails.
Events and Listeners
TaskListenerGets notified if and when tasks start and finish.
TaskEventSent by tasks when they start and finish.
MoveListenerGets notified when the agent tries to move, has moved or failed to move.
MoveEventSent by tasks when they move.

In the following subsections, the contents of the package are explained in further detail. Also, refer to the javadoc which is bundled with the ADK and also found on the Tryllian Developer Website at http://www.tryllian.com/development.

7.4.1. The Task interface

The Task interface is the external interface of all tasks. As seen later, the interface is implemented in a class called AbstractTask, which in turn has two direct extended classes, DefaultTask and TaskScheduler. The last two classes are used by developers, in addition to the ReactiveTask, which extends the DefaultTask. For now let's focus on the Task interface, which consists of the methods below. For your convenience, the methods are organized by logical functionality groupings.

Table 7.2. Task interface (in tryllian.afc.task)

Execution
heartbeatReceived()Called when the task receives a heartbeat to process.
messageReceived()Called when the task receives a message to process.
State
getState()Reports the state of the task.
addTaskListener()Adds a TaskListener, which listens for TaskEvents to occur.
removeTaskListener()Removes a TaskListener.
forceFinish()Forces the task to finish. Whether the result is failure depends on your implementation, but is recommended.
Properties
getAgentContext()Gets information about the Agent to which this task belongs.
getFinishValue()Gets the result of the successfully finished task, or a reason for failure.
Composition
addTask()Adds a subtask to this task.
getSubtasks()Gets all subtasks for this task.
getSupertask()Gets the task of which this is the subtask, or null if none exists.
taskAdded()Called when this task has been added to a supertask.
taskRemoved()Called when this task had been removed from a supertask.

The Task interface allows you to implement the following types of methods:

  • Execution: These methods define when certain tasks are executed, either triggered by a heartbeat or by an incoming message.

  • State: These methods allow you to read and manipulate a task's state.

  • Properties: These methods provide access to the properties of the agent the task is plugged into, and to the results of the executed task.

  • Composition: These methods allow you to schedule tasks in an organized way.

7.4.2. Task State

A task can be in various states, stored in the property task state (accessible through the getState() method of the Task interface). The organization of a task's states is as follows:

Figure 7.1. Task State diagram

Task State diagram

As you can see, the task can be in the following states:

  • Just Created: The task has been created, but has not been scheduled yet.

  • Active: The task is currently executing.

  • Going to succeed: An administrative task state in which the task prepares to succeed.

  • Going to fail: An administrative task state where the task prepares to fail.

  • Success: The task has succeeded.

  • Failure: The task has failed.

For your convenience, there are also two derived states:

  • Idle: Any task state except Active.

  • Finished: Either the task state Success or the task state Failure.

These states are the cornerstones of task programming. You can view a task as an asynchronous method. Just like a method, a task promises to do something. But unlike ordinary methods, tasks are executed asynchronously. The task state can be read to find out if a task is still busy or if it has finished. Discerning between successful and failed completion of a task allows robust programming schemes. A task that completes with success can be seen as a method that has returned with all its postconditions fulfilled. A task that fails is like a method that throws an exception.

An active task can be stopped by calling the forceFinish() method of the Task interface. The expected behavior is that the task fails. But it may also be the case that the task decides all conditions for success are fulfilled so it will succeed.

The task state may be manipulated more directly from within the task by calling the succeed() or fail() methods of the AbstractTask class, which will, of course, set the task state to Succeeded or Failed, respectively.

The program needs to be alerted of changes in the task's state. This can be done using task scheduling, as explained later, but it can also be done by listening to events. This is the subject of the next section.

7.4.3. Events and Listeners

7.4.3.1. TaskListener

It can be useful to implement the TaskListener interface to listen to tasks ending or starting. The TaskListener interface consists of two methods, taskStarted() and taskEnded(). Adding a TaskListener to a task, using the addTaskListener() method, will cause these methods to be called when the task starts and ends, respectively.

7.4.3.2. TaskEvent

The TaskListener is triggered, specifically, by a TaskEvent. This TaskEvent gives you access to the actual task itself through its getTask() method.

7.4.3.3. MoveListener, MoveEvent

One of the most frequent and basic tasks to be performed by an agent is moving from one location to another. Therefore, the task of moving has already been implemented in the ADK.

Tasks can move the agent by calling getAgentContext().requestMove(). Tasks can get notified of movements by implementing MoveListener and registering themselves as such in the agent context. The MoveListener interface contains three methods:

Table 7.3. MoveListener interface (in tryllian.afc.task)

moveRequested()The agent is about to move. Tasks should end conversations.
moveFailed()The agent could not move.
moveSucceeded()The agent moved successfully.

7.4.4. AgentContext

Each task belongs to a certain agent. Information that is the same for all tasks in an agent can be found in the agent context. This context is stored in an object of class AgentContext that can be referenced by calling getAgentContext(). However, it is important to realize that this property does not exist when the task is in the state Just Created, because in that state, the task is not part of an agent yet. In practice, this means that you cannot call getAgentContext() in the constructor of a task (it will return null). The first opportunity to call this method is in taskStarted().

The agent context contains default properties, custom properties and hooks to request and listen to movements of the agent. Here is a simple overview of the type of information that may be retrieved from the agent context (a full list of methods may be found in the javadoc):

MethodDescription
getName()The name of the agent.
getMyAddress()The agent's address.
getDefaultLoggingChannel()Logging channel agents use to log activities.
getMessageFactory()The mechanism for creating and replying to messages.
getProperty()An agent property from the map that can be used by all tasks in the agent.
getDirectory()The directory that provides a lookup service for other agents and habitats.

7.4.5. The Directory

The tryllian.afc.task.find.Directory is obtained by calling getDirectory() on the AgentContext. It offers a lookup service for agents, services and habitats. The Directory contains methods for obtaining sets of agents in a certain habitat. You can find agents that advertise a certain service and get a set of available habitats.

The table shows its most important methods, see the javadoc for a full list.

MethodDescription
getHabitats()Returns the set of all known habitats.
getLocalAgents()Returns the addresses of all agents in a habitat.
getAgents()Returns a set containing the addresses of all agents in the local habitat.
getServiceAgent(String serviceName)Returns the address of the agent that advertises a certain service anywhere in the habitat.
getNamingContext()Returns the JNDI naming context this Directory instance is a wrapper for.

The Directory implementation uses the JNDI implementation of the ARE and facilitates most common JNDI lookups. It is not intended to completely hide or replace JNDI functionality offered by the ARE. If you want to make full use of the features offered by JNDI, you can query the Tryllian ADK Naming Context directly by calling getNamingContext() of the Directory. The structure of the returned JNDI context is described in the JNDI section of the ARE Specifications document.

7.4.6. Derived task classes

The task interface has a basic implementation in the form of an AbstractTask. This derived class has two extended classes, DefaultTask and TaskScheduler. The DefaultTask has an extended ReactiveTask class itself. Schematically:

Figure 7.2. Diagram of Derived Task Classes.

Diagram of Derived Task Classes.

We will now explain these derived classes in detail.

7.4.6.1. AbstractTask

AbstractTask is the abstract implementation of a task, without subtask behavior. In addition to Task, this implementation defines methods that can be used from within a Task, but do not need to be in the external interface. As a developer of agents, you would not usually use subclass AbstractTask directly, unless you wanted to perform some unusual type of task scheduling. Here are AbstractTask's interesting methods:

Table 7.4. AbstractTask interface (in tryllian.afc.task)

State Control
succeed()Set task state to succeeded
fail()Set task state to failed
Events
taskStarted()Is called when the task has started.
taskEnded()Is called when the task has finished.

Subclasses can call succeed() or fail() to change the state of the task to going to succeed and going to fail respectively. Events are sent when the state has changed. In these methods, you can pass the finish value of the task as an argument.

A subclass gets notified of its own task events before its listeners are called. The methods taskStarted() and taskEnded() are the callbacks that are made just before the related task events are sent. Note that these methods are protected and will not take an argument. In contrast, listeners are notified by the public method taskStarted(TaskEvent event). These methods are called when a task becomes active. It is guaranteed that they will be called before the task receives any messages or heartbeats.

It is important to realize that all initialization code of a task should be put in taskStarted() instead of in the constructor. This is because a task may be scheduled again once it has finished, or, in other words, it is reusable. If the task is scheduled a second time, the constructor would not be called, but taskStarted() will be.

AbstractTask has two implementations: DefaultTask and TaskScheduler.

7.4.6.2. DefaultTask

This class extends AbstractTask. DefaultTask has two extra methods: handleHeartbeat() and handleMessage(). These methods are to be filled in by you, the ADK developer, to define the task's proactive behavior (handleHeartbeat()) and reactive behavior (handleMessage()).

Note

A defaultTask, by default, does not receive any messages; it must listen to a conversation in order to do so.

A conversation is a collection of messages sharing a conversation ID. An agent, let's say its name is Alice, can create a conversation using the createConversation() method in the AgentContext interface. The first message Alice sends to another agent, say, Bob, will now have a new conversation ID. Alice can now reply to this message, and the reply will automatically have the same conversation ID. The conversation framework also allows for listening in on conversations. All this is explained in Agent Communication (Chapter 9, Behind Agent Communication).

However, DefaultTask has an extended class called ReactiveTask, which receives all messages. Here, you must add a message filter (using the setMessageFilter() method) to filter out those messages that are interesting to this task. This, too, is also explained in Agent Communication (Chapter 9, Behind Agent Communication).

7.4.6.3. TaskScheduler

The TaskScheduler is a special type of task that can be used to organize tasks in a logical manner.

As explained earlier, in some cases, it is highly desirable to organize tasks in a logical way. We want the success or failure of a task to trigger the execution a specific task and to build a network of interconnected tasks that we can organize any way we like. To do this, we use a special implementation of the Task interface called TaskScheduler.

A TaskScheduler is a task, so it may contain subtasks. Of these subtasks, only one is active at any one time. The TaskScheduler allows you to specify the order in which tasks are executed and supports any control flow that can be specified using a state diagram.

When you add a task to the task scheduler, you can specify explicitly which task should be invoked when this task succeeds, and which one should be invoked when it fails. Alternatively, you can provide a scheduling rule, which allows you to write your own code to retrieve the next task for success and for failure.

The decision to retrieve a new task is usually based on the state of the previous task (failed or succeeded), but it can be any condition. The next task should be one of the subtasks in the scheduler, or the end state. If it is the end state, this means that the task scheduler, itself also a task, will finish. Since a task scheduler itself is a task, you can nest task schedulers to encapsulate complex behavior.

Here is an overview of the TaskScheduler class and its methods:

Table 7.5. public class TaskScheduler extends AbstractTask (in tryllian.afc.task) TaskScheduler (Task[] tasks) creates a new TaskScheduler and adds all tasks in the given array to it.

Scheduling
addTask(Task)Adds a task to the TaskScheduler with the default scheduling rule. If this task finishes, the scheduler finishes as well. The subtask's end state dictates the end state of the scheduler: if the subtask had succeeded, the scheduler succeeds. If the subtask had failed, the scheduler fails.
addTask(Task, SchedulingRule)Adds a task and scheduling rule to TaskScheduler.
addTask(Task, Task)Adds a task (first parameter) and the task to be scheduled after it finishes, regardless of success or failure (second parameter).
addTask(Task, Task, Task)Adds a task (first parameter), the task to be scheduled in case of the first task's success (second parameter), and the task to be scheduled in case of the first task's failure (third parameter).
removeSubtask(Task)Removes a subtask.
setFirstTask(Task)Identifies a subtask as the first to be executed.
getActiveSubtask()Returns the currently active subtask.

The scheduling is implemented through a number of signatures of the addTask() method. This method's first parameter always specifies the task to be added; the other parameters, if any, determine what happens after it finishes. Usually, you would use the addTask(Task, Task, Task) signature. It is also necessary to specify which task the scheduler should execute first. Use the setFirstTask() method for this. You can also remove added tasks, using the removeSubtask() method, and to get the currently active task with getActiveSubtask().

7.4.6.4. SchedulingRule

The SchedulingRule in the second addTask() method is an interface with two methods that both return a task:

  • getNextTaskForSuccess(): Invoked if the subtask succeeded.

  • getNextTaskForFailure(): Invoked if the subtask failed.

To create a custom rule, implement the interface (as an inner class for example) and add it along with a task. It is only possible to schedule a task that has been added. If a scheduling rule selects a task that has not been added, a NoSuchTaskException is thrown.

7.4.6.5. DefaultSchedulingRule

If a task is added and no scheduling rule is provided, either explicitly, or implicitly through extra tasks as parameters, the DefaultSchedulingRule is invoked. The DefaultSchedulingRule implements one task for success, and one for failure. You would not usually use the DefaultSchedulingRule class, since addTask(Task, Task, Task) allows you to do the same thing.

Example 7.1. Task scheduling

You can define complex task behavior by drawing up state diagrams and implementing it using TaskSchedulers. Refer to the diagram below:

Figure 7.3. ExampleScheduler.

ExampleScheduler.

This diagram depicts a TaskScheduler called ExampleScheduler. DoSomething and MoveSomewhere are subtasks. DoSomething is the first task that becomes active. If it succeeds, ExampleScheduler also succeeds. If it fails, MoveSomewhere is scheduled. If the agent moved successfully, it tries to do something again and we are back in the task DoSomething. If the agent could not move, the ExampleScheduler fails.

It is very easy to imagine how this implementation could be used to create an agent that could search a habitat for a certain piece of information. The above example can be translated easily to Java code using the TaskScheduler:

import tryllian.afc.task.*;

public class SchedulerExample extends TaskScheduler {

    // Subtasks
    private final Task doSomething   = new DoSomething();
    private final Task moveSomewhere = new MoveSomewhere();

    public SchedulerExample () {

        addTask(doSomething, null, moveSomewhere);
        addTask(moveSomewhere, doSomething, null);

        setFirstTask(doSomething);
    }
}

7.5. Programming with tasks

It is important to realize that the concept of asynchronous messaging demands that you adhere to several strict rules. For instance, suppose you want your agent to move to another habitat. You need to find out if this task of moving has been executed successfully. Normally, it would sound logical to enter a loop that checks regularly if the requested event has happened. However, because of the way tasks are implemented, you should instead set up a state machine that remembers that the agent is moving and that checks with every incoming message if this is the signal for completion of the move.

Compare this to installing a piece of software. You could start the install procedure and watch attentively as the progress bar fills up. On the other hand, you could also discuss something with a colleague and wait for a popup window to appear, informing you of successful installation. The ADK task model resembles the second approach.

With every heartbeat, the agent checks how many ticks the agent has waited. If the move has not been completed within a certain number of ticks, a timeout exception is thrown. Following the analogy, if the popup window has not appeared after 15 minutes, and the software should take only 5 minutes to install, you would take action.

7.5.1. Common tasks

For your convenience, the AFC contains standard implementations for the most common tasks. They can be found in the packages tryllian.afc.task.* (see the javadoc for further information).

These tasks can be found in tryllian.afc.task.standard.

  • CheckpointTask stores the current state of the agent.

  • CloneAwarenessTask is notified when the agent is cloned or is a new clone.

  • CloneTask duplicates the agent.

  • CreateAgentTask dynamically creates a new agent.

  • DebugTask A task that prints interesting agent events such as incoming and outgoing messages, movements, upgrades etc. to a separate logfile. Intended as a debugging aid.

  • DelayTask waits a specified number of heartbeats.

  • DieTask kills the agent.

  • IterateCollectionTask iterates over a collection of values. Each time you start this task, it will produce the next item of the collection.

  • LogTask prints a message to System.out and succeeds.

  • MoveTask moves the agent to another habitat.

  • PeriodicalTask periodically starts the wrapped task.

  • PingTask pings a remote habitat.

  • QueryCertificateTask Gets a list of certificates for the specified agent.

  • QueryHabitatPropertiesTask Gets the collection of all habitat properties.

  • QueryHabitatPropertyTask Gets the value of a specific habitat property.

  • RegisterTask registers the agent with the specified service name.

  • ReloadPermissionPolicyTask A wrapper task for the ARE reload-permissionpolicy protocol.

  • RepeatTask repeats success a number of times.

  • RestoreAwarenessTask is notified when an agent has been restored after a habitat shutdown.

  • ReturnHomeTask moves the agent back to the location where it was created

  • SetHabitatPropertyTask Sets the value of a habitat property.

  • SuspendTask suspends the agent until it receives a new message.

  • TaskRetryTask retries a specified task a specified number of tasks.

  • TimeoutTask waits for a specified task to finish, or until a specified amount of time has passed. Whichever case happens first, causes this task to finish.

  • UnregisterTask unregisters the agent.

  • UpgradeAgentTaskUpgrades an existing agent.

  • UpgradeAwarenessTask is notified when an active agent has been upgraded.

  • WaitMillisTask waits for a specified number of milliseconds.

  • WaitTask waits for a specified number of heartbeats.

  • WakeupAwarenessTask is notified when an agent has been woken up from suspension.

7.5.2. Finding Agents

The following Tasks, in the tryllian.afc.task.find package, provide a way to look up agents and services and retrieve information about them. Apart from these Find tasks, the AFC contains the tryllian.afc.task.find.Directory interface to easily access local JNDI data using method calls. Thus, the Directory provides an easier way to perform local lookups than the task mentioned below, but is less general because it cannot be used to perform lookups on a remote habitat.

  • FindAgentNamesTask retrieves the names of all agents in a habitat.

  • FindAgentsTask retrieves the addresses of all agents in a habitat.

  • FindHabitatServiceTask retrieves the address of the Habitat Service Agent.

  • JNDIResolveTask resolves a jndi context-name.

  • QueryAgentNameTask asks an agent for its name.

  • TrackAgentsTask is notified when an agent arrives or departs.

7.5.3. Messaging

These methods, from tryllian.afc.task.interaction, can be used for messaging. See chapter on agent communication (Chapter 9, Behind Agent Communication) for more information.

  • QueryTask sends a query message and waits for a reply.

  • RequestTask sends a request message and waits for a reply.

  • SendAndReceiveTask sends a message and waits for a reply message.

  • SendMessageTask sends a message.

  • QueryReplyTask replies to queries made by other agents.

  • RequestReplyTask replies to requests made by other agents.

  • SubscribeTask sends a subscribe message and waits for a reply.

  • SubscribeTask abstract class that handles a complete subscription, including notifications.

7.5.4. JNDI Groups

Agents can create and delete contexts in JNDI user space. Contexts can be hierarchically ordered and are garbage collected when they become empty. If all agents bound to a certain context are persisted, the context is not assumed to be empty. Agents can bind and unbind themselves in a context, and retrieve a directory of any context.

Most of the JNDI functionality is accessible through the Directory interface. However, as a replacement for the old Rooms concept, the more general Groups concept has been introduced in the package tryllian.afc.task.group. Using this package, agents can create (hierarchical) Groups, find the available Gropus, join and leave them, find the agents in a Group, and delete them.

  • CreateGroupTask create a new group

  • DeleteGroupTask delete a group (including its subgroups)

  • FindAgentsInGroupTask get the addresses of agents that joined a given group.

  • FindGroupsTask get the groups currently present in the local habitat

  • JoinGroupTask join a group

  • LeaveGroupTask leave a group

7.5.5. Statistics

The tryllian.afc.task.statistics package contains tasks for getting statistics information about agents or the habitat.

  • AgentStatisticsTask obtains statisticak information about an agent.

  • HabitatStatisticsTask obtains statistical information about the habitat.

  • HabitatStatisticsListenerTask a task that subscribes to the habitat statistics and gets notified when new statistical information arrives.

7.5.6. Other tasks

The subpackage, tryllian.afc.swing, contains tasks that are not often used. SwingAgentTask (in tryllian.afc.swing) implements agents within the class of a Swing application. It propagates all messages to a SwingAgentInterface. See the Javadoc for more information.