We are in early beta, and may not have tested Walnut in your browser/OS combination yet, so it may not look perfect. Thanks for your patience! ×
We are in early beta, and the documentation may be incomplete and/or have missing links. Thanks for your patience! ×

Writing Agent Programs

An agent program is an implementation in JavaScript or Python3, of the behavior of an agent. It uses a provided API to interact with the Walnut simulator.

An example of an agent program is present in the doc/examples/agent_programs/tictactoe_agent_program.js and doc/examples/agent_programs/tictactoe_player.js files, but for simplicity we will explain it using a simpler example.

The most basic example for an agent program is this:

var agent = require('walnut/agent');
agent.run(function (perception) {
    var action = ... compute from perception and internal state ...;
    return action
});

The same example, in python:

from walnut.core import agent
def agent_function(self, perception):
    action = # ... compute from perception and internal state ...
    return action
agent.run(agent_function)

Going line by line over the javascript version code, we can see that

var agent = require('walnut/agent');

Imports the Walnut API.

agent.run(...);

This is the boilerplate to initialize the agent and connect it with the Walnut simulator. The argument is what we call the “agent function”, which is an anonymous function of the following form:

function (perception) {
    var action = ... compute from perception and internal state ...;
    return action
};

This function contains your custom logic and agent behavior. It receives as an argument the last perception of your agent, and should return an action. Perception and action types must be compatible with the world definition where this agent is executed.

The python example has that same structure: we import the Walnut api, define a function with the logic of the agent, and then we call agent.run with that function.

It is important to note that the agent code can have variables at the top level which are accessible by the agent function. This means that the agent function is not necessarily a function in the mathematical sense, so it might produce different action responses to identical perceptions if your code uses global state, which is required for non-reflex agent behaviors (AIMA 2.4.2).

Handling Walnut values

As part of your agent implementation, you will need to handle Walnut values from your JavaScript or Python code: the perceptions received, the actions and extra data returned, etc. The simulation will handle the conversion between Walnut and JavaScript/Python values, as long as you follow some simple rules.

An important detail is that Walnut is strict about the values sent and received to your agent code. When you send an action back, you must ensure that the type of the action matches the action type for the agent role; otherwise your agent will fail and the simulation will stop.

Basic types

Strings are represented using JavaScript and Python strings (we use python3, so they are unicode strings, not encoded bytes), and there is no special logic in the conversion done between languages. And the same is true for Booleans (using JavaScript's true and false, or Python's True and False) and Null (Javascript: null, Python: None).

Numbers are always floating point numbers in Walnut. This follows the same convention as JavaScript. In Python agents you are free to use integers or floating point numbers, and the simulation will interpret them as floating point numbers.

Collections

Walnut Lists are converted to JavaScript arrays and Python lists, with no special conversion logic. If you are sending lists to the Walnut simulator, remember that lists specify the type of their items (example: "a list of strings"), so the elements of your JavaScript arrays and Python lists should respect the value type defined. You can't return arrays/lists with values of different types to the simulation (you still can use them internally in your agent).

Walnut Dictionaries are represented in Python as dicts, and in JavaScript as objects. And the same limitations from Lists apply to Dictionaries: they define the type of their items, so you should respect that limitation when constructing values for the simulation (actions, agent metadata, etc.). Also, in Python dicts can use keys of other types than strings, but Walnut only supports string keys, so you should respect that in the dicts you send to the simulation.

Constructed values

Finally, values built using constructors (example: Point(3, 5)) are also represented in JavaScript and Python as objects, with some minor differences. We will explain them separatedly:

Constructed values in Javascript

In Javascript they are instances of a special, private, object prototype: values.Value. If you receive one of these values (typically in the perception), you can access the label and the fields with a straightforward API.

If you need to know the label or the number of fields, you can use the getLabel and getArity methods of the value. For example, if you have the Point(9, 7) example value stored in a variable perception, you can write:

var label = perception.getLabel(); // label will contain "Point"
var arity = perception.getArity(); // arity will contain 2

Their positional fields are accessed by index, the same way array items are accessed. Using the Point(3, 5) example value stored in a variable point:

var first_field = point[0]; // first_field will contain 3
var second_field = point[1]; // second_field will contain 5

Their labeled field are accessed using the field name, either through indexing or field access. With an example value Obstacle(color="red", size=10) stored in a variable obstacle:

var color_field = obstacle["color"];  // color_field will contain "red"
var size_field = obstacle.size;  // size_field will contain 10

Besides, labeled fields can also be accessed by it positional index:

var color_field = obstacle[0];  // color_field will contain "red"
var size_field = obstacle[1];  // size_field will contain 10

You can also construct values to send back from the agent to the simulator (typically when returning actions). To do this you need to include a module:

