Skip to content

SML Quick Start Guide

SML (Soar Markup Language) provides an interface into Soar based around sending and receiving commands packaged as XML packets. The interface is designed to support connecting environments to Soar (where input and output data structures are sent back and forth) and to support debuggers (where commands to print out specific productions or working memory elements are sent back and forth).

We refer to these environments and debuggers as "clients".

Users of SML do not need to know the details of the XML dialect, but should instead familiarize themselves with the SML API, which hides the details of the underlying XML messaging system. If you are interested in the details, see Soar XML Interface Specification.

We provide a series of classes that together hide the details of the XML messaging system while allowing the client full control over Soar. This guide will provide a quick introduction to using those classes.

The SML API is natively implemented in C++, but there are Java, Python, and Tcl bindings automatically generated by SWIG. Although all examples used in this document are in C++, it should be fairly intuitive how they translate into other languages. Later in the document is a section containing example environments written in Java.

This page explains how to compile SML clients in various languages.

Simple SML example

This example is something of a "hello world" example of how to use the major elements of the SML API. Once you understand this example, you'll be pretty much ready to dive in.

// Generally only need this one header file
#include "sml_Client.h"

using namespace sml ;
using namespace std ;

void main() {

// Create an instance of the Soar kernel in our process
Kernel* pKernel = Kernel::CreateKernelInNewThread() ;

// Check that nothing went wrong.  We will always get back a kernel object
// even if something went wrong and we have to abort.
if (pKernel->HadError())
{
cout << pKernel->GetLastErrorDescription() << endl ;
        return ;
}

// Create a Soar agent named "test"
// NOTE: We don't delete the agent pointer.  It's owned by the kernel
sml::Agent* pAgent = pKernel->CreateAgent("test") ;

// Check that nothing went wrong
// NOTE: No agent gets created if there's a problem, so we have to check for
// errors through the kernel object.
if (pKernel->HadError())
{
cout << pKernel->GetLastErrorDescription() << endl ;
        return ;
}

// Load some productions
pAgent->LoadProductions("testsml.soar") ;

if (pAgent->HadError())
{
        cout << pAgent->GetLastErrorDescription() << endl ;
        return ;
}
Identifier* pInputLink = pAgent->GetInputLink() ;

// Create (I3 ^plane P1) (P1 ^type Boeing747 ^speed 200 ^direction 50.5) on
// the input link.  (We don't own any of the returned objects).
Identifier* pID          = pAgent->CreateIdWME(pInputLink, "plane") ;
StringElement* pWME1 = pAgent->CreateStringWME(pID, "type", "Boeing747") ;
IntElement* pWME2    = pAgent->CreateIntWME(pID, "speed", 200) ;
FloatElement* pWME3  = pAgent->CreateFloatWME(pID, "direction", 50.5) ;

// Send the changes to working memory to Soar
// With 8.6.2 this call is optional as changes are sent automatically.
pAgent->Commit() ;

// Run Soar for 2 decisions
pAgent->RunSelf(2) ;

// Change (P1 ^speed) to 300 and send that change to Soar
pAgent->Update(pWME2, 300) ;
pAgent->Commit() ;
// Run Soar until it generates output or 15 decision cycles have passed
// (More normal case is to just run for a decision rather than until output).
pAgent->RunSelfTilOutput() ;

// Go through all the commands we've received (if any) since we last ran Soar.
int numberCommands = pAgent->GetNumberCommands() ;
for (int i = 0 ; i < numberCommands ; i++)
{
        Identifier* pCommand = pAgent->GetCommand(i) ;

        std::string name  = pCommand->GetCommandName() ;
std::string speed = pCommand->GetParameterValue("speed") ;

        // Update environment here to reflect agent's command

        // Then mark the command as completed
        pCommand->AddStatusComplete() ;

// Or could do the same manually like this:
        // pAgent->CreateStringWME(pCommand, "status", "complete") ;
}

// See if anyone (e.g. a debugger) has sent commands to Soar
// Without calling this method periodically, remote connections will be ignored if
// we choose the "CreateKernelInCurrentThread" method.
pKernel->CheckForIncomingCommands() ;

// Create an example Soar command line
std::string cmd = "excise --all" ;

// Execute the command
char const* pResult = pKernel->ExecuteCommandLine(cmd.c_str(),pAgent->GetAgentName()) ;

// Shutdown and clean up
pKernel->Shutdown() ;   // Deletes all agents (unless using a remote connection)
delete pKernel ;                // Deletes the kernel itself

} // end main

