This Developer's Guide describes the design and implementation of the Open Agent Architecture (OAA), and provides a description of how to create agents within this framework.
This document assumes a general familiarity with software engineering practices and programming language concepts. Some familiarity with the basics of the Prolog language is also helpful, as Prolog serves as the basis for OAA's Interagent Communication Language (ICL). Section 3.5, Notes on Terminology, provides some perspective on the terminology and notations that have been adopted from Prolog. For those wanting some background information on Prolog, a brief tutorial can be found at http://cbl.leeds.ac.uk/~tamsin/prologtutorial/, a longer tutorial is at http://www.intranet.csupomona.edu/~jrfisher/www/prolog_tutorial/contents.html and an online book is at http://cbl.leeds.ac.uk/~paul/prologbook/.
Sections 2 and 3 of this document give an overview of the design objectives and architectural features of the OAA. Section 4 explains the architectural elements of the OAA in greater detail, so as to provide the developer with a clearer understanding of how these elements fit together. Each of the remaining chapters focuses on one particular aspect of OAA agent programming, and presents detailed explanations of how to accomplish the most important and most common programming objectives.
For an exhaustive and detailed listing of library procedures, consult
the OAA Reference Manual.
The term "agent-based programming" has come to mean many things in today's technological jargon. Within the OAA framework, agents are seen as independent processes which communicate and cooperate with each other across a distributed network. We think of agents as more than just distributed objects because of their high-level communication language and their abilities to actively contribute to a computation as opposed to being only passive participants.
Although the goals and concepts for the Open Agent Architecture have been developed in detail elsewhere (OAA Specification Report, OAA Definition Report), it will be helpful to briefly restate some of the design objectives of the architecture here.
The OAA focuses on the idea of a community of agents working together to solve tasks for the user. Although it is possible in principle to create a single agent whose role is to autonomously accomplish all envisioned tasks, the benefits of the agent-based approach are best realized when computation is spread over many specialized "expert" agents. In the OAA framework, most requests from users are handled by the combined effort of multiple agents (for example, participating agents may include those that understand natural language input, plan actions, access databases, display results, etc.). This imposes a requirement that communication among agents be efficiently implemented, so as not to incur unacceptable overhead in executing a task.
The community of agents should be diverse, allowing agents to run on whatever platforms they choose, and to be written in any number of programming languages. The OAA provides a set of standard conventions which allows agents to work together under these conditions. In addition, distributed computation opens the door to parallel computation, where multiple agents may work either cooperatively or competitively on various aspects of a task.
As new members join the community, the overall effect of their interactions should change. If some agent joins the collaborative process as a latecomer, the interactions among agents must be flexible enough to allow the new agent to participate in computations. A "plug & play" architecture allows systems built with preexisting agents to easily take advantage of new functionality added later in the form of a new agent or agents.
Since human users are expected to be participants in the collaborative agent experience, the Interagent Communication Language (ICL), however defined, must be powerful enough to represent natural language (human) input. If the ICL can represent full natural language expressions, procedural (programmatic) interactions will also be possible.
Agents are more than just passive data sources that perform actions
or return information only when requested. Agents should be able
to monitor actions in the world around them and decide when to take
action, perhaps alerting a user or group of other agents about
some pertinent situation. In addition, agents should be able to
watch the interactions of other agents, and perhaps make suggestions
about how to do something better.
Figure 1 presents the basic structure of the Open Agent Architecture, using several agents from the Office Assistant prototype as examples of typical agents. The configuration shown in Figure 1 is composed of a multimodal user interface agent that analyzes input and coordinates the presentation of multimedia output to the user; a collection of domain agents for various office-related tasks; and a specialized server agent - the facilitator agent - that is responsible for coordinating agent communication and control. In many systems, the facilitator is also used to provide a global data store to its client agents. Note that a system configuration is not limited to a single facilitator; connectivity between multiple facilitators is discussed later in this document (see Using Multiple Facilitators).
Figure 1: OAA structure
Each agent in the OAA is either a facilitator agent or a client agent. Client agents are so called because each acts (in some respects) as a client of some facilitator, which provides communication and other essential services for the client. When invoked, a client agent makes a connection to a facilitator, which is known as its "parent facilitator". Upon connection, an agent informs its parent facilitator of the services it is capable of providing. When the agent is needed, the facilitator sends it a request using the Interagent Communication Language (ICL). The agent parses this request, processes it, and returns answers or status reports to the facilitator. In processing a request, the agent can make use of a variety of capabilities provided by the OAA. For example, it can request services of other agents, set triggers and read or write shared data on the facilitator (or other client agents that maintain shared data).
A facilitator maintains a knowledge base that records the capabilities of a collection of agents, and uses that knowledge to assist requesters and providers of services in making contact.
At the heart of the facilitator's functionality is a very general notion of transparent delegation. This means that a requesting agent can generate a request, and a facilitator can manage the satisfaction of that request, without the requester needing to have any knowledge of the identities or locations of the satisfying agents. At the same time, it is possible for a requesting agent to specify various forms of advice, or constraints, to guide the facilitator in handling its request. Transparent delegation is possible because agents' capabilities (solvables) are treated as an abstract description of a service, rather than as an entry point into a library or body of code.
A number of key declarations and other program elements are specified using ICL expressions. These include capabilities declarations, events (communications between agents), requests for services, responses to requests, trigger specifications, and shared data elements.
Additional details regarding the ICL appear in The Interagent Communication Language.
In starting up an entire system, the only general requirement to remember is that each facilitator must be started up before its client agents. More precisely, the facilitator must be listening to its assigned port before any of its clients attempt to make a connection.
However, because of the heterogeneity of implementation languages, platforms, and origins that is likely to be present among the agents of a system, there can be a fair amount of complexity involved in starting up a system, and in keeping in running. To alleviate some of the effort involved, OAA includes an execution manager, StartIt.
Once a collection of interoperable agents has been assembled to work on a set of tasks, StartIt provides the means of invoking each of the agents on the correct platform, according to the system protocols of that platform, and ensuring that the agent makes the required connection to an OAA facilitator. Of equal importance, StartIt monitors the status of each agent to see that it continues to function correctly. In the event that StartIt detects a failure of one of the agents, it is able to take steps to recover from the failure and automatically restart the agent.
The use of StartIt is described in more detail here.
One goal of the OAA is to facilitate the creation of agents using a wide variety of implementation languages. This Developer's Guide is intended for use by developers who are working with any of the languages currently supported. However, this creates a considerable challenge in selecting the appropriate terminology to use in referring to programming concepts.
The OAA has roots in Prolog programming, and some of the concepts of Prolog (in particular, unification and backtracking) are employed by the OAA. Consequently, the developer should have some understanding of these concepts, and these terms are used wherever appropriate in this Guide.
It is helpful to distinguish between general design concepts and implementation issues. In discussing design concepts, this manual employs general terminology that is clearly not related to any programming language. For example, we say that agents ``provide services'' that can be ``requested'' by other agents.
In discussing implementation issues, a careful mixture of Prolog-specific terminology and language-neutral terminology has been selected. In cases where Prolog terminology helps to characterize some OAA feature that is inherently Prolog-like, we use that terminology. For example, we say that an agent can ``solve a goal'' (or ``solve a query'') to emphasize first, that the goal must unify with one of the agent's capabilities specifications, and second, that the goal can have multiple solutions.
In a few other cases, we have adopted Prolog or logical notation simply because there is nothing language-neutral that is equally clear or convenient. For example, we use the notation foo/3 to refer to a procedure foo with 3 arguments, and we refer to foo itself as the functor of the procedure.
Otherwise, insofar as possible, we have tried to use terminology that is widely familiar and can be considered to be programming language-neutral. For example, instead of the Prolog term ``predicate'', we use ``procedure'' or ``declaration'', depending on context.
When describing library procedures, we sometimes characterize their arguments with the symbols '+', '-', and '*', as in this example:
oaa_Solve(+Goal, +Params, -Solutions)
The '+' means that the corresponding argument is an input
argument (bound to some value at the time of calling the procedure),
the '-' means it is an output argument (not bound when calling,
but expected to be bound when the procedure returns), and the '*'
means the argument can be used for either input or output.
The agent library provides functionality that is common to all agents in the OAA, including facilitator agents; in a sense, the capabilities provided by the agent library determine what it means to be an OAA agent. The library has been ported to a number of different programming languages, and the inclusion of the library is part of the implementation of every agent. The agent developer needs to call procedures provided by the library, and needs to provide certain declarations and define certain callback procedures that are expected by the library. These procedures and declarations are described throughout this Developer's Guide. This section, on Agent Infrastructure, describes some basic features of the agent library, and some uses of the library that are common to all agents.
To date, all OAA-based systems have employed TCP/IP (Transmission Control Protocol / Internet Packets) as transport protocol. TCP is standardized on many operating systems (UNIX, Macintosh, DOS, Microsoft Windows, and so forth), so its use facilitates the task of interoperating agents on a wide variety of platforms.
It should be noted, however, that OAA has been structured so as to allow the use of a variety of transport protocols. This has been done by specifying an API for the transport layer. The transport layer is handled as a distinct module, and loaded separately from the remainder of the library procedures. Procedures in the transport layer are named using the prefix com_. All communications between agents go through this API. In no case does the developer need to be concerned with any of the details of TCP/IP, or any other transport protocol.
With the exception of com_Connect, the agent developer does not normally need to call procedures in the transport layer. Calls to these procedures are usually made by higher-level procedures, such as oaa_Solve.
For those familiar with speech-act-based agent frameworks such as FIPA or KQML, the ICL includes a layer of conversational protocol, similar in spirit to that provided by KQML, and a content layer, analagous to that provided by KIF. The basis of the ICL in logic programming allows for a consistent integration of these two layers, while at the same time providing considerable flexibility in how they are used.
The conversational layer of the ICL is defined by the event types, together with the parameter lists that are associated with certain of these event types. The content layer consists of the specific goals, triggers, and data elements that may be embedded within various events.
While it is possible to embed content that is expressed in other languages within an ICL event, it is advantageous to express content in the ICL wherever possible. The primary reason for this is to allow the facilitator access to the content, as well as the conversational layer, in delegating requests. Not only does this give the facilitator more information about the nature of a request, but it also makes it possible for the facilitator to decompose compound requests, and delegate the subrequests individually.
In the following sections, we briefly explain how the ICL is used in each of its different roles.
One aspect of a solvable that is worth mentioning up front is its type, which may be either procedure, data, or trigger. Procedure and data solvables declare capabilities that may be used directly by other agents. (Normally, other agents use these solvables by calling the library procedure oaa_Solve.)
Conceptually, a procedure solvable accomplishes an action, whereas a data solvable stores a set of facts (just as a database relation does). For example, in creating an agent for a mail system, procedure solvables might be defined for sending a message to a person, listing messages that have arrived about a particular subject, or displaying a particular message onscreen. For a database wrapper agent, one might define a distinct data solvable corresponding to each of the relations present in the database. Often a data solvable is used to provide a shared data store, which may be not only queried, but also updated, by a number of agents having the required permissions.
Although many of the OAA library procedures for requesting services apply to both procedure and data solvables, there are also a number of library procedures that are exclusively for use with data solvables, as explained in Section 7, Maintaining Data.
Trigger solvables are not directly accessible by other agents. Rather, other agents use trigger solvables indirectly, by setting task triggers on them (as explained in in Section 8, Using Triggers). A trigger solvable exists just for the purpose of explicitly declaring task-specific conditions or events, on which the agent is able and willing to allow triggers to be set. For example, an email agent might declare a trigger solvable that checks for the arrival of a message for some person, about some subject. This tells other agents (and the facilitator) that triggers can be installed on the agent, which fire when a condition of that form is satisfied.
Note that there are several types of triggers, of which task is only one. However, task is the only trigger type that requires the declaration of trigger solvables. An agent declares its solvables by calling library procedures such as oaa_Declare and oaa_Undeclare.
These procedures, and the precise form of the solvable, are given in section 5, Providing Services.
For example, a call to
oaa_Solve(Goal, Params)
within some agent A, results in an event having the form
ev_solve(GoalId, Goal, Params)
going from A to the facilitator, and a return message having the form
ev_solved(GoalId, Requestees, Solvers, Goal, Params, Solutions).
(In addition, between the occurrence of these two messages, there are
other messages between the facilitator and the solver(s) of the request.)
Because events are normally constructed and transmitted transparently, the developer of an agent is not generally concerned with their details, except for those events that correspond to requests for the services provided specifically by that agent.
The OAA libraries provide an agent with a single, unified point of entry for requesting services of other agents: the library procedure oaa_Solve. In the style of logic programming, oaa_Solve may be used both to retrieve data and to initiate actions. To put this another way, calling a data solvable looks the same as calling a procedure solvable.
oaa_Solve provides a number of different parameters that may be used to modify the behavior of the facilitator and/or other agents in performing the requested service. In particular, it is the address parameter that allows for the explicit delegation of a call to one or more agents. It is also possible for an agent to use oaa_Solve "locally"; that is, as a convenient and consistent means of accessing the agent's own capabilities. This is done by giving the calling agent's own address as the value of the address parameter.
Section 6, Requesting Services, provides full details on the use of oaa_Solve.
Also, as in logic programming, it is not necessary to be explicit about types. That is, in declaring a solvable, the types of its arguments need not be specified. However, starting with release 2.3.0, it is possible to specify argument types, using the scheme of ICL data types explained here.
Section 5.2 explains how these data types may be used in characterizing the arguments of solvables. We emphasize that type specifications for solvables are optional.
atomic. Supertype of number and string.
number. Supertype of float and integer.
string. Supertype of atom, icldataq(_), and icldataq(_,_,_).
compound. Each value of this type is a "structure"; i.e. a functor with
1 or more arguments.
atom. Each value of this type is an ordinary single-quoted string.
(To be more precise, values of this type may always be enclosed within
single quotes, but for strings meeting certain requirements, the
single quotes may be omitted.)
float.
integer.
list. Each value of this type is a list containing elements (which may
be of any type). (Note that when declaring a list, it is not possible
to constrain the type of the list elements.)
icldataq(_). Subtype both of compound and of string.
This type is declared using a variable in the argument
position. A
value of this type has the same form, with the single argument
instantiated to a double-quoted string. (See icldataq documentation
elsewhere.)
icldataq(_,_,_). Subtype both of
compound and of string.
This type is declared using variables in each of the argument
positions. A
value of this type has the same form, with each argument
instantiated appropriately. (See icldataq documentation elsewhere.)
icldataq(_), or icldataq(_,_,_).
If icldataq(_) or icldataq(_,_,_) is specified, that indicates that
in a value of the document type, the document itself must be carried in that
icldataq format. If it's a variable, the document can be represented
using any subtype of string.
document - supertype of xml(_,_) and mime(_,_).
xml(XSDSpec, _XMLDocument).
In specifying an XML document type,
XSDSpec may be an atom giving a URL for an XML Schema declaration. It
may also be a variable, which indicates that the type of the XML document is unconstrained.
The
second argument may be a variable,
icldataq(_), or icldataq(_,_,_).
A value of this type has the same form. As with the XML document type specification, its first argument may be an atom giving a URL for an XML Schema declaration, or a variable. Its second argument must contain a string that conforms to the specified XML Schema declaration indicated by the first argument (if the first argument is bound).
mime(MimeType, _MimeDocument).
In specifying a Mime document type,
MimeType may be an atom giving the name of a mime type; such as
' text/plain' and 'text/x-gene-expression+xml'. It
may also be a variable.
MimeDocument can be a variable,
icldataq(_), or icldataq(_,_,_).
A value of this type has the same form. Its first argument gives the name of a mime type, or a variable. Its second argument must contain a string that conforms to the specified mime type indicated by the first argument (if the first argument is bound).
document types are subtypes both of compound and
of document.
OAA triggers provide a general mechanism by which an agent can specify an action to be taken when some set of conditions is met. Each agent can install triggers either locally, on itself, or remotely, on its facilitator or a peer agent. Triggers fall into four categories: communication, data, task, and time triggers.
Triggers are related to solvables in two ways. First, all triggers are implemented as data solvables, declared implicitly for every agent. That is, when a trigger is installed on an agent, the agent library records the trigger as an instance of a built-in data solvable, oaa_trigger/5. This makes it possible for an agent to query currently installed triggers, using oaa_Solve, just as it would query any other data solvable.
Second, task triggers require the declaration of a trigger solvable, as explained in Declarations of Capabilities (Solvables).
The use of triggers is described in Section 8, Using Triggers.
[type(data), single_value(true), persistent(true)]
As illustrated by this example, all parameters are specified using a
functor (such as type), and one or more arguments. Most, but not
all, take only a single argument. Most, but not all, are boolean,
where the argument's value is either true or false.
Many parameters have a default value; for example, the default value of parameter type/1 is procedure. When you want a parameter to have its default value, it is not necessary to specify the parameter at all. In fact, when you call a library procedure, such as oaa_Solve, and your parameter list includes default values, these are normally removed before the request goes to the facilitator, to conserve bandwidth. However, there is certainly no harm in specifying default values, and at times you may wish to do so to produce more readable code.
A boolean parameter with value true can omit the value specification. For example, the parameter list above could also be written as:
[type(data), single_value, persistent]
To retrieve a parameter's value from a parameter list, call the library procedure icl_GetParamValue. For example, in Prolog, given a parameter list Params, you can retrieve the value of the from parameter in this fashion:
icl_GetParamValue(from(Requestor), Params)
In Java, this can be coded as follows:
value = iclGetParamValue(IclTerm.fromString("from(Requestor)"), Params)
The relevant parameters in each context, and their meanings and default values, are described in the appropriate sections of this manual and the reference manual.
An agent may have one or more open server connections ("listener" sockets in TCP), and one or more open client connections. Normally, the facilitator maintains a single server connection, and each client agent maintains a single client connection to its facilitator (which is obtained by calling com_Connect, followed by oaa_Register).
Every agent (including facilitators) has a symbolic name, a full address, and a local address (or "local ID"), for each open client connection and each open server connection. The form of the full address is dependent on the communications protocol in use. For example, assuming the use of TCP/IP, a facilitator's address will appear as
addr( tcp(Host, Port) ),
and the address of a client agent will appear as
addr( tcp(Host, Port), LocalID).
Even though it doesn't appear in the full address, a facilitator also has a local ID, for consistency -- but this isn't normally used by a client agent. The local ID of a client agent is assigned to it by its facilitator, which passes the client's full address to the client at connection time (that is, during the execution of the client's call to oaa_Register).
Since a facilitator must have exactly 1 (server) connection with connection id 'fac_listener', and a client (non-facilitator) must have exactly 1 (client) connection with connection ID 'parent' (although they may have other connections with other connection ids), we have the notion of a "primary address". The primary address for a facilitator is the full address for its fac_listener connection, and for a client is the full address for its parent connection.
After a client agent has registered with its facilitator, it can obtain its full address by calling
oaa_PrimaryAddress(MyAddress)
and the full address of its facilitator can be obtained by calling
oaa_Address(parent, _, ParentAddr)
Full addresses are globally unique, and local addresses are unique with respect to a facilitator. Symbolic names are not unique in any sense. The local ID happens to be an integer in the current implementation, but developers should not rely on this.
When specifying addresses, in address/1 parameters for calls to oaa_AddData, oaa_Solve, etc., either names or addresses may be used. In addition, for convenience, reserved terms self, parent, and facilitator may also be used.
More precisely, the value of an address parameter can be one of the following, or a list containing any number of the following:
When calling oaa_AddData, oaa_Solve, etc., address parameters are standardized as follows: local IDs are changed to full addresses, relative to the 'fac_listener' connection (for a root or node agent), or to the 'parent' connection (for a leaf). Full addresses are left as is. Names are left as is, but ensuring they're enclosed within name/1. 'self', 'parent', and 'facilitator' are changed to the appropriate full address.
The addresses of an agent's peer agents can be obtained by calling the facilitator solvable can_solve. For example, to find the address of a peer agent that can solve Goal, an agent would ask for solutions to can_solve from its parent facilitator:
oaa_Solve(can_solve(Goal, AgentAddress), [address(parent)])
which returns addresses
for agents whose solvable list matches the desired Goal.
There are also several other means of obtaining addresses of other
agents:
by calling the facilitator's solvable agent_data/6; by
retrieving the value of the from/1 parameter to a callback procedure; or by the use of the
get_address/1 and get_satisfiers/1 parameters, which may
be used with a number of different library procedures.
oaa_RegisterCallback(app_idle, ProcedureName)
Library code calls this procedure, if registered,
with no arguments. (Note: the
app_idle callback is not used in the Java library.)
oaa_RegisterCallback(app_done, ProcedureName)
or, in Java:
oaa.oaaRegisterCallback("app_done", listener)
When an agent is quitting, library code calls this procedure, if registered,
with no arguments.
oaa_RegisterCallback(app_setup_trigger, ProcedureName)
or, in Java:
oaa.oaaRegisterCallback("app_setup_trigger", listener)
When a task trigger is installed, library code calls this procedure
with 4 arguments: Type, Condition, Action, Params. These
arguments are the same as those passed to oaa_AddTrigger, by the agent requesting the
trigger installation.
fac -oaa_connect "tcp('mymachine.ai.sri.com', 3345)" ...
Some Java agents -- in particular, agents constructed using the WebL library --
may not check command line arguments. In this case, use the "-D"
property specification described next.
tcp('mymachine.ai.sri.com', 3345)
Java agents don't normally have access to environment variables. The
appropriate alternative to use with a Java agent is setting the
OAA_CONNECT property on the command line:
-DOAA_CONNECT="tcp('mymachine.ai.sri.com', 3345)"
oaa_connect( tcp('mymachine.ai.sri.com', 3345)).
(Note, however, that oaa_connect should be used with care in a setup file. Another alternative, default_facilitator, works better for most situations.)
When creating new invocation arguments for specific client agents, developers are encouraged to follow these same conventions. Library procedure oaa_ResolveVariables may be used to search the command line, environment, and setup file.
The agent library contains code that loads a setup file if it is present in any of the following locations:
A setup file is loaded when library procedure oaa_ResolveVariables is called for the first time, with setup/3 specified in its argument list.
default_facilitator(tcp(HostName, PortNumber)).
oaa_AddData(Clause,[address(self)])for each data predicate declared.
try_again(10.0).
next_highest(100).the facilitator will try listening on each port numbered 3500 - 3600, until one is available, or it has tried them all without success (in which case it exits). In the absence of the integer argument, the default value of 100 will be used.
try_again(10.0).
next_highest(100).the agent will try connecting to each port numbered 3500 - 3600, until it successfully connects, or has tried them all (in which case it exits).
For this reason, when it is desired to specify a connection address in a setup file, it is usually best to use
default_facilitator(tcp(HostName, PortNumber)).
which specifies the OAA address at which a facilitator
agent may be found. This predicate will be used by a client
agent to find a Facilitator agent to connect to, and if read
by a Facilitator agent, will be interpreted as the communication address
at which it should listen.
Currently, tcp is the only communication protocol used with OAA
agents. In this case, PortNumber is expressed as an integer,
and HostName as a Prolog atom, as in the following example:
default_facilitator(tcp('acapulco.ai.sri.com', 3333)).
Note that when Facilitator agents are arranged hierarchically in a multi-facilitator topology, the use of default_facilitator in a setup file is not appropriate, unless it is known that each facilitator will read a different setup file, or will get its address information from the command line or environment.
Although connection addresses are usually fully specified for a
Facilitator agent, it is possible to specify a variable for either
the host or port. In this case, acceptable values will be
automatically chosen by the system.
solvable(GoalTemplate, Parameters, Permissions)
Any permissions or parameters taking a default value (see Parameter Lists) may be omitted, so it is often
the case that a solvable's permissions and/or parameters are just the
empty list. (In these cases, several shorthand forms allow for briefer and
more readable solvables.)
For example, the solvable list for a simple mail agent might look like this:
[solvable(send(mail, ToPerson, Msg), [callback(send_mail)], []),
solvable(last_message(MessageNum),
[type(data), single_value(true)],
[write(true)]),
solvable(get_message(MessageNum, Msg), [callback(get_mail)], [])
]).
This simple agent offers three solvables. The first, a
procedure solvable (by default), results in sending an email
message to the person indicated in the second argument,
ToPerson. The second, a data solvable, provides the
numeric index of the last message received. The third, a
procedure solvable, when given a numeric index, returns the
message associated with that index.
For those readers with limited knowledge of Prolog, we note that ToPerson, MessageNum, and Msg are variables, whereas the other argument values (mail, data, true and false) are constants.
In the case of the goal template send/3, the variable ToPerson is expected to be bound when the solvable is called, whereas in the goal template last_message/1, MessageNum provides a return value; that is, free when the solvable is called, and bound when the solvable returns. This distinction is not apparent from the solvable specifications, however; it is only known by considering the meaning and context, or by reading the comments for the solvables.
The goal template of a solvable provides, at a conceptual level, a description of the service (or data) provided; at a functional level, a specification that the agent's facilitator can use in making delegation decisions; and at the level of communications, a template for the event which the agent will receive when the service is requested.
When writing a solvable goal template, it is important to understand that each goal template specification will be used according to the semantics of unification, just as it is used in logic programming. When the facilitator receives a request to solve some goal, it uses unification to test that request against the goal templates contained in the solvables specifications of its connected agents. (Solvables permissions and parameters do not figure in the unification, only goal templates.) By default, each connected agent for which the unification succeeds receives the request. The event that expresses this request, as sent to the agent by the facilitator, and passed to the agent's callback procedure, is the result of this unification.
A solvable's permission list may contain the following boolean permissions. In each case, the meaning of the permission applies to all agents ( including the one declaring the solvable). Although this may be considered counterintuitive in some respects, improvements have been deferred until a more comprehensive permissions scheme is developed.
In the example solvable list above, the first and third solvables need not specify any permissions, because they use only the default values, which are call(true), write(false), read(false). The second solvable, last_message, specifies the non-default permission write(true), to allow updates to the data associated with this solvable. It is very common to use write(true) with data solvables. A solvable's permissions can be modified at anytime.
There are a number of parameters that may be used to characterize a solvable. Of these, the most important are type/1, utility/1, and callback/1.
The value of type/1 can be either data, procedure, or trigger; the default is procedure. These three types are informally characterized in Section 4.3.1, Declarations of Capabilities (Solvables).
As explained there, procedure and data solvables can both be accessed by calling oaa_Solve. Technically, there are three specific differences between these two types of solvables. First, a data solvable can be updated using library procedures oaa_AddData, oaa_RemoveData, and oaa_ReplaceData. Second, it is not necessary (or possible) to define a callback procedure for a data solvable. Finally, for a data solvable, the agent library maintains a record of which facts are created by which agents, and uses this information to remove an agent's facts when the agent goes offline. (This behavior can be modified using solvable parameters bookkeeping/1 and persistent/1.)
The value of utility/1 can be any integer between 0 and 10; the default is 5. This parameter provides a means of giving a qualitative indication of how well the agent performs the service implemented by the solvable, with larger numbers expressing higher quality of service. This number is used by the agent's facilitator, when multiple agents offer the same solvable. When a facilitator receives a request for such a solvable, it orders the available solvers by decreasing utility, which means the agents claiming higher utility will receive the request first. Or, if the request is accompanied by certain combinations of parameters (see the strategy(action) parameter to oaa_Solve), the agent claiming the highest utility may be the only one to handle the request.
The value of callback/1 is an ICL expression that names a user-defined procedure. The use of this procedure is described below, in Defining Request Handlers.
An exhaustive list of solvables parameters, with their possible values and default values, is given in the Reference Manual. General characteristics of parameter lists are explained in Section 4.3.6, Parameter Lists.
Specifications of solvables have a standard form and several alternative shorthand forms. The shorthand forms are provided primarily for backwards compatibility with earlier versions of the agent library. Each library procedure that takes a solvable list as an argument will accept any of these forms, so that developers' calls to these procedures may be made more readable, in some situations. In addition, it is always acceptable to pass in a single solvable, instead of a list. When the solvable or solvable list is passed in using one of the shorthand forms, it is converted to standard form. Thus, when debugging their code, developers may see the results of this conversion.
The differences between standard and shorthand forms are straightforward. In standard form, each solvable term contains all three arguments:
solvable(GoalTemplate, Parameters, Permissions)
and default values are omitted from Permissions and
Parameters.
Shorthand forms are obtained simply by omitting the second and/or
third arguments. That is, if Permissions is the empty list, it
may be omitted. If both Permissions and Parameters
are empty (this can only happen with a procedure solvable,
using the default callback), they may both be omitted. In this case,
it is also acceptable to omit the solvable functor.
For example, this solvable list:
[solvable(get_message(MessageNum, Msg), [], [])]
could be passed to a library procedure in any of the following forms:
[solvable(get_message(MessageNum, Msg), [], [])]
[solvable(get_message(MessageNum, Msg), [])]
[solvable(get_message(MessageNum, Msg))]
solvable(get_message(MessageNum, Msg), [], [])
solvable(get_message(MessageNum, Msg), [])
solvable(get_message(MessageNum, Msg))
get_message(MessageNum, Msg)
A call to oaa_Register is the means by which a client (that is, non-facilitator) agent normally registers with a facilitator, and informs the facilitator of its solvables. oaa_Register should be called after opening a communications channel to the facilitator, using com_Connect. Every client agent is required to connect and register with a facilitator to participate in a community of agents. However, it is not required that the agent specify its solvables when it registers. If an agent wishes to specify its solvables later, it may do so using oaa_Declare.
If an agent has no solvables to declare when it connects, it may pass the empty list ([]) as the third argument to oaa_Register.
The AgentName argument is the symbolic name by which the client agent will be known to its facilitator and other agents (see Agent Names and Addresses).
A call to oaa_Declare may be used at anytime after connecting to a facilitator. Each solvable in Solvables is added to the agent's list of current solvables, both locally and in the agent's facilitator (unless private(true) appears as a parameter, in which case the facilitator does not learn of it).
A call to oaa_Undeclare results in the removal of each previously declared solvable that is listed in Solvables.
This library procedure may be used to simultaneously remove one solvable, and install another. Conceptually this is the same as a call to oaa_Undeclare followed by a call to oaa_Declare, with the appropriate arguments, except that oaa_Redeclare ensures the two operations take place at the same time. This is especially useful when changing the permissions or parameters associated with an existing solvable.
In addition to declaring solvables for itself, an agent can ask its facilitator to declare solvables of type data; this is done by including address(parent) in the Params argument. When this is done, the result is a shared data predicate that can be read and written by all clients of the facilitator. This is one way of implementing a "blackboard" approach to communication and coordination between agents.
When an agent receives a request for one of its services declared as a procedure solvable, the callback procedure for that service is called. To handle these calls, each agent must define code that implements the callback procedure of each solvable it has declared of type procedure. (Callbacks for data solvables are provided transparently by the agent library, and trigger solvables have no need of callbacks.)
As explained in the preceding section, a callback procedure for a solvable can be specified in the parameter list of the solvable's declaration. It is also possible to declare a default app_do_event callback for procedure solvables, using oaa_RegisterCallback. When this is done, any request for a procedure solvable, which has no specified callback, will go to the default callback.
In Prolog, callback procedures, are assumed to be defined in module user, unless some other module is explicitly indicated. This may be done using the standard syntax Module:CallBack; for example, callback(my_module:my_procedure).
The callback procedure is always called with two arguments: the incoming goal to solve, and a parameter list. The return value(s) of the callback procedure are the instantiated solutions to the goal. In Prolog, these are returned by the usual means, using backtracking to provide various bindings of the goal's variables.
For example, for the solvables declared above, in a Prolog implementation, the callbacks would be defined as follows:
send_mail(send(mail, ToPerson, Msg), Params) :-
<code implementing the send solvable ...>
get_mail(get_message(MessageNum, Msg), Params) :-
<code implementing the get_message solvable ...>
Either or both of these solvables could have had their goals handled by the default app_do_event callback, if one has been declared. Regardless of whether you choose to use a default callback for any (or all) of your solvables, it is sometimes necessary to define a default callback for handling some events other than those associated with solvables. For instance, Section 6.8 describes how it may be used to intercept ev_solved/6 events returned asynchronously from other agents.
Notice that there is no callback defined for the last_message solvable, because it is a data solvable.
In general, when defining a callback procedure, the parameter list argument may be ignored; that is, the goal argument contains the essential information for satisfying the incoming request. However, occasionally it may be useful to know what agent has made this particular request. This may be determined by retrieving the value of the from parameter; for example, in Prolog:
send_mail(send(mail, ToPerson, Msg), Params) :-
icl_GetParamValue(from(Requestor), Params),
....
In the agent libraries for languages such as C, Delphi or Visual Basic, because those languages do not provide backtracking, the conventions for returning multiple solutions from a callback procedure are slightly different than for Prolog.
In the C, Visual Basic and Delphi libraries, the callback procedure returns success, failure or multiple solutions to the incoming request in the variable answers. For an event request send(mail,'Adam', Msg), a successful action would return "[send(mail,'Adam', Msg)]" in answers, and failure would be indicated by returning the empty solution list "[]". Multiple solutions can be returned as well: the query manager(adam,X) could return two solutions by storing "[manager(adam,jerry),manager(adam,doug)]" in answers.
Beginning with release 2.3.0, OAA allows for optional type declarations to be given with solvables. A type declaration characterizes each of a solvable's arguments, in terms of ICL data types and binding constraints. (In a future release, it may also be possible to work with "user-defined" data types). When this information is present, the facilitator uses it in its matchmaking operations.
To declare types associated with a solvable, include an argspecs term in the solvable's parameter list:
argspecs(ArgSpec1, ArgSpec2, ..., ArgSpecN)
where N is the number of arguments of the solvable's goal template, and
each ArgSpec is one of the following:
in(TypeSpec, Required?)
out(TypeSpec, Deterministic?)
inout(TypeSpec, Deterministic?)
A variable may also be given as a type spec, meaning that arguments in that position can be of any type.
When argspecs isn't present, every argument is assumed to be declared as inout(_, false); that is, any type allowed and not required (input arguments) or deterministic (output or inout arguments). This conforms with current practice of the facilitator.
Note that with respect to compound types, there are now two places in which they may be declared. One is the traditional means of specifying the compound type skeleton within the goal template of the solvable. For instance, the goal template
send(Recipient, mime('text/plain', _))
indicates that the type of the second argument must be
mime('text/plain', _). This same type indicator could also be used
within an argspec. Compound type indicators may be specified either
within the goal template, within the argspecs parameter, or in both places.
The argspecs declaration may be viewed as a generalization of the very
limited typing capabilities provided in OAA prior to version 2.3.0.
Prior to version 2.3.0, the only way to associate "typing" with a
solvable was by including compound type indicators within the
goal template.
argnames(Name1, Name2, ..., NameN)
where N is the number of arguments of the solvable's goal template, and
each Name is an atom. These names are provided for
documentation and display purposes only; that is, they are not used by
the facilitator. argnames, like argspecs, is entirely optional for OAA
purposes. In general, the presence of argspecs does not imply a
requirement for the presence of argnames, and vice versa.
Note that there is a supertype/subtype hierarchy implicit in the built-in types above. For instance, float and integer are subtypes of number, and string and number are subtypes of atomic. These relationships will be respected by the facilitator, in matchmaking. For instance, if an argspec indicates type 'number' for a given argument, the faciliator checks that the value of that argument in the goal is either an integer or a float.
For primitive argument values, such as atoms, numbers, the type of an argument is determined by standard means (provided by the Prolog programming language).
For argument values having the functor icldataq, the type is simply icldataq(_) or icldataq(_,_,_), depending on the structure of the argument value.
For argument values having the form xml(XSDSpec, _XMLDocument), the type is identified as xml(XSDSpec, _Variable). Similarly for mime(MimeType, _MimeDocument).
Normally, a callback procedure responding to a request from the agent community does all of its work and assembles all its solutions before returning.
There is also the possibility of delaying the return of a callback procedure's solutions until some future time. This is often useful for events which may take a long time to complete, or will depend on information or events that arrive asynchronously. For example, if a robot has been asked to attain a certain position, this is not a goal that can be achieved quickly. In such cases, it may be desirable to return immediately from the callback procedure, so that other processing steps can proceed.
The process of delaying solutions in an app_do_event callback is handled in three steps:
Here is an example in Prolog, where an agent managing the control of robot movements publishes a solvable move_robot(RobotId,PosX,PosY). Since robotic movement can take a long time, and since event updates as to the robot's position and status arrive asynchronously over time, delayed solutions are used to implement the solvable.
% If someone asks me to make a robot move to (X,Y),
% I will tell the robot to start moving, but won't
% have completed the task until I know if the robot
% has reached the goal or has given up.
oaa_AppDoEvent(move_robot(RobotId, X,Y),_Params) :-
start_moving(RobotId, X,Y).
oaa_DelaySolution(RobotId).
% As the robot moves, we receive updates on its position,
% which we broadcast to all interested agents.
oaa_AppDoEvent(current_position(RobotId, X,Y),_Params) :-
% instead of calling oaa_Solve(robot_pos(RobotId,X,Y),[broadcast]),
% we break it into 2 steps to allow oaa_AddDelayedContextParams()
% to mix in any context parameters.
oaa_AddDelayedContextParams(RobotId, [broadcast], ParamsWithContext),
oaa_Solve(robot_pos(RobotId,X,Y), ParamsWithContext).
% We receive the event that say the robot has completed its task
% so we return solutions to the original request.
oaa_AppDoEvent(finished_moving_to(RobotId, X,Y),_Params) :-
oaa_ReturnDelayedSolutions(RobotId, [move_robot(RobotId, X, Y)]).
% Here we are notified the robot can't do the task and has
% given up, so we return failure (the empty solution list)
oaa_AppDoEvent(cant_move_to(RobotId, X,Y),_Params) :-
oaa_ReturnDelayedSolutions(RobotId, []).
Even though internally the treatment of the solvable move_robot() is handled in a delayed, asynchronous way, this is transparent to a requesting agent, who can issue a oaa_Solve(move_robot(robot1, 100,100),[]) request which will block until the robot has succeeded or failed in its goal.
The priority(P) parameter may be specified in two places:
Imagine declaring a solvable for your agent a(X), which relies on some information provided by the agent community, a1(X). When an incoming request arrives at your agent, the callback associated with a(X) is called as described above. This callback calls oaa_Solve(a1(X),[]), which blocks, waiting for solutions to be returned from the agent community. While the agent is waiting for solutions to be returned, new events may arrive on the communication channel. What happens to these new events? The following behavior is produced:
Meta-agents are simply OAA agents that define a special solvable or capability called "meta", in the form:
Note: other meta hooks may be added in the future.
Several meta-agent hooks may fire for the same query. For instance, if an agent performs oaa_Solve(a(X),[strategy(action)]) and no agent is currently loaded that can handle a(X), a "lookup" meta-agent may fire, loading several agents that can perform a(X). These agents will be prioritized in order of expected utility, and this sequencing may be then improved upon by "prioritize" meta-agent.
What happens if several meta-agents have differing opinions about how a goal should be handled? In this case, the Facilitator uses the "action" strategy to handle them: all meta-agents that can contributed are collected and themselves ordered according to expected utility. The first meta-agent is consulted, and if it returns useful information, the Facilitator uses it. If not, the next meta-agent in the list is consulted until at most one agent succeeds in providing information the Facilitator can use. If no information is found, the Facilitator proceeds with its default handling of the delegated task.
While performing a task, an agent can often make use of information and services provided by other agents. The mechanism for making requests of other agents is encapsulated in a single procedure, oaa_Solve/2. The oaa_Solve/2 procedure provides considerable flexibility in making requests, by selecting different combinations of parameter values. Here, we describe some of the more commonly used parameters. For a complete list, see the Reference Manual.
The standard way (in Prolog) to request a solution to some query is to call the procedure
oaa_Solve(Goal, ParameterList).
Note: Other programming languages such as C or Delphi have their own syntactic variants for all of the library routines described in this developer's guide. For instance, in C, the solve procedure is declared as
oaa_Solve(char *Goal, char *ParameterList, char **answers)
By default (that is, with an empty ParameterList ([]), or by calling the oaa_Solve/1 procedure which assumes an empty Parameter list), a call to this procedure directs the agent's facilitator to assemble all available solutions to Goal; that is, solutions from all of the agent's peers that offer Goal as a solvable. (Note that this description applies when Goal is non-compound; things are slightly more involved with compound goals.) From the programmer's point of view, however, it is used just as if Goal were being executed locally by a Prolog interpreter: it can fail or succeed, and multiple solutions can be obtained through backtracking. This is a very convenient and natural method of writing code when creating an agent's functionality.
When a goal is sent to a facilitator through a solve request, the facilitator looks for agents that can provide a response to the goal by matching the goal against the procedure and data solvables of all connected agents. If multiple agents have indicated that they can return solutions to the goal in question, the default behavior used by the facilitator is to send the request to all pertinent agents and to wait until all agents have responded, collecting the solutions from each agent and routing the set of all solutions back to the requesting agent. If you wish to override this default behavior, perhaps having individual agents send their specific solutions back to the requesting agent separately, use the reply parameter.
However, this default use of the solve procedure, while providing all of the power and expressiveness of Prolog, also has its shortcomings; for example, execution of the agent calling oaa_Solve is suspended until the goal has been solved, either succeeding or failing. This goal may take a certain time to resolve, as it will be posted to the facilitator, and then routed to another agent on another machine for processing. And during this time, the agent posting the query is dormant, waiting for the solution to return. There are, however, a number of alternative methods of posting queries, using the ParameterList, as explained below.
In some particular cases, an agent may decide to use oaa_Solve/2 with the cache parameter; that is,
oaa_Solve(Goal, [cache]).
This behaves just like oaa_Solve(Goal, []), except that
once the goal is computed, the solution is stored locally in the
agent's database. The next time that the same goal is recomputed,
the answer is found immediately, without accessing a remote agent.
This optimization must be used with care, as the agent is responsible
for maintaining the coherency of his own cache: if a solution
is subject to changes over time, it is safer to omit the cache
parameter.
The cache option should correctly handle subsumption. Imagine that a user first issues the request
oaa_Solve(hotel('fairmont', Info), [cache])
which returns one solution, storing this in the cache. If at a later
time, the user asks the query
oaa_Solve(hotel(Any,Info),[cache])
it would be an error to return only the solution stored in the cache;
rather, the system must recognize that the cache does not have
information stored which subsumes this query, so a new query must be
posted over the network. Subsumption is currently correctly handled
by the Prolog agent library, but is not correctly handled by
the agent libraries in other programming languages.
An agent can choose to clear its own cache by using the primitive oaa_ClearCache.
As mentioned above, the default use of oaa_Solve/2, oaa_Solve(Goal, []), causes the responding agent(s) to generate all possible solutions to Goal. In some cases, depending on how the called procedure is written, finding multiple solutions may have undesirable side-effects. Thus, if the calling program knows that it needs only a single solution, or a limited number of solutions, it can prevent the responding program from finding all possible solutions, by using the solution_limit(N) parameter:
oaa_Solve(Goal, [solution_limit(N)])where N can be any positive integer.
This parameters tells each responding agent to find at most N solutions. This means that, when there are multiple responding agents, the total number of solutions returned to the requester may exceed N. However, in the presence of parameter parallel_ok(false), a solution limit can also influence the facilitator's behavior. For example, suppose that both solution_limit(10) and parallel_ok(false) have been specified for some request, and that the facilitator identifies 4 candidate solvers. If, say, the first two solvers return a total of 12 solutions, the facilitator will not send this request to either of the remaining two candidates.
Similarly, a requester can also limit the number of providers selected to handle a request, using the the provider_limit(N) parameter. That is,
oaa_Solve(Goal, [provider_limit(N)])tells the facilitator to select at most N agents to satisfy the given Goal.
provider_limit(N) and solution_limit(N) can be used in conjunction. For example,
oaa_Solve(Goal, [provider_limit(2), solution_limit(3)])tells the facilitator to select at most two providers to handle the given Goal, and tells each of those providers to generate at most three solutions.
When a request is received by a facilitator from some agent, the facilitator's normal behavior is to solicit solutions from all agents that claim to handle that sort of request, including for the requesting agent itself. If it is not desired that the requesting agent be included as a solver of a request, that may be indicated using the reflexive parameter:
oaa_Solve(Goal, [reflexive(false)])
Even with the reflexive parameter true, the requesting agent will only
be asked to solve a goal when the goal matches one of its procedure or
data solvables.
In general, when requesting the solution to a goal, an agent posts the request to the facilitator without knowing which remote agent (or agents) will solve the goal -- the facilitator is responsible for transparently locating an appropriate agent (or agents). However, if the address(es) of a specific agent (or agents) are known, the address(es) can be indicated in the parameter list as
oaa_Solve(Goal, [address(AgentAddress)])
Addresses are described in Agent Names and Addresses.
oaa_Solve(airline_rating(A, R), [owner(6)])
will retrieve all facts of the data solvable airline_rating/2,
which are owned by the agent with local ID 7.
Data ownership is explained in Section 7.5, Ownership of Data Elements.
OAA provides several options for controlling whether, and when, solutions are returned for a goal submitted to oaa_Solve; these are indicated using the blocking/1 and reply/1 parameters. blocking/1 controls the behavior of the requesting agent (the agent calling oaa_Solve), whereas reply/1 controls the behavior of the facilitator and the agents responding to the request.
The blocking/1 parameter is boolean:
The reply/1 parameter at present also has two possible values, but it is expected that there may be additional possibilities introduced in the future.
As explained in the previous subsection, the parameter blocking(false) may be used to request information from remote agents in a non-blocking fashion:
oaa_Solve(Goal, [blocking(false)])
In this way, an agent's computation can continue while the request is
being resolved. This is more efficient for the local agent; however it
is slightly more difficult to program, because additional code is
needed to handle the results of the request when they arrive.
Assuming the parameter reply(none) is not present, the results of a non-blocking query will generate an event of the form
ev_solved(GoalId, Requestees, Solvers, Goal, Params, SolutionList).
The Requestees argument will be a list of the local ID(s) all
agents asked to handle the request, and Solvers will be a list
off the local ID(s) of the agent(s) that succeeded in handling
it. Goal is a copy of the original goal submitted to
oaa_Solve. Params is a copy of the original parameters
submitted to oaa_Solve, except that any variables appearing in
partially instantiated parameters will have been bound.
SolutionList is a list of the goal's solutions, each of which
is unifiable with Goal.
One way of handling the results of a non-blocking query is to provide a handler for the ev_solved/5 event, by declaring and defining an app_do_event callback. Another way is to trap the event by a local communication trigger, which supplies an action to be executed when the solutions are received.
The following example posts the query to be solved, and then sets up a trigger to display the results of the goal asynchronously when they are returned. While the goal is being executed remotely, the agent can perform other actions.
interface :-
get_query_from_user(PostableQuery),
oaa_Solve(PostableQuery, [blocking(false)]),
oaa_AddTrigger(comm,
ev_solved(_GoalId, _Requestees, _Solvers, PostableQuery, _Params, Solutions),
display(Solutions),
[address(self), recurrence(when), on(receive)] ).
Triggers are explained in Section 8, Using
Triggers.
Using the test parameter, you can specify a test to be executed on a local facilitator, and if the test succeeds locally, only then will the goal be solved on that facilitator. This is useful, for example, when specifying a goal such as "send a message to the interface agent, but only on a facilitator where the user's name is 'phil'" :
oaa_Solve(ui_inform('message'), [test(name('phil'))])
Note: the test parameter is useful primarily when multiple facilitators are used, as described in Section 10.1, Using Multiple Facilitators.
When agents are arranged in a hierarchy of multiple facilitators (see Using Multiple Facilitators), it is often important to indicate whether and to what extent a request should be propagated to remote facilitators. For this purpose, the propagate parameter may be used.
The propagate parameter takes a single argument, which itself is a parameter list; the elements that may appear within this list are: up/1, down/1, up_limit/1, and down_limit/1.
The up and down parameters each may take one of three values: true, false, and if_no_solvers. The value true indicates that the request should be propagated upward (or downward); false indicates that it should not be propagated; and if_no_solvers indicates that it should be propagated if there are no local agents that can handle this request (i.e., none that are clients of the same facilitator as the requester).
Given that a goal is to be propagated upward or downward, the up_limit/1 and down_limit/1 parameters can be used to limit the number of levels, of facilitator agents, to which it is propagated. For example, specifying an up_limit of 0 would preclude all upward propagation (the request would go only to the facilitator of the requester, and thus would be no different than specifying up(false)), whereas an up_limit of 1 would allow the requester's facilitator to pass the request upward to its parent facilitator (but no further upward).
The default for the up/1 and down/1 parameters is false. For up_limit/1 and down_limit/1, there is no default value, which means that the number of propagation levels is unlimited.
The following example shows the propagate parameter that would be used if the requester wants its request to be handled by all agents that declare the appropriate solvable, and are clients of its facilitator, or of the parent of its facilitator, or of any other facilitator below that parent in the hierarchy.
oaa_Solve(Goal, [propagate([up(true), up_limit(1), down(true)])])
Note that the propagate parameter does not have any effect when an explicit address has been given, using the address parameter. When an explicit address has been given, the facilitator attempts only to route the request to the agent or agents specified, and does not consider propagation opportunities at all.
These same parameters may also be used with oaa_AddData, oaa_RemoveData, oaa_ReplaceData, oaa_AddTrigger,, and oaa_RemoveTrigger.
Except when a non-default value of the reply parameter is specified, the oaa_Solve/2 procedure blocks until a solution to a query has been found (either in a positive way - success - or a negative way - no solution can be obtained). For this reason, it is sometimes desirable to set a limit on the amount of time a remote agent can use to satisfy a query. The time_limit parameter is used to limit the amount of solve-time to a given number of seconds, with oaa_Solve/2 failing if no solution can be found in this amount of time.
The time_limit parameter encloses a positive real number, as in the following example:
oaa_Solve(Goal, [time_limit(10.5)])
which indicates that the responding agent is not to be given any more
than 10.5 seconds to respond. If it happens that the responding agent
does not respond in the allowed time, the facilitator causes the call
to oaa_Solve/2 to fail.
However, it is still possible that the responding agent will return solutions after the time limit has expired. If this happens, the solutions are still returned to the requesting agent; that is, a ev_solved/6 event is sent to the requesting agent. This makes it possible for the requesting agent to make use of the belated response, if it takes the required steps to make use of that event. The way to do this is described in Section 6.8.
oaa_Solve(notify('Adam Cheyer', 'OAA meeting at 3:00'), [])
In providing this service, this agent may call upon a calendar agent ( to determine the whereabouts of the specified recipient), a database agent ( to retrieve the recipient's phone number or email address), and one or more communications agents (e.g., a phone agent, an email agent, or a fax agent). If the phone agent is employed, the notification agent will also call upon a text-to-speech agent, to prepare digitized audio waveforms to be played over the phone line.
In a scenario such as this, where a series of service requests (calls to oaa_Solve) is initiated by a single call to oaa_Solve, it is sometimes helpful to have a means of establishing a context that ties together all of the service requests. The context/1 parameter provides a simple means of doing this.
The value of the context/1 parameter is unconstrained; that is, it may be any ICL atom or expression that is convenient. Whenever a call to oaa_Solve includes context(SomeValue) in its parameter list, that parameter is automatically propagated to all subsequent calls to oaa_Solve resulting from the initial call (regardless of which agent is making the call). In the scenario above, a context/1 parameter passed to the initial call:
oaa_Solve(notify('Adam Cheyer', 'OAA meeting at 3:00'),
[context(message_request_114)])
would be present in each callback that transmits one of the subsequent
service requests. For example, assume that the email agent is
selected for use by the notification agent, and that it
provides a solvable send/3 and defines a callback function
send_mail/2, as described in the example from Section 5.3. Then its callback function will be
called like this:
send_mail(send(mail, 'Adam Cheyer', 'OAA meeting at 3:00'),
[..., context(message_request_114), ...])
(The parameters list will also include such things as the
from/1 parameter, indicating which agent generated this particular
request.)
It is possible for multiple context/1 parameters to appear in a parameter list. If the initial call to oaa_Solve contains multiple contexts, each of them is propagated to subsequent calls. Furthermore, subsequent calls may introduce additional context/1 parameters, in which case these are passed along together with those introduced by earlier calls in the chain.
This may easily be done, using the get_address/1 parameter. Unlike the other oaa_Solve parameters discussed above, get_address takes a variable for its argument. When oaa_Solve returns, this variable will be bound to a list of all agents to which the goal was sent (or, in the case of a compound goal, all agents to which any subgoal was sent). The elements of this list will be local IDs (as described in Section 4.3.7, Agent Names and Addresses).
Because this parameter returns a value, it is called a "return parameter". Another return parameter, closely related to get_address/1, is get_satisfiers/1. Whereas get_address returns all agents to which the goal was sent, get_satisfiers returns only those agents which returned one or more solutions to the goal (or to some subgoal, in the case of a compound goal).
A future enhancement will enable a user to create their own strategies defining frequently-used combinations of parameters.
This behavior may be modified by a parameter list, which may contain some combination of the following parameters:
| cache(T_F) |
cache all solutions locally, and if good solutions already exist in the
cache, use the local values instead of making a distributed
request. Default: false. |
| level_limit(N) |
highest number of hierarchical levels to climb for solutions. default: N = 0 (don't climb) |
| address(AgentId) | send request to specific agent, given its name or Addr. If AgentID is 'self', solves the goal locally. |
| reply(Mode) |
true: Reply desired. none: No reply desired. Default: true, except when the call to oaa_Solve is a trigger action, in which case it is none. 'none' is used here instead of false, because we anticipate some additional values. |
| blocking(Mode) |
true: Block until the reply arrives. : false: Don't block.
In this case, the reply events (ev_solved) can be handled by the
user's app_do_event callback function. Default: true, except when the call to oaa_Solve is a trigger action, in which case it is false. Note that reply(none) overrides blocking(true). |
| callback(Pred) | When blocking(false) and reply(true) are in effect, hence asking for an asynchronous query, this parameter may be used to specify the predicate to be called with the reply event (ev_solved). In the absence of this parameter, the app_do_event callback is used. |
| solution_limit(N) | Limits the maximum number of solutions found to N. |
| time_limit(N) | Waits a maximum of N seconds before returning (failure if no solution found in time). |
| context(C) | Passes a context value through any subsequent solves. |
| parallel_ok(T_F) | if T_F is 'true' (default), multiple agents that can solve the Goal will attempt to work on it in parallel. If 'false', one agent will be selected at a time to solve the goal, until the maximum number of requested solutions (see solution_limit) is found. |
| reflexive(T_F) |
If T_F is `true', the Facilitator will consider the
originating agent when choosing agents to solve a
request. Default: true. |
| priority(P) | P ranges from 1 (low priority) to 10 (high priority) with a default of 5. |
| flush_events(T_F) |
Will flush (dispose of) all events of lower priority
currently queued at the destination agent. These
events are lost, and will not be executed.
This parameter should be used with caution!!! Default: false. |
| get_address(X) | Returns a list of addresses (ids) of agents that were asked to solve the goal, or one of its subgoals |
| get_satisfiers(X) | Returns a list of addresses (ids) of agents that succeeded in solving the goal, or one of its subgoals. |
| get_goal_id(X) | Returns the GoalId generated for this solve request. |
| strategy(S) |
Shorthand for certain combinations of the above
parameters. S is one of
action = [parallel_ok(false), solution_limit(1)] inform = [parallel_ok(true), reply(none)] |
Remarks: Note that certain combinations of parameters are inconsistent, and are handled as follows: reply(none) overrides blocking(true), reply(none) overrides parallel_ok(false)
All of the above parameters may be used in the "global"
parameter list (the second argument to oaa_Solve), when Goal is
non-compound. Most can be used in the global list with compound goals also.
Some of these parameters can also be used in the NESTED
parameter lists of compound goals.
A data solvable can be used privately by an agent, or can be made publicly readable and/or publicly writable, so that other agents have access to it. The level of access is determined by the permissions and the private parameter associated with the solvable.
The library procedures oaa_AddData, oaa_RemoveData, and oaa_ReplaceData are used to read and write data solvables.
A call to any of these causes the agent that provides the data solvable to check its active data triggers, and may result in the firing of one or more of them, as explained in Section 8.3, Trigger Firing Conditions
Note on terminology: In describing these procedures, we use the name clause for the argument that specifies the fact to be added, removed, or replaced. The connotation of clause is actually more general than that of fact, and clause is used deliberately. This is because in future versions of the OAA library, in certain situations, we expect that it may become possible to add, remove, or replace rules in addition to facts. For present purposes, however, clause may be considered synonymous with fact.
Any agent may add data to one of its writable data solvables (whether private or public), or to a publicly writable data solvable of another agent, by calling
oaa_AddData(+Clause, +Params)
This causes the the data element Clause to be recorded, in
precisely the form given.
Normally, there may be multiple facts recorded for a data solvable, and any number of facts that are exact duplicates of one another. However, note that the creation of multiple facts can be disallowed by the provider of the solvable, by declaring the solvable with the parameter single_value(true). Similarly, the creation of duplicate facts can be disallowed with the parameter unique_values(true).
By default, the data element is recorded following al