An Edge
is a bounded buffer message passing channel between two ports belonging to their respective agents
.
Terms:
-
A
contract
is defined as: The Cap’n Proto schema on the output port of the upstreamagent
MUST be the same as the Cap’n Proto schema on the input port of the downstreamagent
. If the two are the same, thecontract
is said to be satisfied, otherwise it is unsatisfied.
There are three phases building up to a successful Edge
formation:
-
During
agent development time
anagent
's port is assigned a Cap’n Proto schema. -
During
subgraph development time
the syntax->
or=>
is used to instruct an upstreamagent
's port to connect to a downstreamagent
's port later at run-time. It represents aconnection
between twonodes
. Though it is not yet anedge
because thecontract
might not be satisfied. -
Lastly, the graph is successfully loaded into the virtual machine without errors. This act means all
agent
contracts
were satisfied, and allsubgraph
connections
are now classified asedges
.
Once an edge
is formed, it becomes a bounded buffer message passing channel, which can only contain messages
with data in the shape of whatever the Cap’n Proto schema is.
So despite you seeing only Cap’n Proto schema in this directory, the concept of an Edge
is more profound. Hence we would prefer naming this concept after it’s grandest manifestation, and in the process, the name encapsulates all of the above information. The name should also tie in with the concept of a node
in graph theory, as such we use nodes
and edges
to construct subgraphs
.
When developing a subgraph
there comes a time when the developer wants to inject data into an agent
or another subgraph
. One needs to use an exposed edge
or an imsg
(initial message) which has this syntax:
{ subgraph, nodes, edges }:
subgraph {
src = ./.;
flowscript = with nodes; with edges; ''
'${prim_bool}:(boolean=true)' -> INPUT_PORT NAME()
'';
}
Exposed edges
or imsgs
kick start agents
into action, otherwise they won’t start. Due to the dataflow nature of agents
they will politely wait for data before they start doing anything. This is the equivalent of passing the path of a data source to some executable on the command line. Without the argument the program would just sit or fail. Another way of looking at it might be a pipeline that has come to the surface to accept some form of input.
We use the name exposed edge
to differentiate between a hidden edge
, but by far the most common usage is imsg
and edge
.
Hidden Edge or Edge
Hidden edges
are represented with this syntax ->
and =>
, and are used to control the direction flowing data. Hence the process of programming a subgraph
is essentially digging ditches and laying pipelines between buildings.
Examples of hidden edges
-
From one
agent
to anotheragent
:
{ subgraph, nodes, edges }:
subgraph {
src = ./.;
flowscript = with nodes; with edges; ''
agent1() output -> input agent2()
'';
}
-
Into a
subgraph
:
{ subgraph, nodes, edges }:
subgraph {
src = ./.;
flowscript = with nodes; with edges; ''
output => input subgraph()
'';
}
-
Out of a
subgraph
:
{ subgraph, nodes, edges }:
subgraph {
src = ./.;
flowscript = with nodes; with edges; ''
agent() output => output
'';
}
-
Contracts between components are critical for creating living systems.
-
Schema ensure we do not need to parse strangely formatted
stdin
data. -
A
node
does one and only one thing, and the schema represents a language thenode
speaks. If you want anode
to do something, you have to speak it’s language.
The edges
directory is where all the schema go:
├── key
│ └── value
│ └── default.nix
├── list
│ ├── command
│ │ └── default.nix
│ ├── text
│ │ └── default.nix
│ ├── triple
│ │ └── default.nix
│ └── tuple
│ └── default.nix
├── maths
│ ├── boolean
│ │ └── default.nix
│ └── number
│ └── default.nix
Edges
may depend on other edges
.
The { edge, edges }:
lambda passes in two arguments, the edge
builder and edges
which consists of every Edge
or Edge Namespace
in the system.
The edge
building function accepts these arguments:
-
The
src
attribute is used to derive anEdge
name based on location in the directory hierarchy. -
The
edges
attribute resolve transitive dependencies and ensures youragent
has all the needed files to type check. -
a Cap 'n Proto
schema
. This is the heart of the contract, this is where you may create potentially complex deep hierarchies of structured data. Please read more about the schema language.
{ edge, edges }:
edge {
src = ./.;
edges = with edges; [ command ];
schema = with edges; ''
@0xf61e7fcd2b18d862;
using Command = import "${command}/src/edge.capnp";
struct ListCommand {
commands @0 :List(Command.Command);
}
'';
}
Please use CamelCase for struct
and enum
names. The naming should reflect this manner:
-
if the schema is in folder
/edges/maths/boolean
then thestruct
should have nameMathsBoolean
. -
if the schema is in
fractal
/edges/net/http/response
then thestruct
should have nameNetHttpResponse
.
The same naming applies for Cap’n Proto enums
and interfaces
. It’s crucial this naming is adopted.
We prefer composition of schema, and the schema must have fully qualified struct names. Hence, this is example shouldn’t be used:
{ edge, edges }:
edge {
src = ./.;
edges = with edges; [ command ];
schema = with edges; ''
@0xf61e7fcd2b18d862;
struct Person {
name @0 :Text;
birthdate @3 :Date;
email @1 :Text;
phones @2 :List(PhoneNumber);
struct PhoneNumber {
number @0 :Text;
type @1 :Type;
enum Type {
mobile @0;
home @1;
work @2;
}
}
}
struct Date {
year @0 :Int16;
month @1 :UInt8;
day @2 :UInt8;
}
'';
}
The Date
name can collide!
Schema are pulled into agent
's scope just before compile time, now we are unable to predict what combinations will happen.
So if we have two schema that have struct Date …
then a name collision will take place.
Therefore to avoid this scenario please put struct Date …
into it’s own schema and import it via this mechanism.
Fractalide resolves transitive dependencies for you but you have to use this method:
{ edge, edges }:
edge {
src = ./.;
edges = with edges; [ command ];
schema = with edges; ''
@0xf61e7fcd2b18d862;
using CommandInstanceName = import "${command}/src/edge.capnp";
struct ListCommand {
commands @0 :List(CommandInstanceName.Command);
}
'';
}
You must pull explicitly mention the edge
you want to import via the ` edges = with edges; [ command ];`
Then you must import
it via this mechanism: using CommandInstanceName = import "${command}/src/edge.capnp";
and lastly use it … commands @0 :List(CommandInstanceName.Command); …
Out of curiosity what does the output of the above list_command
contract
function look like?
$ cat /nix/store/3s25icpbf1chayvrxwbyxr9qckn7x669-list_command/src/edge.capnp
@0xf61e7fcd2b18d862;
using CommandInstanceName = import "/nix/store/bgh37035cbr49r7mracmdwwjx9sbf4nr-command/src/edge.capnp";
struct ListCommand {
commands @0 :List(CommandInstanceName.Command);
}
The generated Rust code consists of the list_command
, command
and tuple
contract concatenated together.