Simple example explained

Creating the kernel

// Create an instance of the Soar kernel in our process
Kernel* pKernel = Kernel::CreateKernelInNewThread() ;

The client can either create a local Soar kernel or a remote connection to an existing Soar kernel (where commands are sent over a socket to a separate process on the same or a different machine).

The local kernel can either be created in the same thread as the caller or in a new thread.

Using the same thread will generally be a bit faster, but it requires the client to periodically call pKernel->CheckForIncomingCommands() so that the kernel has a chance to check for commands coming in from other remote processes (e.g. from a debugger). So if you want maximum speed choose the current thread option, but your code will be a bit more complicated. If speed is not as critical then choose the new thread option. The speed difference isn't that large, perhaps a factor of two on the cost of making a call - which will often be dwarfed by the cost of matching productions or updating the environment.

As you are just reading the "SML Quick Start Guide" I would strongly recommend you use CreateKernelInNewThread until you are somewhat familiar with the system. If you find you need higher performance later, switching over to the CurrentThread model later only requires changing this one line of code.

1
2
3
4
// Create (I3 ^plane P1) (P1 ^type Boeing747 ^speed 200 ^direction 50.5) on
// the input link.  (We don't own any of the returned objects).
Identifier* pID          = pAgent->CreateIdWME(pInputLink, "plane") ;
StringElement* pWME1 = pAgent->CreateStringWME(pID, "type", "Boeing747") ;

The client can build up an arbitrarily complex graph of working memory elements (WMEs) attached to the input-link. Each WME consists of a triplet: (identifier ^attribute value). The first identifier comes from "getInputLink" and then new identifiers are created by CreateIdWME() and new simple WMEs are created through CreateStringWME/CreateIntWME/CreateFloatWME.

A WME's value can be updated through the pAgent->Update() method and it can be removed through pAgent->DestroyWME() which also makes the working memory object invalid.

A graph (rather than a simple tree) can be created through pAgent->CreateSharedIdWME(). This creates a new identifier WME with the same value as an existing identifier. (E.g. given pOrig = (P7 ^object O3) then CreateSharedIdWME(pInputLink, "same", pOrig) would create (I1 ^same O3)).

Committing changes

// Send the changes to working memory to Soar
pAgent->Commit() ;

The client must explicitly request that changes to working memory be sent over to Soar. This explicit command allows the communication layer to be more efficient, by collecting all changes together and sending them as a single command. With 8.6.2 changes are sent over immediately they are made so Commit() is unnecessary. This produces slightly worse performance (as changes are not collected together into a single packet) so to get maximum performance call SetAutoCommit(false) and write the manual commit calls.

Running Soar

1
2
3
4
5
// Run Soar until it generates output or 15 decision cycles have passed
pAgent->RunSelfTilOutput() ;

// Run Soar for 2 decisions
pAgent->RunSelf(2) ;

In most real environments, Soar should be run with pKernel->RunAllAgentsForever() and then use a call to pKernel->StopAllAgents() to interrupt. This allows a user to run the environment from a debugger as well as from the environment.

There's more discussion of this in Section 2 of this document.

Retrieving Output

// Go through all the commands we've received (if any) since we last ran Soar.
int numberCommands = pAgent->GetNumberCommands() ;
for (int i = 0 ; i < numberCommands ; i++)
{
        Identifier* pCommand = pAgent->GetCommand(i) ;

        std::string name  = pCommand->GetCommandName() ;
std::string speed = pCommand->GetParameterValue("speed") ;

        // Update environment here to reflect agent's command

        // Then mark the command as completed
        pCommand->AddStatusComplete() ;

// Or could do the same manually like this:
        // pAgent->CreateStringWME(pCommand, "status", "complete") ;
}

There is direct support provided for an output model where "commands" are represented as distinction identifier's on the output-link. For example, if the output-link identifier is O1 then the agent might add a move command with (O1 ^move M1) (M1 ^speed 20).

If you choose to adopt this model for the agent's actions then it is possible to query the agent for the number of commands added since the last time Soar was run and to retrieve each Command in turn, its name and parameter values. In this example, pCommand would point to M1, the name would be move and the parameter value for speed would be 20.

If you wish to adopt a different model for agent actions, that is also supported, but the support is less direct.

