mahler
Version:
A automated task composer and HTN based planner for building autonomous system agents
195 lines • 6.93 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Node = void 0;
exports.isValue = isValue;
exports.isFork = isFork;
exports.reduce = reduce;
exports.mapReduce = mapReduce;
exports.reverse = reverse;
exports.find = find;
exports.toString = toString;
exports.createFork = createFork;
exports.createJoin = createJoin;
exports.createValue = createValue;
const assert_1 = require("./assert");
function isValue(n) {
return n._tag === 'value';
}
function isFork(n) {
return n._tag === 'fork';
}
// Utility function to visit every node in the DAG in a
// depth first fashion, applying a reducer function to every
// node
function traverse(root, initial, reducer, exit = () => false, coords = { fork: 0, branch: 0, index: 0, depth: 0 }) {
if (root == null) {
// We have reached the end of the graph without
// triggering the exit condition
return { done: true, exited: false, acc: initial };
}
//
if (exit(initial)) {
// The exit condition was met, terminate the search
return { done: true, exited: true, acc: initial };
}
if (isValue(root)) {
return traverse(root.next, reducer(initial, root, coords), reducer, exit, {
...coords,
index: coords.index + 1,
depth: coords.depth + 1,
});
}
// Process the node, we need to increase the fork index before
// processing
if (isFork(root)) {
initial = reducer(initial, root, coords);
let res = null;
for (const [index, bNode] of root.next.entries()) {
const r = traverse(bNode, initial, reducer, exit, {
...coords,
index: 0,
branch: index,
fork: coords.fork + 1,
depth: coords.depth + 1,
});
if (r.done && r.exited) {
return r;
}
else if (!r.done) {
res = r;
}
initial = r.acc;
}
// Call the reducer once on the join node
let next = null;
if (res != null) {
(0, assert_1.assert)(!res.done);
initial = reducer(initial, res.node, coords);
next = res.node.next;
}
return traverse(next, initial, reducer, exit, {
...coords,
depth: coords.depth + 2,
});
}
// if the first node of the graph is a JOIN then
// we just ignore it
if (coords.depth === 0) {
return traverse(root.next, initial, reducer, exit, coords);
}
return { done: false, node: root, acc: initial };
}
/**
* Visit every node once, applying reducer function for every node and
* exiting early if an exit predicate is met.
*
* Iteration of the dag is done Depth-first. When a fork node is
* reached, branches are traversed in order until a join node (or a null) is found
* before continuing with the following branch
*/
function reduceWhile(node, initial, reducer = (acc) => acc, exit = () => false) {
const res = traverse(node, initial, reducer, exit);
return res.acc;
}
function reduce(node, reducer, initial) {
return reduceWhile(node, initial, (acc, n) => isValue(n) ? reducer(acc, n) : acc);
}
// Traverse the DAG following branches of a forking node in parallel
// and combining results after
function traverseCombine(root, initial, reducer, combiner, depth = 0) {
if (root == null) {
return { done: true, acc: initial };
}
if (isValue(root)) {
return traverseCombine(root.next, reducer(initial, root), reducer, combiner, depth + 1);
}
if (isFork(root)) {
// Call the reducer with the fork node first
initial = reducer(initial, root);
// Then call each branch independently
const ends = root.next.map((n) => traverseCombine(n, initial, reducer, combiner, depth + 1));
(0, assert_1.assert)(ends.length > 0, 'Malformed DAG found, empty Fork node');
const [res] = ends;
(0, assert_1.assert)(res != null); // Typescript is not smart enough to figure out that res cannot be undefined
(0, assert_1.assert)(!res.done, 'Malformed DAG found, disconnected fork branch');
// Combine the results from the branches passing the
// join node
const acc = combiner(ends.map((r) => r.acc), res.node);
return traverseCombine(res.node.next, acc, reducer, combiner, depth + 2);
}
// If the first node of the graph is a join, we ignore it
if (depth === 0) {
return traverseCombine(root.next, initial, reducer, combiner, depth);
}
return { done: false, node: root, acc: initial };
}
function reduceCombine(root, initial, reducer, combiner) {
// The any below is because typescript is being weird in interpreting the types
const res = traverseCombine(root, initial, reducer, combiner);
return res.acc;
}
function mapReduce(root, initial, mapper, reducer) {
return reduceCombine(root, initial, (acc, n) => (isValue(n) ? mapper(n, acc) : acc), reducer);
}
function reverse(root) {
return reduceCombine(root, null, (prev, n) => {
if (isValue(n)) {
n.next = prev;
return n;
}
return exports.Node.join(prev);
}, (nodes) => exports.Node.fork(nodes.filter((n) => n != null)));
}
function find(root, condition) {
return reduceWhile(root, null,
// Select the node if it matches the condition
(_, n) => (isValue(n) && condition(n) ? n : null),
// Exit as soon as the node is found
(v) => v != null);
}
function toString(root, toStr) {
function indentIf(r, cond = true) {
if (!cond) {
return '';
}
return ' '.repeat(r);
}
const res = reduceWhile(root, { str: '', indent: -2 }, (acc, node, { fork, branch, index }) => {
let str = acc.str;
if (index === 0 && fork > 0) {
// add a `~` to mark the first element of the branch
str += indentIf(acc.indent + 1, branch > 0) + '~ ';
}
if (isFork(node)) {
acc.str = str + indentIf(acc.indent + 2, index > 0) + '+ ';
// A new fork increases the base indent by 2
acc.indent += 2;
return acc;
}
if (isValue(node)) {
str += indentIf(acc.indent + 2, index > 0) + '- ';
str += toStr(node) + '\n';
acc.str = str;
return acc;
}
// An empty node resets the fork depth
acc.indent -= 2;
return acc;
});
return res.str.trim();
}
function createFork(next = []) {
return { _tag: 'fork', next };
}
function createJoin(next = null) {
return { _tag: 'join', next };
}
function createValue(data) {
return { _tag: 'value', next: null, ...data };
}
exports.Node = {
value: createValue,
fork: createFork,
join: createJoin,
};
//# sourceMappingURL=dag.js.map