var values = require('walnut/core/values');

Every constructor name will be a property of the values module, once the call to agent.run() starts (not before, so do not try to construct values as initialization).

Constructors with only positional fields take as argument a single array. This array, with the field values, must be of length equal to the constructor's arity. Alternatively, each field's value can be given as a separate argument (as long as the first field is not a List).

// using positional fields:
var point0 = new values.Point([3, 4]);
var point1 = new values.Point(3, 4);  // alternative declaration.

Constructors with labeled fields take a single object as argument. The argument's enumerable keys must include all the constructor's field names: extra keys will be ignored but missing keys will raise an error.

Alternatively, instead of an object all the fields values can be given in an array. The order of the fields is the order defined in the world. Otherwise, each field's value can be given as a separate argument (as long as the first field value is not a List).

// using labeled fields:
var obstacle0 = new values.Obstacle({color: "red", size: 10});
var obstacle1 = new values.Obstacle("red", 10);  // alternative declaration.
var obstacle2 = new values.Obstacle(["red", 10]);  // alternative declaration.

Constructed values in Python

In Python they are instances of a special, private, class: walnut.core.values.Value. If you receive one of these values (typically in the perception), you can access the label and the fields with a straightforward API.

If you need to know the label, you can use the label attribute. And if you need to know the arity of the value, you can access that information and more from the constructor attribute. For example, if you have the Point(9, 7) example value stored in a variable perception, you can write:

label = perception.label  # label will contain "Point"
arity = perception.constructor.arity  # arity will contain 2

Their positional fields are accessed by index, the same way array items are accessed. Using the Point(3, 5) example value stored in a variable point:

first_field = point[0]  # first_field will contain 3
second_field = point[1]  # second_field will contain 5

Their labeled fields are accessed using the field name. With an example value Obstacle(color="red", size=10) stored in a variable obstacle:

color_field = obstacle["color"]  # color_field will contain "red"
size_field = obstacle["size"]  # size_field will contain 10

Besides, labeled fields can also be accessed by their positional index:

color_field = obstacle[0]  # color_field will contain "red"
size_field = obstacle[1]  # size_field will contain 10

You can also construct values to send back from the agent to the simulator (typically when returning actions). To do this you need to use the types attribute of the agent module you imported. Every constructor name will be a property of types, once the call to agent.run() starts (not before, so do not try to construct values as initialization).

Constructors with only positional fields take their arguments in order:

# using positional fields:
point = agent.types.Point(3, 4)

Constructors with labeled fields can either take all the constructor's fields as named arguments, or all the fields in order as positional arguments.

# using labeled fields:
obstacle0 = agent.types.Obstacle(color="red", size=10)
obstacle1 = agent.types.Obstacle("red", 10)

Recording additional information in the trace

Sometimes it is desirable to store additional information about the agent state in the trace. That allows visualization of aspects of the agent which are not intrinsic to the world.

Let us say for example that you are writing an agent program for a cleaning robot. You have decided to make the program switch between several strategies according to some policy; sometimes the robot will be "cleaning" (looking for dirt to pick up) other times it will be "waiting", and other times it will be "recharging" (trying to find a charging station to replenish its battery). The current state is relevant to your algorithm but not part of the problem. While visualizing a simulation it would be helpful to have that information to understand the behavior of your robot.

You can do the following in the agent program (Javascript example):

var strategy = "cleaning";
agent.run(function (perception) {
    this.meta.strategy = strategy;
    if (strategy == "cleaning") {
        ...
        return some_action;
    } else {
        ...
});

The same example in Python:

strategy = "cleaning"
def agent_function(self, perception):
    self.meta['strategy'] = strategy
    if strategy == "cleaning":
        # ...
        return some_action
    else:
        # ...
agent.run(agent_function)

The important line is the assignment to this.meta.strategy in Javascript, or self.meta['strategy'] in Python. The meta attribute is initially set to an empty object in Javascript, and an empty dict in Python, and can be modified or overwritten. Whatever values it has when the action is returned are also stored in the trace. From the visualization language you can then access to agent_metadata.cleanbot.strategy (assuming “cleanbot” is the agent id).

Full API

The run() method introduced at the beginning of this document takes two optional arguments:

  • the agent id (which should match the problem file). If not given, the agent id is picked from the command line argument (which is already passed when you run a simulation from your browser, you shouldn't need to use that argument for agents run from the web).
  • the URL for a simulator (host and port). If not given, it is also picked from the command line argument or, finally, the default one is used ("walnut://localhost:1337"). Again, you shouldn't need to modify this in the web app.
agent.run(myAgentFunction, 'my agent ID', 'walnut://localhost:1337');

This call creates an “agent object”, connects it to the given Walnut simulator and runs the agent.