First, notice that GetCommand() returns a standard Identifier WME, so this can be manipulated in the same manner as any other WME. In particular Identifier's offer GetNumberChildren and GetChild methods, so using these it is possible to start from the output link and examine all of working memory beneath the output link. There are also other methods (such as FindByAttribute) that can make this search more efficient.

Secondly, you can use IsJustAdded() and AreChildrenModified() on WMEs on the output-link (and below) to determine what has changed since the last time Soar was run.

Third, you can call AddOutputHandler() to register a function that is called whenever a specific attribute is added to the output link.

Finally, if that is not sufficient, it is possible to call GetNumberOutputLinkChanges() and GetOutputLinkChange() to get a list of all of the WMEs to have been added or removed since the last time Soar was run.

From this collection of methods it should be possible to support just about any manner of output model you wish to adopt, but we would recommend the "Command" model shown above.

The Command Line

1
2
3
4
5
// Create an example Soar command line
std::string cmd = "excise --all" ;

// Execute the command
char const* pResult = pKernel->ExecuteCommandLine(cmd.c_str(),pAgent->GetAgentName()) ;

To this point the discussion has been purely about environments and supporting I/O. However, the ExecuteCommandLine methods allow a client to send any command to Soar that can be typed at the Soar command prompt. Using this method, productions can be loaded, excised, printed out etc.

ExecuteCommandLineXML() is also available which returns the output as a structured XML message, making it easier and safer for a client to parse values from the output. See the online documentation for details the format of that XML output for each command.

Capturing print output

To capture output during a run you need to register for the smlEVENT_PRINT event which will be called periodically during the course of the run. To do this you need to define a handler function which will be called during the run. Here's a simple example:

1
2
3
4
5
6
7
void MyPrintEventHandler(smlPrintEventId id, void* pUserData, Agent* pAgent, char const* pMessage)
{
        // In this case the user data is a string we're building up
        std::string* pTrace = (std::string*)pUserData ;

        (*pTrace) += pMessage ;
}

The method includes a piece of "userData" which is defined by you when you register for the event. In this case we would have:

std::string trace ;
int callbackp = pAgent->RegisterForPrintEvent(smlEVENT_PRINT, MyPrintEventHandler, &trace) ;

Note how the string "trace" is passed into the registration function. This object is then passed back to the handler, which uses it to build up a complete trace. After this handler has been registered calling:

result = pAgent->Run(4) ;

would run Soar for 4 decisions and the trace output would be collected in the string "trace".

There is now also another way to get this output by registering for smlEVENT_XML_TRACE_OUTPUT. This event sends XML objects rather than text strings. Displaying these to the user requires more work by the client, but if the client wishes to parse the text working with XML is much easier. This is the approach taken in the Java debugger.

Events

