@sema-agent/core
Version:
Stateless, task-oriented AI agent core
467 lines (466 loc) • 22.1 kB
JavaScript
import { literalTildeSpelling } from "../../shell/lex.js";
import { expandsOf, partPattern, readOfText, rewritten } from "./shell-read-word.js";
import { arithmeticSubstitutions } from "./shell-read-bindings.js";
import { argvProgramOf, attachedOptionPayloads, isPathShapedToken } from "./bash-readonly-classifier.js";
import { assignmentArgumentsAreNotPaths, deliversOperands, evaluatesOperands, messageWords, moduleTailOf, objectPathsOf, opensNoOperand, programRow, programTextMayOpen, programTextOpensOf, readArgs, readOptionSlot, selectsContentsTraversal, slotValueCandidates, subcommandOf, valuesAreLiteral, walksCwdWithoutOperand, wordsReadBySomeImplementations, } from "./program-read-model.js";
export function pathSubcommandOf(row, args) {
const sub = subcommandOf(row, args);
return sub !== undefined && (sub.row.operands !== undefined || sub.row.traversal !== undefined) ? sub : undefined;
}
function slotOperandAt(row, words) {
for (const e of readArgs(row, words, { firstOperandSlot: true }))
if (e.kind === "operand" && e.slot !== undefined)
return e.at;
return undefined;
}
function mayOwnNextWord(row, tok) {
if (tok.startsWith("--"))
return !tok.includes("=");
for (let k = 2; k <= tok.length; k++)
if (readOptionSlot(row, tok.slice(0, k)).form === "unexplained")
return k === tok.length;
return false;
}
const rootValue = (spec) => spec?.walks === true || spec?.delivers === "operands";
class RootsGathered {
positional = [];
places = [];
maybeValues = new Set();
stdinOperands = 0;
slotOwned = false;
unsettled = false;
slotWord;
value(r, e, namesPath) {
if (!e.consumed) {
this.maybeValues.add(r);
if (e.unsettled === true)
this.unsettled = true;
}
else if (namesPath && rootValue(e.spec))
this.positional.push(r);
}
attached(named, spec) {
if (rootValue(spec))
for (const c of named)
if (c.denyOnly !== true)
this.places.push({ ...c, provenance: "traversal_root" });
}
slot(r) {
this.slotWord = r;
if (this.maybeValues.has(r))
this.slotOwned = true;
}
operand(r, namesPath, stdin) {
if (stdin) {
if (!this.maybeValues.has(r))
this.stdinOperands++;
}
else if (namesPath)
this.positional.push(r);
}
restart() {
this.positional.length = 0;
this.places.length = 0;
this.stdinOperands = 0;
this.slotOwned = false;
this.unsettled = false;
this.slotWord = undefined;
}
roots(row) {
const leftOver = this.positional.filter((r) => !this.maybeValues.has(r)).length + this.places.length + this.stdinOperands;
const reads = this.unsettled
? [...this.positional, ...(this.slotWord !== undefined ? [this.slotWord] : []), ...(walksCwdWithoutOperand(row) ? [readOfText(".")] : [])]
: walksCwdWithoutOperand(row) && leftOver <= (this.slotOwned ? 1 : 0) ? [...this.positional, readOfText(".")] : [...this.positional];
return [...reads.map((r) => bare(r, "traversal_root")), ...this.places];
}
}
export const programTextSentence = (language) => `runs inline ${language} program text, which may open files this station does not read`;
const deliveredOperandsSentence = "takes the files it works on from a list file, whose names this station does not read";
function noteDeliveredOperands(row, words, ctx) {
if (deliversOperands(row, words))
ctx.programText ??= deliveredOperandsSentence;
}
export function bare(r, provenance, spelled = r.folded) {
const whole = spelled === r.folded;
const pattern = whole ? r.pattern : partPattern(r, spelled);
return {
spelled,
raw: r.raw,
provenance,
expands: r.expands,
...(pattern !== undefined ? { pattern } : {}),
...(whole && r.homeFloor !== undefined ? { floorSpelled: r.homeFloor } : {}),
...(whole && r.floorValues !== undefined ? { floors: r.floorValues } : {}),
};
}
export function roleReads(r, provenance, value, role, globs, ctx, alsoOperand = false) {
if (role.kind === "program-text")
return alsoOperand ? [] : programTextReads(r, provenance, value, role.language, ctx);
if (role.kind === "object-path")
return [bare(r, provenance, value), ...objectPathsOf(value).map((c) => ({ ...bare(r, "object_path", c), denyOnly: true }))];
const named = slotValueCandidates(value, role, globs);
if (named === undefined)
return [{ ...bare(r, provenance), expands: "unresolvable" }];
if (role.kind === "mount-source")
return named.flatMap((c) => [bare(r, "mount_source", c), bare(r, "traversal_root", c)]);
return named.map((c) => bare(r, provenance, c));
}
function programTextReads(r, provenance, text, language, ctx) {
ctx.programTextSeen = true;
const opens = programTextOpensOf(language);
if (opens === "arithmetic") {
ctx.reentered.push(...arithmeticSubstitutions(text, ctx.table));
return [];
}
if (opens === "parsed" || opens === "never")
return [];
if (programTextMayOpen(language, text))
ctx.programText ??= programTextSentence(language);
return [{ ...bare(r, provenance, text), denyOnly: true }];
}
export function pushAssignment(reads, w, provenance) {
const a = w.assignment;
if (a === undefined || a.form === "compound")
return;
const expandsTilde = a.value.expansions.some((p) => p.expansion?.kind === "tilde");
const value = expandsTilde ? a.value.folded : literalTildeSpelling(a.value.folded);
const settled = a.op === "=" && !expandsTilde;
const named = a.value.folded === "" || (a.op === "+=" ? expandsTilde : !settled) ? undefined : value;
if (a.value.folded === "" && settled)
return;
const v = { spelled: value, raw: a.bare ? a.value.raw : w.raw, provenance };
const unsettled = provenance !== "env_assignment" ? { ...v, expands: "unresolvable" } : v.spelled.startsWith("~") && !v.spelled.includes(":") ? { ...v, expands: expandsOf(a.value) } : undefined;
if (!settled && unsettled !== undefined)
reads.push(unsettled);
const substitutes = a.value.expansions.some((p) => p.expansion?.kind === "command" || p.expansion?.kind === "process" || p.expansion?.kind === "arithmetic");
const pattern = named === undefined ? undefined : partPattern({ folded: w.folded, ...(w.judged !== undefined ? { pattern: w.judged } : {}) }, named);
if (named !== undefined && !substitutes)
reads.push({ spelled: named, raw: w.raw, provenance, expands: expandsOf(w), ...(pattern !== undefined ? { pattern } : {}) });
if (a.value.folded.includes(":"))
for (const m of a.value.folded.split(":"))
if (m !== "") {
const r = readOfText(m);
reads.push({ spelled: r.folded, raw: r.raw, provenance, expands: r.expands, denyOnly: true });
}
}
export function unlistedWords(reads, toks, from, launcherIndices, provenance, ctx, program) {
const argv = program !== undefined ? { program: program.basename, from: program.index } : argvProgramOf(toks.map((r) => r.word));
const argvStart = argv?.from ?? toks.length;
const slotVerb = argv?.program;
const slotRow = slotVerb === undefined ? undefined : programRow(slotVerb);
const noRow = slotVerb !== undefined && slotRow === undefined;
const speculate = (start, how) => {
if (!noRow)
return;
for (let i = start; i < reads.length; i++) {
const c = reads[i];
if (c.expands === "unresolvable" || c.speculative !== undefined || c.denyOnly === true || c.provenance === "program_path" || c.provenance === "env_assignment")
continue;
reads[i] = { ...c, speculative: how };
}
};
const message = slotVerb === undefined || argv === undefined ? new Set() : messageWords(slotVerb, toks, argvStart);
let declaredNotPaths = assignmentArgumentsAreNotPaths(slotVerb ?? "");
const noPathOperands = slotVerb !== undefined && opensNoOperand(slotVerb);
const expanding = new Set();
for (let j = from; j < toks.length; j++) {
if (launcherIndices.has(j) || message.has(j))
continue;
if (noPathOperands && j > argvStart && !evaluatesOperands(slotVerb))
break;
if (toks[j].expands !== "unresolvable")
continue;
expanding.add(j);
reads.push(bare(toks[j], j === argvStart ? "program_path" : provenance));
if (j < argvStart)
pushAssignment(reads, toks[j].word, "env_assignment");
else if (j > argvStart && !declaredNotPaths)
pushAssignment(reads, toks[j].word, provenance);
}
const gathered = new RootsGathered();
let slotWord;
const pastOperand = new Set();
let globs = "expanded";
let walkRow = slotRow;
let walkFrom = argvStart + 1;
let relocated = false;
if (argvStart >= toks.length) {
let endOfOptions = false;
for (let j = from; j < toks.length; j++) {
if (launcherIndices.has(j) || expanding.has(j) || message.has(j))
continue;
const r = toks[j];
const word = r.folded;
if (j > from && !endOfOptions && word === "--") {
endOfOptions = true;
continue;
}
if (!endOfOptions && word.startsWith("-") && word !== "-") {
for (const payload of attachedOptionPayloads(word))
reads.push(bare(r, provenance, payload));
if (isPathShapedToken(word))
reads.push(bare(r, provenance));
continue;
}
reads.push(bare(r, provenance));
pushAssignment(reads, r.word, "env_assignment");
}
}
else if (!noPathOperands) {
const programWord = toks[argvStart];
if (!expanding.has(argvStart) && !launcherIndices.has(argvStart) && !message.has(argvStart)) {
const word = programWord.folded;
if (isPathShapedToken(word)) {
if (word.startsWith("-") && word !== "-") {
for (const payload of attachedOptionPayloads(word))
reads.push(bare(programWord, provenance, payload));
reads.push(bare(programWord, provenance));
}
else {
reads.push(bare(programWord, "program_path"));
if (!declaredNotPaths)
pushAssignment(reads, programWord.word, provenance);
}
}
}
const args = toks.slice(argvStart + 1).map((r) => r.folded);
const sub = pathSubcommandOf(slotRow, args);
const tail = moduleTailOf(slotRow, args);
const skipped = (j) => launcherIndices.has(j) || expanding.has(j) || message.has(j) || (tail !== undefined && j >= argvStart + 1 + tail.from);
const walk = (row, first, last, slotKind) => {
const at = (k) => first + k;
const words = toks.slice(first, last).map((r, k) => ({ folded: r.folded, skip: skipped(at(k)) }));
globs = valuesAreLiteral(row, words.map((w) => w.folded)) ? "literal" : "expanded";
noteDeliveredOperands(row, words.map((w) => w.folded), ctx);
const someRead = wordsReadBySomeImplementations(row, words.map((w) => w.folded));
const slotAt = slotOperandAt(row, words);
slotWord = slotAt === undefined ? undefined : at(slotAt);
for (const e of readArgs(row, words, { firstOperandSlot: slotKind, ownedValues: true })) {
const start = reads.length;
handleUnlistedEvent(e, at(e.at));
if (someRead.has(e.at))
for (let i = start; i < reads.length; i++)
if (reads[i].expands !== "unresolvable" && reads[i].denyOnly !== true)
reads[i] = { ...reads[i], speculative: "containment" };
}
};
if (sub !== undefined) {
walk(slotRow, argvStart + 1, argvStart + 1 + sub.at, false);
declaredNotPaths = sub.row.operands?.assignmentArguments === "not-paths";
walkRow = sub.row;
walkFrom = argvStart + 2 + sub.at;
relocated = sub.relocated;
gathered.restart();
walk(sub.row, walkFrom, toks.length, true);
}
else
walk(slotRow, argvStart + 1, toks.length, "program-text");
if (tail !== undefined)
for (let j = argvStart + 1 + tail.from; j < toks.length; j++)
if (!expanding.has(j))
reads.push(...roleReads(toks[j], "operand", toks[j].folded, tail.role, globs, ctx));
}
function handleUnlistedEvent(e, j) {
const r = toks[j];
const start = reads.length;
switch (e.kind) {
case "end-of-options":
return;
case "dash-word-operand":
pastOperand.add(j);
reads.push(bare(r, provenance));
speculate(start, "whole");
return;
case "value": {
if (!e.consumed) {
gathered.value(r, e, false);
if (r.expands !== "unresolvable")
reads.push(...roleReads(r, "option_value", r.folded, e.role, globs, ctx, true));
return;
}
if (expanding.has(j))
return;
const named = roleReads(r, "option_value", r.folded, e.role, globs, ctx);
reads.push(...named);
gathered.value(r, e, named.some((c) => c.denyOnly !== true));
return;
}
case "option":
if (e.reading === "no-value")
return;
if (e.reading === "speculative") {
for (const payload of attachedOptionPayloads(r.folded))
reads.push(bare(r, provenance, payload));
speculate(start, "containment");
const whole = reads.length;
if (isPathShapedToken(r.folded))
reads.push(bare(r, provenance));
speculate(whole, "whole");
return;
}
{
const named = roleReads(r, "option_value", literalTildeSpelling(e.reading.attached), e.reading.role, globs, ctx, pastOperand.has(j));
reads.push(...named);
gathered.attached(named, e.reading.spec);
}
return;
case "operand": {
const role = e.dash === undefined ? (e.slot ?? e.role) : undefined;
const named = role !== undefined ? roleReads(r, provenance, r.folded, role, globs, ctx) : [bare(r, provenance)];
const inSlot = j === slotWord;
if (inSlot)
gathered.slot(r);
else
gathered.operand(r, named.some((c) => c.denyOnly !== true), e.dash === "stdin");
reads.push(...named);
speculate(start, r.word.assignment !== undefined ? "whole" : "containment");
const values = reads.length;
if (!declaredNotPaths && e.slot === undefined)
pushAssignment(reads, r.word, provenance);
speculate(values, "containment");
return;
}
}
}
if (walkRow !== undefined && argv !== undefined && selectsContentsTraversal(walkRow, toks.slice(walkFrom).map((x) => x.folded))) {
for (const root of gathered.roots(walkRow))
reads.push(relocated ? { ...root, expands: "unresolvable" } : root);
}
for (let j = from; j < toks.length; j++) {
if (launcherIndices.has(j) || j === argvStart)
continue;
if (noPathOperands && j > argvStart)
break;
const start = reads.length;
if (toks[j].expands === "glob")
reads.push(bare(toks[j], provenance));
speculate(start, "containment");
}
}
export function listedVerbCandidates(name, argReads, reads, ctx) {
if (opensNoOperand(name))
return;
const row = programRow(name);
const args = argReads.map((r) => r.folded);
const expanding = new Set();
argReads.forEach((r, k) => {
if (!rewritten(r))
return;
expanding.add(k);
reads.push({ ...bare(r, "operand"), expands: "unresolvable" });
});
const sub = pathSubcommandOf(row, args);
const tail = moduleTailOf(row, args);
if (sub !== undefined) {
walkListed(row, argReads, 0, sub.at, expanding, reads, ctx, false);
walkListed(sub.row, argReads, sub.at + 1, argReads.length, expanding, reads, ctx, sub.relocated);
}
else
walkListed(row, argReads, 0, tail !== undefined ? tail.from : argReads.length, expanding, reads, ctx, false);
if (tail !== undefined)
for (let k = tail.from; k < argReads.length; k++)
if (!expanding.has(k))
reads.push(...roleReads(argReads[k], "operand", args[k], tail.role, "expanded", ctx));
for (let k = 0; k < args.length; k++) {
if (expanding.has(k))
continue;
if (argReads[k].expands === "glob")
reads.push(bare(argReads[k], "operand"));
}
}
function walkListed(row, argReads, first, last, expanding, reads, ctx, relocated) {
const slice = argReads.slice(first, last);
const args = slice.map((r) => r.folded);
const gathered = new RootsGathered();
let unexplained = false;
const globs = valuesAreLiteral(row, args) ? "literal" : "expanded";
noteDeliveredOperands(row, args, ctx);
const someRead = wordsReadBySomeImplementations(row, args);
const pastOperand = new Set();
const offer = (k, value, role, provenance, alsoOperand = false) => {
const named = roleReads(slice[k], provenance, value, role, globs, ctx, alsoOperand);
reads.push(...named);
return named;
};
const opensPath = (named) => named.some((c) => c.denyOnly !== true);
for (const e of readArgs(row, args.map((folded, k) => ({ folded, skip: expanding.has(first + k) })), { firstOperandSlot: true, ownedValues: true })) {
const t = args[e.at];
const start = reads.length;
switch (e.kind) {
case "end-of-options":
break;
case "dash-word-operand":
pastOperand.add(e.at);
reads.push(bare(slice[e.at], "operand"));
break;
case "value": {
if (expanding.has(first + e.at))
break;
gathered.value(slice[e.at], e, opensPath(offer(e.at, t, e.role, "option_value", !e.consumed)));
break;
}
case "option":
if (e.reading === "no-value")
break;
if (e.reading === "speculative") {
if (row === undefined || mayOwnNextWord(row, t))
unexplained = true;
for (const payload of attachedOptionPayloads(t))
reads.push(bare(slice[e.at], "option_value", payload));
break;
}
gathered.attached(offer(e.at, literalTildeSpelling(e.reading.attached), e.reading.role, "option_value", pastOperand.has(e.at)), e.reading.spec);
break;
case "operand": {
if (e.dash !== undefined) {
if (e.slot !== undefined) {
gathered.slot(slice[e.at]);
break;
}
gathered.operand(slice[e.at], true, e.dash === "stdin");
break;
}
if (e.slot !== undefined) {
offer(e.at, t, e.slot, "operand");
if (unexplained && e.slot.kind === "datum" && e.slot.form === "pattern")
reads.push(bare(slice[e.at], "operand"));
gathered.slot(slice[e.at]);
break;
}
let opens = true;
if (e.role !== undefined)
opens = opensPath(offer(e.at, t, e.role, "operand"));
else
reads.push(bare(slice[e.at], "operand"));
gathered.operand(slice[e.at], opens, false);
if (row?.operands?.assignmentArguments !== "not-paths")
pushAssignment(reads, slice[e.at].word, "operand");
break;
}
}
if (someRead.has(e.at))
for (let i = start; i < reads.length; i++)
if (reads[i].expands !== "unresolvable" && reads[i].denyOnly !== true)
reads[i] = { ...reads[i], speculative: "containment" };
}
if (row !== undefined && selectsContentsTraversal(row, args)) {
for (const root of gathered.roots(row))
reads.push(relocated ? { ...root, expands: "unresolvable" } : root);
}
}
export function subscriptOnly(word) {
const at = word.indexOf("[");
return at < 0 ? "" : word.slice(at);
}
export function assignmentArithmetic(w, integer, ctx, asWritten = false) {
const a = w.assignment;
if (a === undefined) {
if (!asWritten)
ctx.reentered.push(...arithmeticSubstitutions(subscriptOnly(w.folded), ctx.table));
return;
}
if (a.subscript !== undefined && !asWritten)
ctx.reentered.push(...arithmeticSubstitutions(`[${a.subscript.folded}]`, ctx.table));
if (integer)
ctx.reentered.push(...arithmeticSubstitutions(a.value.folded, ctx.table));
}