-
Notifications
You must be signed in to change notification settings - Fork 1
RSP Latest Specification
This is the latest version of the RSP (Remote Simulator Protocol) protocol: version 1. For an introduction pertaining to the protocol's purpose, versioning guarantees, etc., see RSP Introduction. It is recommended you read it before continuing here.
The RSP protocol allows for the communication between two entities: a "simulator", simulating a set PDDL domain-problem pair, and an agent, which will interact with said domain-problem pair, to "solve" it. This communication is described semi-abstractly, such that messages have a specific serialization format and structure, but the underlying communication channel is unspecified. In practice, RSP will generally be used over a transport such as TCP, HTTP, HTTPS, QUIC, etc. In all of these cases, RSP can also be used locally, via localhost and the like.
Communication is achieved by the simulator and agent exchanging "messages" with each other. Each message has a string "type", to identify it, and a "payload", which may contain any data, according to its type. Messages are sent using CBOR, and are described here using CDDL.
RSP is a request-response oriented protocol, where almost all messages are either requests, from the agent, or responses, from the simulator. The agent cannot send a request while another is being dealt with. This makes the implementation of it extremely simple and foolproof.
In RSP, there are two main phases:
- Session setup, which is guaranteed to not change between protocol versions.
- Session operation, which may look very different depending on the protocol version.
Throughout session operation, agents send requests to observe and interact with their environment. For example:
- Problem setup, to access the domain-problem pair's definition
- Get grounded actions, to see which grounded actions an agent may perform in the environment's current state
- Perform grounded action, for sending the simulator the action the agent would like to perform in the current state
- And others.
Once the agent solves the domain-problem pair, or should simulation stop due to any other reason, a termination message may be sent. These can be sent at any time, by the agent, or the simulator, according to the type of termination message, and are therefore not requests/responses.
Note
You many benefit from first reading some parts of the full specification, as otherwise some parts here may not make sense. Of particular importance is understanding the Messages (invariant) section.
Note
Additionally, this section assumes you can read CDDL well enough to understand the messages being sent.
It suffices to say that it mostly looks like JSON, using ; for comments.
Consider an example problem, with the following PDDL domain:
(define (domain simple-world)
(:predicates (at ?location) (reachable ?a ?b))
(:action move
:parameters (?from ?to)
:precondition (and (at ?from)
(or (reachable ?to ?from) (reachable ?from ?to)))
:effect (and (not (at ?from))
(at ?to))))And a PDDL instance for it:
(define (problem simple-instance)
(:domain simple-world)
(:objects ?a ?b ?c)
(:init (at ?a)
(reachable ?a ?b)
(reachable ?b ?c))
(:goal (at ?c)))Given a simulator loaded with this problem, let's play the role of an agent, interacting with the simulator using the RSP protocol. This agent and simulator will only support communication via protocol version 1.
We (agent) will first request a session setup, with the following request:
{ ; Agent to simulator (request)
type: "session-setup-request",
payload: 1,
}
Note the payload of this request. The simulator supports this version, and so will then give a session-setup response accordingly:
{ ; Simulator to agent (response)
type: "session-setup-response",
payload: null,
}
The simulator, expectedly, chose major version 1. Session operation has now begun. Let's begin by seeing what problem we are actually dealing with. We, the humans, know this, but our hypothetical agent, does not. Thus, we will use the Session setup service, by sending a message like so:
{ ; Agent to simulator (request)
type: "problem-setup-request",
payload: null,
}
The simulator will respond by returning the PDDL strings used to simulate the problem. There isn't any hidden information, so the full strings seen above will be returned, like so:
{ ; Simulator to agent (response)
type: "problem-setup-response",
payload: {
domain: "
(define (domain simple-world)
(:predicates (at ?location) (reachable ?a ?b))
(:action move
:parameters (?from ?to)
:precondition (and (at ?from) (or (reachable ?to ?from) (reachable ?from ?to)))
:effect (and (not (at ?from))
(at ?to))))
",
problem: "
(define (problem simple-instance)
(:domain simple-world)
(:objects a b c)
(:init (at a)
(reachable a b)
(reachable b c))
(:goal (at ?c)))
",
}
}
We can now begin to interact with the environment. To better understand our options though, let's first see which grounded actions we may perform, using the get-grounded-actions-request message type, sending a message like so:
{ ; Agent to simulator (request)
type: "get-grounded-actions-request",
payload: null
}
The simulator will then respond as expected:
{ ; Simulator to agent (response)
type: "get-grounded-actions-response",
payload: [
{
name: "move",
grounding: ["a", "b"]
},
],
}
Note that one cannot do (move a a), as according to the problem, a is not reachable from a. If (move a a) was possible, the problem could end up in a broken state, with our agent technically being "nowhere", due to how we implemented move. Luckily, this isn't the case. Since we only have one valid grounded action, let's perform it, like so:
{ ; Agent to simulator (request)
type: "perform-grounded-action-request",
payload: {
name: "move",
grounding: ["a", "b"]
},
}
Being a valid grounded action, the simulator will apply the grounded action, and respond with payload null, as the domain is yet to be solved. Specifically, the simulator will respond with:
{ ; Simulator to agent (response)
type: "perform-grounded-action-response",
payload: null,
}
Great! We're one step closer to solving the problem. Let's see what our surroundings look like now, using the perception service:
{ ; Agent to simulator (request)
type: "perception-request",
payload: null,
}
This is the environment state returned by the simulator:
{ ; Simulator to agent (response)
type: "perception-response",
payload: [
{name: "at", assignment: ["b"]},
{name: "reachable", assignment: ["a", "b"]},
{name: "reachable", assignment: ["b", "c"]},
],
}
As one can see, it is composed of the predicates that hold true in the state. Let's now finish the problem, by moving to c:
{ ; Agent to simulator (request)
type: "perform-grounded-action-request",
payload: {
name: "move",
grounding: ["b", "c"],
},
}
As we have now solved the problem, the simulator will respond with the closing of the session, like so:
{ ; Simulator to agent (termination message)
type: "goal-reached",
payload: null
}
As we have reached the problem's goal, the simulator, instead of acknowleding the perform-grounded-action request, terminated the session with a goal-reached termination message. See Session termination for more information. We should now close the communication channel.
For the example above, we can expect a protocol sequence diagram like seen below.
sequenceDiagram
participant Agent
participant Simulator
Agent->>Simulator: session-setup-request: 1
activate Simulator
Simulator->>Simulator: is_version_supported(): true
Simulator-->>Agent: session-setup-response
deactivate Simulator
Agent->>Simulator: problem-setup-request
activate Simulator
Simulator->>Simulator: redact_hidden_info(pair_definition())
Simulator-->>Agent: problem-setup-response: {domain: "...", problem: "..."}
deactivate Simulator
Agent->>Simulator: get-grounded-actions-request
activate Simulator
Simulator->>Simulator: get_grounded_actions()
Simulator-->>Agent: get-grounded-actions-response: [{name: "move", grounding: ["a", "b"]}]
deactivate Simulator
Agent->>Simulator: perform-grounded-action-request: {name: "move", grounding: ["a", "b"]}
activate Simulator
Simulator->>Simulator: is_possible_grounded_action(...): true
Simulator->>Simulator: apply_grounded_action(...)
Simulator->>Simulator: is_pair_solved(): false
Simulator-->>Agent: perform-grounded-action-response
deactivate Simulator
Agent->>Simulator: perception-request
activate Simulator
Simulator->>Simulator: get_facts()
Simulator->>Simulator: redact_hidden_facts(...)
Simulator-->>Agent: perception-response: [{name: "at", assignment: [...]}, {name: "reachable", assignment: [...]}, ...]
deactivate Simulator
Agent->>Simulator: perform-grounded-action-request: {name: "move", grounding: ["b", "c"]}
activate Simulator
Simulator->>Simulator: is_possible_grounded_action(...): true
Simulator->>Simulator: apply_grounded_action(...)
Simulator->>Simulator: is_pair_solved(): true
Simulator-->>Agent: goal-reached
deactivate Simulator
The "Remote Simulator Protocol" (henceforth, "RSP") facilitates communication over a TCP connection between an agent and a simulator, for running a simulation session. Messages are written in CBOR and are framed with the size of the message in bytes. This size appears before the actual message, and is stored as a 32-bit unsigned integer, meaning messages have a maximum size of around 500 megabytes. Throughout the specification, messages will be presented using CBOR's Concise Data Description Language (CDDL).
All messages sent will be of the form:
message = {
type: text,
payload: any,
}
When different kinds of messages will be introduced in the following sections, their type and their payload will both be detailed. Almost all messages are either a request, or a response, such that an agent may only send requests, and a simulator, only responses. Message types are unique. Types used for requests and responses, will be of the form <PREFIX>-request, or <PREFIX>-response respectively. <PREFIX> may be used to identify both of the derived types. For example, when we want to refer to the perception-request message type, we may refer to it as the perception request. The only messages without this kind of suffixing, are termination messages. Termination messages cause the termination of the communication channel, and may be sent at any time, so either party must always be available to handle them. Despite this, for any given message type, the payload will always be uniquely determined.
Once the communication channel is set up, the agent must send a session-setup request, with the following payload:
session-setup-request = supported-version
supported-version = uint
essentially, the payload should be the protocol version this agent supports communication with. Upon receiving this information, it should send one of two messages, both with a null payload, depending on wether the version is supported. If it is, a session-setup response, with a null payload. Otherwise, a session-unsupported termination message with a similarly null payload.
After session setup, the actual simulation begins. Session operation is the final stage of an RSP session, and where the bulk of its time is spent. In this phase, an agent may use a set of provided "services", and advance the simulation by performing grounded action. All of this, using RSP requests, each of which is detailed below.
To receive the initial setup of the decision-making problem, alongside its unchanging domain, the agent can use a problem-setup request, with a null payload. The problem-setup response from the simulator will have the following payload:
problem-setup-response = {
domain: text,
problem: text,
}
where domain and problem are both in the PDDL-like language PDDLSIM uses, but without any revealables (:reveal) or fallible actions (:fail).
The perception request allows an agent to get from the simulator the information it perceives in the current state, which is some fraction of the full simulated state, as some information may be hidden. It has a null payload. The perception response from the simulator must have this payload:
perception-response = [* predicate]
predicate = {
name: text,
assignment: [* object]
}
object = text
Essentially, the returned information is a partial encoding of the current state, as a set of all predicates that hold true. For example, given state (west a b), (east b a), assuming all information is known to the agent, the resulting payload would be [{name: "west", assignment: ["a", "b"]}, {name: "east", assignment: ["b", "a"]}}.
The goals request allows the agent to receive information on which goals of the problem it has reached, and which it has yet to reach. This request has a null payload. The goals response from the simulator has the following payload:
goals-response = {
reached: [* goal]
unreached: [* goal],
}
goal = int
Every goal integer is a goal index. Simulations problems may have multiple goals, which can be obtained via Problem setup. The goal index is a simple index into the list of goals an agent must complete. In case of a single goal, that index will be 0.
The get-grounded-actions request allows the agent to receive the valid grounded actions it can perform in state, assuming the agent should be aware of them. Grounded actions relying on hidden information will not be shown. This request has a null payload. The get-grounded-actions response from the simulator has the following payload:
get-grounded-actions-response = [* grounded-action]
grounded-action = {
name: text,
grounding: [* object],
}
object = text
In order to interact with the environment of the simulation, and change its state, the agent can perform grounded actions. To do so, it must send a perform-grounded-action request, with the following payload:
perform-grounded-action-request = grounded-action
grounded-action = {
name: text,
grounding: [* object],
}
object = text
Then, after applying the grounded action, if the problem is still unsolved, the simulator will send a perform-grounded-action response with a null payload. If instead, the problem was solved by the grounded action, the simulator will terminate the session with a goal-reached termination message.
As actions in the simulation can have probabilistic effects, it is up to the agent to figure out what precise effect an action had on the environment, by using perception requests, and understanding the model of the problem.
Session termination messages, or simply "termination messages" may be sent by either party to indicate it is forcibly terminating the simulation. After the transmission of one of these messages, the simulator and the agent should close the communication channel. Termination will usually occur because:
- The agent has solved the problem, causing the simulator to terminate the session with a
goal-reachedtermination. - Either the agent, or the simulator, have encountered an error, causing them to terminate the session with an
errortermination.
Despite this, termination may occur for any reason, due to either party, and at any time during communication. Therefore, termination messages are not classified as requests or responses.
Throughout this section, we will detail every kind of termination message possible. Note that although in the general case, termination messages may occur for any reason, at any time, and may be sent by either party, some specific kinds of termination messages have specific restrictions on their use.
A goal-reached termination message should be sent by the simulator, to indicate that the agent has solved the problem. The problem can be solved after performing a grounded action (via the perform-grounded-action request), or directly after the start of the simulation, assuming the problem started in a solved state. Once the problem is solved, the simulator should send directly after the agent performs a grounded action (via the perform-grounded-action request). In this case, the simulator must not send the usual perform-grounded-action-response message as a response. This termination message is also detailed in the Perform grounded actions section.
The payload of a goal-reached message is null.
An error termination message can be sent by either party, when it has encountered an error. This error can be an unrecoverable internal one, or an external one, coming from a problem with the messages the other party sent. Therefore, an error termination message can be internal, or external, accordingly. An error termination message has the following payload:
error = {
source: "internal" / "external",
reason: text / null
}
The reason field is unspecified here, but should generally contain text that is relevant to the reason for the error. The source field should contain the cause of the error. If it is from the sender, "internal" should be used. If it was caused by input provided by the sendee, "external" should be used.
A timeout termination message can be sent by either party, indicating that the other party failed to send a message within a given time span. This termination message has a null payload.
A give-up termination message can be sent by either party, indicating that it has decided to give up on the simulation. This may be done for any reason. This termination message has the following payload:
give-up = give-up-reason
give-up-reason = text / null
where the reason is left unspecified, but should contain text relevant to the reason for the sender giving up.
A session-unsupported termination message may be sent by the simulator, and only during session setup. See Session setup for more information. This termination message has a null payload.
When a termination reason doesn't fit into one of the existing termination message kinds, one may use a custom termination message. This message has the following payload:
custom = custom-reason
custom-reason = text / null
where the reason is left unspecified, but should contain text relevant to the reason for the termination.