There are a lot of events you can register for and the list given here will surely grow over time. Here are the types of the handlers in C++ and Java (the Java ones are a little different from the C++ and are more error prone as they're checked at runtime not at compile time):

C++ event handlers (if you're not sure how to convert these types into functions look at the example of the print handler in the previous section):

// Handler for Run events.
// Passed back the event ID, the agent and the phase together with whatever user data we registered with the client
typedef void (*RunEventHandler)(smlRunEventId id, void* pUserData, Agent* pAgent, smlPhase phase);

// Handler for Agent events (such as creation/destruction etc.).
typedef void (*AgentEventHandler)(smlAgentEventId id, void* pUserData, Agent* pAgent) ;

// Handler for Print events.
typedef void (*PrintEventHandler)(smlPrintEventId id, void* pUserData, Agent* pAgent, char const* pMessage) ;

// Handler for Production manager events.
typedef void (*ProductionEventHandler)(smlProductionEventId id, void* pUserData, Agent* pAgent, char const* pProdName, char const* pInstantion) ;

// Handler for System events.
typedef void (*SystemEventHandler)(smlSystemEventId id, void* pUserData, Kernel* pKernel) ;

// Handler for XML events.  The data for the event is passed back in pXML.
// NOTE: To keep a copy of the ClientXML* you are passed use ClientXML* pMyXML = new ClientXML(pXML) to create
// a copy of the object.  This is very efficient and just adds a reference to the underlying XML message object.
// You need to delete ClientXML objects you create and you should not delete the pXML object you are passed.
typedef void (*XMLEventHandler)(smlXMLEventId id, void* pUserData, Agent* pAgent, ClientXML* pXML) ;

// Handler for RHS (right hand side) function firings
// pFunctionName and pArgument define the RHS function being called (the client may parse pArgument to extract other values)
// The return value is a string which allows the RHS function to create a symbol: e.g. ^att (exec plus 2 2) producing ^att 4
// NOTE: This is the one place in clientSML where we use a std::string in an interface.  If you wish to compile with a pure "C" interface
// this can be replaced by a handler that is passed a buffer and a length.  The length is passed within the framework already (from the kernel to here)
// so this is an easy transition.
typedef std::string (*RhsEventHandler)(smlRhsEventId id, void* pUserData, Agent* pAgent, char const* pFunctionName, char const* pArgument) ;

Java event handlers are based on implementing an interface within an object:

From Kernel:

public interface SystemEventInterface {
    public void systemEventHandler(int eventID, Object data, Kernel kernel) ;
}

public interface UpdateEventInterface {
    public void updateEventHandler(int eventID, Object data, Kernel kernel, int runFlags) ;
}

public interface StringEventInterface {
    public void stringEventHandler(int eventID, Object userData, Kernel kernel, String callbackData) ;
}

public interface AgentEventInterface {
            public void agentEventHandler(int eventID, Object data, String agentName) ;
}

public interface RhsFunctionInterface {
            public String rhsFunctionHandler(int eventID, Object data, String agentName, String functionName, String argument) ;
}

public interface ClientMessageInterface {
            public String clientMessageHandler(int eventID, Object data, String agentName, String functionName, String argument) ;

From Agent:

public interface RunEventInterface {
    public void runEventHandler(int eventID, Object data, Agent agent, int phase) ;
}

public interface ProductionEventInterface {
    public void productionEventHandler(int eventID, Object data, Agent agent, String prodName, String instantiation) ;
}

public interface PrintEventInterface {
            public void printEventHandler(int eventID, Object data, Agent agent, String message) ;
}

public interface xmlEventInterface {
            public void xmlEventHandler(int eventID, Object data, Agent agent, ClientXML xml) ;
}

public interface OutputEventInterface {
            public void outputEventHandler(Object data, String agentName, String attributeName, WMElement pWmeAdded) ;
}

public interface OutputNotificationInterface {
            public void outputNotificationHandler(Object data, Agent agent) ;
}

Examples of implementations:

public void runEventHandler(int eventID, Object data, Agent agent, int phase)
{
        System.out.println("Received run event in Java") ;
}

// We pass back the agent's name because the Java Agent object may not yet
// exist for this agent yet.  The underlying C++ object *will* exist by the
// time this method is called.  So instead we look up the Agent object
// from the kernel with GetAgent().
public void agentEventHandler(int eventID, Object data, String agentName)
{
        System.out.println("Received agent event in Java") ;
}

public void productionEventHandler(int eventID, Object data, Agent agent, String prodName, String instantiation)
{
        System.out.println("Received production event in Java") ;
}

public void systemEventHandler(int eventID, Object data, Kernel kernel)
{
        System.out.println("Received system event in Java") ;
}

public void printEventHandler(int eventID, Object data, Agent agent, String message)
{
        System.out.println("Received print event in Java: " + message) ;
}

public void xmlEventHandler(int eventID, Object data, Agent agent, ClientXML xml)
{
        String xmlText = xml.GenerateXMLString(true) ;
        System.out.println("Received xml trace event in Java: " + xmlText) ;

        String allChildren = "" ;

        if (xml.GetNumberChildren() > 0)
        {
                ClientXML child = new ClientXML() ;
                xml.GetChild(child, 0) ;

                String childText = child.GenerateXMLString(true) ;
                allChildren += childText ;

                child.delete() ;
        }

}

public String testRhsHandler(int eventID, Object data, String agentName, String functionName, String argument)
{
        System.out.println("Received rhs function event in Java for function: " + functionName + "(" + argument + ")") ;
        return "My rhs result " + argument ;
}

To see more about events look at the sml_ClientEvents.h header file or the TestSMLEvents test program (currently located in the Tools folder).

Events in Tcl

Tcl requires that callbacks to a Tcl interpreter happen in the same thread as the interpreter is executing in. By default, this will not always happen for SML events. This is because there is an event thread started up by SML (running in the client) which is used to check for incoming events and make the necessary callback calls.

Therefore for Tcl we recommend shutting down this event thread and polling explicitly for incoming events. This can be done in a few lines of code like this:

# We want to make sure to handle events in the Tcl thread
# so we turn off the event thread and poll for events instead.
$_kernel StopEventThread
checkForEvents $_kernel

# Explicitly check for incoming commands and events every n milliseconds
proc checkForEvents {k} {
   $k CheckForIncomingCommands
   after 10 checkForEvents $k
}

This assumes $_kernel is a variable set to the SML kernel object. Note, the after 10 triggers another call to the checkForEvents() method after a 10ms delay. Making this value larger will make Tcl less responsive to events. Making it smaller will consume more CPU time checking for events.

Building an Environment

If you are converting an existing SGIO environment you should also read the "Moving from SGIO to SML" document as well as this section.

The Java implementation of Towers of Hanoi (TOH) is a useful reference, showing a simple environment environment. I'll use that to as an example here, so the code snippets will be in Java, but a very similar approach can be taken in other languages (currently Tcl or C++).

Initialization

The first step is to create an instance of the kernel and then create an agent. The name passed to CreateKernelInNewThread is the name of the library to load (DLL on Windows). This is optional in 8.6.2 and defaults to SoarKernelSML.

It is important to check for an error using the kernel.HadError() method. CreateKernel will not return an empty kernel object even if initialization fails. This is deliberate design to ensure that meaningful errors can be reported to the user.

Initialization Code Sample

// create our Soar kernel
kernel = Kernel.CreateKernelInNewThread();

if (kernel.HadError())
{
    // Better to use a “Message Box” to display this if your platform/toolkit allows.
    System.out.println("Error creating kernel: " + kernel.GetLastErrorDescription()) ;
    System.exit(1);
}

agent = kernel.CreateAgent(AGENT_NAME);
boolean load = agent.LoadProductions("towers-of-hanoi-SML.soar");
if (!load || agent.HadError()) {
    throw new IllegalStateException("Error loading productions: "
        + agent.GetLastErrorDescription());
}

Input

Mapping from the environment's state to input values involves calls to create, update and destroy working memory elements using calls like these:

1
2
3
agent.Create<type>WME (e.g. agent.CreateIntWME)
agent.Update()
agent.DestroyWME

Working memory elements are linked together to form a tree (actually a graph) with the input link as the root of the tree. To get the input link identifier (the top of the tree) call agent.GetInputLink().

A key step after making multiple calls to modify the input link is to call Commit(). All changes to working memory are buffered (in the environment) until the moment Commit is called, at which point they are all sent over as a single large message to the kernel. This greatly improves performance, but you must remember to call Commit before running the agent or your changes won't actually appear on the agent's input link. This was changed in 8.6.2 so the default now is that Commit() is called automatically each time a wme is modified on the input link. To enable the faster version which then requires manual calls to Commit() call SetAutoCommit(false). Calling Commit multiple times during a single input cycle will not cause problems.

Output

The most common way to examine output from an agent is to use the "Commands" model. With this method the agent places each output command on the output-link using the format:

1
2
3
(X ^output-link I3)
(I3 ^command-name C1)
(C1 ^param1 value ^param2 value)

Thus the name of the command appears directly under the output-link and all parameters are added to the command identifier and can only be one level deep. (If you wish to use an alternative model see below).

If the agent adopts this format for output commands they can be easily retrieved by the environment using code like this:

Update World Code Sample

Procedure:

  1. Get Output
  2. Change-World-State
  3. Send Input
private void updateWorld()
        // See if any commands were generated on the output link
        // (In general we might want to update the world when the agent
        // takes no action in which case some code would be outside this if statement
        // but for this environment that's not necessary).
        if (agent.Commands())
        {
                // perform the command on the output link
                Identifier command = agent.GetCommand(0);
                if (!command.GetCommandName().equals(MOVE_DISK)) {
                    throw new IllegalStateException("Unknown Command: "
                            + command.GetCommandName());
                }
                if (command.GetParameterValue(SOURCE_PEG) == null ||
                        command.GetParameterValue(DESTINATION_PEG) == null) {
                    throw new IllegalStateException("Parameter(s) missing for Command "
                            + MOVE_DISK);
                }
                int srcPeg = command.GetParameterValue("source-peg").charAt(0) - 'A';
                int dstPeg = command.GetParameterValue("destination-peg").charAt(0) - 'A';

                // Change the state of the world and generate new input
                moveDisk(srcPeg, dstPeg);

                // Tell the agent that this command has executed in the environment.
                command.AddStatusComplete();

                // Send the new input link changes to the agent
                agent.Commit();

                // "agent.GetCommand(n)" is based on watching for changes to the output link
                // so before we do a run we clear the last set of changes.
                // (You can always read the output link directly and not use the
                //  commands model to determine what has changed between runs).
                agent.ClearOutputLinkChanges() ;

                if (isAtGoalState())
                    fireAtGoalState();
        }
}

The method GetCommandName and GetParameterValue extract the appropriate attributes and values from the output link and return them to the environment.

The method AddStatusComplete() adds (C1 ^status complete) to the command structure, indicating to the agent that this command has executed and can now be removed by the agent.

(Note that this is simple form of input that in this case is sent to the output link rather than the input link. This is technically a bit of "back door" input, but is simpler than passing a large structure to the input link and having the agent match up the two. This addition is made during the agent's next input cycle as with all input).

In order for the Commands method to work, the system has to keep track of changes to working memory as only newly added commands are reported through the GetCommand method. For this process to work, the environment must call ClearOutputLinkChanges() before running the agent again.

While this Command model should be sufficient for almost all environments, if there is a need to process other structures, the environment can always choose to ignore this model partially or completely. First, the method GetCommand returns an Identifier object. The environment can use the methods GetNumberChildren and GetChild to walk this Identifier and locate any arbitrary working memory element beneath an Identifier or the environment can use the FindByAttribute method to retrieve the values of working memory elements that have a particular attribute.

In fact a environment can abandon the Command model completely and simply call GetOutputLink() to get the Identifier object at the top of the output link and proceed to examine the tree (graph) from there.

Running the agent(s)

There are two main methods for running an agent:

  • RunSelfForever() and it's twin RunAllAgentsForever()
  • RunSelf(1) and RunAllAgents(1) (to step the environment rather than running it).

The trick here is that the code for running the agents should be separate from the code for updating the world (collecting output; updating world state; sending input). By separating the two we can either issue the run from the environment or from a debugger (or other client) and everything works correctly.

Let's assume we have the updateWorld() method from above, which should have the form:

1
2
3
4
5
6
void updateWorld()
{
    check-for-output() ;
    update-world-state() ;
    send-new-input() ;
}

Then the code samples below show how to connect up the system so that updateWorld() is only called once all agents have completed the output phase (i.e. at the end of a decision cycle).

Run Sample Code

public void run()
{
    m_StopNow = false ;

    // Start a run
    // (since this is single agent could use agent.RunSelfForever() instead, but this shows how to run multi-agent environments)
    kernel.RunAllAgentsForever() ;
}

public void step()
{
    // Run one decision
    kernel.RunAllAgents(1) ;
}

public void stop()
{
    // We'd like to call StopSoar() directly from here but we're in a different
    // thread and right now this waits patiently for the runForever call to finish
    // before it executes...not really the right behavior.  So instead we use a flag and
    // issue StopSoar() in a callback.
    m_StopNow = true ;
}

Event-based method for updating the world

public void registerForUpdateWorldEvent()
{
int updateCallback = kernel.RegisterForUpdateEvent(sml.smlUpdateEventId.smlEVENT_AFTER_ALL_OUTPUT_PHASES, this, "updateEventHandler", null) ;
    }

public void updateEventHandler(int eventID, Object data, Kernel kernel, int runFlags)
{
        // Might not call updateWorld() depending on runFlags in a fuller environment.
        // See the section below for more on this.
        updateWorld() ;

        // We have a problem at the moment with calling Stop() from arbitrary threads
        // so for now we'll make sure to call it within an event callback.
        // Do this test after calling updateWorld() so that method can set m_StopNow if it
        // wishes and trigger an immediate stop.
        if (m_StopNow)
        {
        m_StopNow = false ;
        kernel.StopAllAgents() ;
        }
}

The Run code is pretty simple. The only issues to be aware of are:

  • RunAllAgentsForever() is currently (8.6.2) a blocking call, so we usually spawn a thread and issue this call in that thread.
  • StopAllAgents() can currently not be called from an arbitrary thread (or it blocks waiting for the Run to terminate). This may be fixed in a later version, but for now calling it from an event callback solves this problem.

The way updateWorld() is called is after the smlEVENT_AFTER_ALL_OUTPUT_PHASES event fires. Why bother to do this rather than writing run as:

1
2
3
4
5
while (!stopped)
{
    run(1) ;
    update-world() ;
}

There are two basic reasons. First, there are no guarantees that run(1) will run for a decision. If we include breakpoints on productions or phase transitions this call may run for less time, possibly confusing the environment (as the agents have not progressed as far as expected). Second, by making the call to updateWorld() event based, we can issue arbitrary run commands from a debugger and the environment will function correctly. For more on this see the "Event-Driven Environments Proposal.doc" file.

Also in Tcl please be sure to read about how to poll for events in Tcl (see Section 1.6.1).

Supporting running without updating the environment

For some environments it may make sense to allow the user to run an agent without starting the environment. This can be helpful when debugging one agent or having reached a particular situation in the world, stopping and wanting to step slowly to observe the reasoning.

To support this the environment should check the runFlags parameter passed to the updateEventHandler. This is a bit field which currently consists of the values in this enum:

Run Flags

1
2
3
4
5
6
7
8
typedef enum
{
    sml_NONE                =  0,           // No special flags set
    sml_RUN_SELF            =  1 << 0,      // User included --self flag when running agent
    sml_RUN_ALL             =  1 << 1,      // User ran all agents
    sml_UPDATE_WORLD        =  1 << 2,      // User explicitly requested world to update
    sml_DONT_UPDATE_WORLD   =  1 << 3,      // User explicitly requested world to not update
} smlRunFlags ;

Based on these flags, the environment should decide whether to update or not. We leave this entirely to the discretion of the designer because different combinations may make sense for different situations but this might be a typical set (for a multi-agent environment):

if sml_RUN_SELF set and SML_UPDATE_WORLD not set or sml_DONT_UPDATE_WORLD set then don't call updateWorld().

The meaning of this would be that:

  • run --self would cause the agent to run but not the environment
  • run --noupdate would cause all agents to run but not the environment
  • run --self --update would cause one agent to run and update the environment
  • run would cause all agents to run and update the environment.

You are free to make other choices if there's a compelling reason.

Integrating with other clients (esp. the debugger)

One final issue is updating the controls in the environment correctly if the user runs Soar from the debugger and not the environment (e.g. disabling the run button and enabling stop during a run).

There are two approaches to this. First, ignore the problem and don't enable/disable controls. Instead, just handle errors if the user tries to issue a second run while a run is already going ahead. The second option is to register for the smlEVENT_SYSTEM_START and smlEVENT_SYSTEM_STOP events and use these to enable/ disable the UI in the environment in appropriate ways.

Here's an example for implementing this support:

Registering and Handling Start/Stop Sample Code

public void registerForStartStopEvents()
{
    if (kernel != null)
    {
        int startCallback = kernel.RegisterForSystemEvent(smlSystemEventId.smlEVENT_SYSTEM_START, this, “systemEventHandler”, null) ;
        int stopCallback  = kernel.RegisterForSystemEvent(smlSystemEventId.smlEVENT_SYSTEM_STOP, this, “systemEventHandler”, null) ;
    }
}

public void systemEventHandler(int eventID, Object data, Kernel kernel)
{
        if (eventID == sml.smlSystemEventId.smlEVENT_SYSTEM_START.swigValue())
        {
                // The callback comes in on Soar's thread and we have to update the buttons
                // on the UI thread, so switch threads.
                dpy.asyncExec(new Runnable() { public void run() { updateButtons(true) ; } } ) ;
        }

        if (eventID == sml.smlSystemEventId.smlEVENT_SYSTEM_STOP.swigValue())
        {
                // The callback comes in on Soar's thread and we have to update the buttons
                // on the UI thread, so switch threads.
                dpy.asyncExec(new Runnable() { public void run() { updateButtons(false) ; } } ) ;
        }
}

public void updateButtons(boolean running)
{
        boolean done = game.isAtGoalState() ;

        startButton.setEnabled(!running && !done) ;
        stopButton.setEnabled(running) ;
        resetButton.setEnabled(!running && !done) ;
        stepButton.setEnabled(!running && !done) ;
}

Further details

To learn more about ClientSML and SML in general, the best documentation is the header files for the methods in ClientSML. In particular, sml_ClientKernel.h, sml_ClientAgent.h and sml_ClientIdentifier.h contain a lot of useful methods and explanations.

Beyond that check the Soar XML Interface Spec and any other documentation in its vicinity.

Using other languages

The Tcl and Java interfaces were generated by SWIG. We have provided some custom code to help SWIG out in a few places, mostly with callbacks. If you're interested in creating interfaces for other languages that SWIG supports, check out the SWIG documentation and try to follow our existing solutions.

A Tcl package, called Tcl_sml_ClientInterface is available. On Windows it is located in the soar-library directory. This can be used by including the following line in your Tcl code:

package require tcl_sml_clientinterface

Note that the directory where the package resides must be added to Tcl's auto_path variable. The available functions are direct translations of their C++ counterparts. See TOHtest.tcl in soar-library for examples showing how to use the Tcl interface. The Tcl package includes Tcl_sml_ClientInterface.dll (or the equivalent for other platforms), which is required.

Example Environments in Java

Here are two example clients written in Java, intended to explain the basics of using SML in that language. Thorough explanatory comments are inlined.

  1. A very simple Soar environment.
  2. HelloWorld.java
  3. helloworld.soar
  4. A more complex interactive asynchronous environment.
  5. SimpleAsyncEnv.java
  6. simpleasyncenv.soar

Helpful Tips

Memory Management

Memory management is actually really easy. Generally, the only objects you should explicitly delete are the kernel object and any objects you directly allocated through a call to new. In Java and Tcl, this generally means you can just let things go out of scope when you're done with them. There are a couple special cases you should be aware of, though:

  • Agent objects are automatically deleted when the owning Kernel object is deleted (actually, when the call to Kernel::Shutdown is made, which you should always make before deleting the kernel). If you want to destroy an agent earlier, you can by making a call to Kernel::destroyAgent. Under no circumstances should you delete (in the C++ sense) an Agent object.
  • In Java if you create a ClientXML object through xml = new ClientXML() you should call xml.delete() on it when you're done. This isn't strictly required (the garbage collector will get it eventually) but is good practice and will avoid messages about leaked memory when the application shuts down. As per the general rule, in C++ if you create it with new you're responsible for destroying it with delete.
  • Since there can be multiple clients interacting with the same kernel and agents, your application needs to be listening for the appropriate events so if some other client deletes/destroys a kernel or agent your application is using, you don't crash. Specifically, listen for the BEFORE_AGENT_DESTROYED and BEFORE_SHUTDOWN events so you can clean things up as needed in your application.

Boosting Performance

It is often desirable to maximize the performance of your SML application. This section assumes that you just want to make things as fast as possible after you have finished debugging your application. Debugging is an inherently slow process, so these tips will be less helpful while you're still debugging.

  • Compile with optimizations turned on. In Visual Studio this means doing a release build. On Linux and OS X, the default settings are probably sufficient, but you can experiment with new settings if you want (let us know if you find better settings).
  • Put primary application and Soar in the same process. That is, use CreateInNewThread or CreateInCurrentThread, not CreateRemoteConnection. Using a remote kernel means socket communications are used, which is slow.
  • Don't register for unnecessary events. Every event that is registered for causes extra work to be done. Try to find an appropriate event to register for so you don't end up getting more event calls than you actually need - that is, try to avoid registering for events which occur more frequently than you need and then filtering them on the application side.
  • Don't connect the debugger. Connecting the debugger creates a remote connection and also registers for several events.
  • Set watch level 0. Even if you don't have a client registered for any of the print or XML events, work is still done internally to generate some of the information that would have been sent out. Setting watch level 0 avoids this work.
  • Disable monitor productions. Again, even if no client is registered to print out the text of monitor productions, work is still done internally to prepare the text. Monitor productions can be disabled by excising them or commenting them out, but an easier method is to have each monitor production test a debug flag in working memory which is set by some initialization production or operator. Thus all of the monitor productions can be turned on or off by changing one line of code.
  • Disable timers. Soar uses timers internally to generate the output of the stats command. If you don't need this information, you can use the timers - off command to disable this bookkeeping. This can make a significant difference in the watch 0 case.
  • Avoid running agents separately. Instead of calling RunSelf or RunSelfTilOutput on each agent, just call RunAllAgents on the kernel itself. This runs all agents together and avoids the overhead of running them separately. The absolute best you can do is to call RunAllAgentsForever as described in section 2.4 - this avoids repeatedly calling the run functions at all and will make it easier to stop and restart your application from the debugger (or other clients).
  • In the case where the absolute best performance under SML is desired, use CreateKernelInCurrentThread instead of CreateKernelInNewThread and set the "optimized" flag to true in the parameters passed to CreateKernelInCurrentThread. This means Soar will execute in the same thread as your application. Without this each call to and from the Soar kernel requires a context switch (assuming a single processor machine). This method also eliminates the thread which polls for new events. This means you must poll for the events yourself by periodically calling CheckForIncomingCommands, which is a little more work for the programmer.