@sema-agent/core
Version:
Stateless, task-oriented AI agent core
282 lines (281 loc) • 13.7 kB
JavaScript
import { argvOf, lexShell } from "../../shell/lex.js";
import { shellWordsOf } from "../../shell/shell-words.js";
import { resolveOperandLexically } from "./bash-readonly-classifier.js";
import { readProgramPosition } from "./bash-program-position.js";
import { valueOf } from "./shell-read-bindings.js";
import { evalDefinedFunctions, functionCallOf } from "./shell-read-functions.js";
import { codeBodies } from "./shell-reentry-text.js";
import { isAbsoluteForFamily } from "./safety.js";
const TEXT_RUNNING_PROGRAMS = new Set(["eval", "source", "."]);
const BASE_CANDIDATE_MAX = 256;
const tableSays = (name, at, ctx) => ctx.textBinds(name) || at.table.commandSets.has(name);
export class WorkdirThread {
readable;
candidates;
lostAt = false;
floors;
physical;
movedAt = -1;
constructor(start, readable, startsPhysical = false) {
this.readable = readable;
this.physical = startsPhysical;
this.lostAt = start === "unmodelled";
this.candidates = start === "unmodelled" ? [] : start.map((c) => (typeof c === "string" ? { base: c, skippedCd: -1, ...(readable && isAbsoluteForFamily("own", c) ? { pwd: c } : {}), dirStack: [] } : { ...c, skippedCd: -1 }));
}
get lost() {
return this.lostAt;
}
get floorBases() {
return this.floors;
}
reachable(andRunStart) {
return this.candidates.filter((c) => c.skippedCd < andRunStart);
}
end() {
return this.lostAt ? "unmodelled" : this.candidates;
}
where(andRunStart) {
if (this.lostAt || !this.readable)
return { PWD: "unknown", OLDPWD: "unknown", movedAt: this.movedAt };
const reachable = this.reachable(andRunStart);
const values = (pick) => {
const out = new Set();
for (const c of reachable) {
const v = pick(c);
if (v === undefined)
return "unknown";
out.add(v);
}
return [...out].map((spelled) => ({ spelled }));
};
return { PWD: values((c) => c.pwd), OLDPWD: values((c) => c.old), movedAt: this.movedAt };
}
setPhysical() {
this.physical = true;
}
get mayBePhysical() {
return this.physical;
}
lose(at, reachable, floorTargets = []) {
this.movedAt = Math.max(this.movedAt, at);
this.lostAt = true;
this.floors ??= [...new Set([...reachable.map((c) => c.base), ...floorTargets])];
}
shift(si, at, ctx) {
if (this.lostAt) {
if (this.floors !== undefined && this.floors.length > 0)
this.floors = [...new Set([...this.floors, ...floorOf(at.shift, this.reachable(at.andRunStart), ctx)])];
return;
}
this.movedAt = si;
const reachable = this.reachable(at.andRunStart);
const s = at.shift;
if (s.kind === "lost")
return this.lose(si, reachable);
if (s.kind === "stack") {
this.candidates = this.candidates.map(({ dirStack: _, ...c }) => c);
return;
}
const branched = [];
for (const c of this.candidates) {
if (c.skippedCd < at.andRunStart) {
const moved = this.movedFrom(c, at, ctx);
if (moved === "lost")
return this.lose(si, reachable, floorOf(s, reachable, ctx));
for (const m of moved)
branched.push({ ...m, skippedCd: c.skippedCd });
}
branched.push({ ...c, skippedCd: at.provable ? si : c.skippedCd });
}
const byKey = new Map();
for (const c of branched) {
const key = JSON.stringify([c.base, c.pwd, c.old, c.dirStack]);
const prior = byKey.get(key);
if (prior === undefined || c.skippedCd < prior.skippedCd)
byKey.set(key, c);
}
const next = [...byKey.values()];
if (next.length > BASE_CANDIDATE_MAX)
this.lose(si, reachable);
else
this.candidates = next;
}
movedFrom(c, at, ctx) {
const s = at.shift;
if (s.kind === "eval") {
const ran = ctx.evalEnd(s.text, c, at.table);
if (ran.physical)
this.physical = true;
return ran.end === "unmodelled" ? "lost" : ran.end.map((e) => (this.physical ? withoutPwd(e) : e));
}
const old = tableSays("PWD", at, ctx) ? {} : c.pwd !== undefined ? { old: c.pwd } : {};
const land = (base, physical, stack) => ({ base, skippedCd: c.skippedCd, ...(physical || this.physical || !this.readable ? {} : { pwd: base }), ...old, ...(stack !== undefined ? { dirStack: stack } : {}) });
if (s.kind === "swap" || s.kind === "pop") {
const top = c.dirStack?.[0];
if (top === undefined)
return "lost";
return [land(top, false, s.kind === "swap" ? [c.base, ...c.dirStack.slice(1)] : c.dirStack.slice(1))];
}
if (s.kind !== "cd")
return "lost";
const stack = s.push === true ? (c.dirStack === undefined ? undefined : [c.base, ...c.dirStack]) : c.dirStack;
const targets = targetsOf(s.target, c, at, ctx);
if (targets === "lost")
return "lost";
const out = [];
for (const t of targets) {
if ((s.physical || this.physical || c.pwd === undefined) && t.split("/").includes(".."))
return "lost";
const moved = resolveOperandLexically(c.base, t, ctx.homeDir);
if (moved === undefined)
return "lost";
out.push(land(moved, s.physical, stack));
}
return out;
}
}
const withoutPwd = ({ pwd: _, ...c }) => c;
function floorOf(s, reachable, ctx) {
if (s.kind !== "cd")
return [];
if (s.target === "home")
return ctx.homeDir !== undefined ? [ctx.homeDir] : [];
if (typeof s.target === "string")
return [];
const target = s.target;
return [...(target.floorSpelled !== undefined ? [target.floorSpelled] : []), ...(target.expands === false ? reachable.flatMap((c) => resolveOperandLexically(c.base, target.spelled, ctx.homeDir) ?? []) : [])];
}
function targetsOf(target, c, at, ctx) {
if (target === "home")
return settledValues(at.table.stateOf("HOME"));
if (target === "previous")
return tableSays("OLDPWD", at, ctx) ? settledValues(at.table.stateOf("OLDPWD")) : c.old === undefined ? "lost" : [c.old];
if (target.expands !== false)
return "lost";
const dir = target.spelled;
if (!searchesCdPath(dir))
return [dir];
if (at.cdable && /^[A-Za-z_][A-Za-z0-9_]*$/.test(dir))
return "lost";
const cdPath = settledValues(at.table.stateOf("CDPATH"));
if (cdPath === "lost")
return "lost";
const entries = cdPath.flatMap((v) => (v === "" ? [] : v.split(":")));
return [...new Set([...entries.map((e) => (e === "" ? dir : `${e.replace(/\/+$/, "")}/${dir}`)), dir])];
}
export function searchesCdPath(dir) {
return dir !== "" && !isAbsoluteForFamily("own", dir) && !/^\.\.?(?:\/|$)/.test(dir) && !dir.startsWith("~");
}
function settledValues(state) {
if (state === "unknown" || state.some((v) => v.glob === true))
return "lost";
return state.map((v) => v.spelled);
}
export function baseShiftOf(program, args, targetOf) {
if (program === "cd") {
let physical = false;
let at = 0;
for (; at < args.length; at++) {
const w = args[at].folded;
if (w === "--") {
at++;
break;
}
if (!w.startsWith("-") || w === "-")
break;
for (const letter of w.slice(1)) {
if (letter === "P")
physical = true;
else if (letter === "L")
physical = false;
else if (letter !== "e" && letter !== "@")
return { shift: { kind: "lost" }, reported: "unmodelled" };
}
}
if (at >= args.length)
return { shift: { kind: "cd", target: "home", physical }, reported: "home" };
if (args[at].folded === "-")
return { shift: { kind: "cd", target: "previous", physical }, reported: "previous" };
const target = targetOf(args[at]);
return { shift: { kind: "cd", target, physical }, reported: target };
}
if (shellWordsOf("stack_mover").has(program)) {
const words = args.map((a) => a.folded);
if (words.some((w) => w === "-n"))
return { shift: { kind: "stack" }, reported: "unmodelled" };
if (words.some((w) => /^[+-]\d+$/.test(w)) || words.some((w) => w.startsWith("-") && w !== "--"))
return { shift: { kind: "lost" }, reported: "unmodelled" };
const operand = args.find((a) => a.folded !== "--");
if (program === "popd")
return { shift: operand === undefined ? { kind: "pop" } : { kind: "lost" }, reported: "unmodelled" };
if (operand === undefined)
return { shift: { kind: "swap" }, reported: "unmodelled" };
return { shift: { kind: "cd", target: targetOf(operand), physical: false, push: true }, reported: targetOf(operand) };
}
if (shellWordsOf("stack_editor").has(program) && args.some((a) => a.folded.startsWith("-") || a.folded.startsWith("+")))
return { shift: { kind: "stack" } };
return undefined;
}
export function textMayMove(text, functions) {
const lexed = lexShell(text);
if ("parseFailure" in lexed)
return true;
return lexed.segments.some((s) => segmentMayMove(argvOf(s), functions));
}
const programOf = (argv) => {
const pos = readProgramPosition(argv);
return pos.program === undefined ? undefined : { name: pos.program.basename, args: argv.slice(pos.program.index + 1).map((w) => w.folded) };
};
const setsPhysical = (p) => p?.name === "set" && p.args.some((a, k) => /^-[A-Za-z]*P/.test(a) || (a === "-o" && p.args[k + 1] === "physical"));
export function textSetsPhysical(text) {
const lexed = lexShell(text);
return !("parseFailure" in lexed) && lexed.segments.some((s) => setsPhysical(programOf(argvOf(s))));
}
export function withCommandEnvironment(table, argv) {
const overlay = new Map();
const overlaid = () => ({ ...table, stateOf: (name) => overlay.get(name) ?? table.stateOf(name) });
for (const k of readProgramPosition(argv).assignments) {
const a = argv[k].assignment;
if (a === undefined)
continue;
if (a.op !== "=" || a.subscript !== undefined || a.form !== "scalar" || a.value.unresolved !== undefined) {
overlay.set(a.name, "unknown");
continue;
}
const before = valueOf({ pieces: a.value.pieces }, table, true);
const ordered = valueOf({ pieces: a.value.pieces }, overlaid(), true);
overlay.set(a.name, before === "unknown" || ordered === "unknown" ? "unknown" : [...before, ...ordered.filter((v) => !before.some((b) => b.spelled === v.spelled))]);
}
return { ...overlaid(), commandSets: new Set(overlay.keys()) };
}
export function textCalls(text, functions) {
const lexed = lexShell(text);
if ("parseFailure" in lexed)
return true;
return lexed.segments.some((s) => functionCallOf(argvOf(s), functions) !== undefined);
}
export function segmentMayMove(argv, functions) {
const pos = readProgramPosition(argv);
if (pos.program === undefined)
return false;
const word = argv[pos.program.index];
return shellWordsOf("base_mover").has(pos.program.basename) || TEXT_RUNNING_PROGRAMS.has(pos.program.basename) || functionCallOf(argv, functions) !== undefined || word.pieces.some((p) => p.expansion !== undefined || p.literal === undefined);
}
export function textMovesOf(defined, argvs, inFunctionBody, inherited) {
const unread = new Set([...evalDefinedFunctions(argvs), ...(inherited?.unread ?? [])]);
const functions = new Set([...defined, ...unread, ...(inherited?.functions ?? [])]);
const programs = argvs.map(programOf);
const mayMove = argvs.map((argv) => segmentMayMove(argv, functions));
const bodyMoves = mayMove.some((m, si) => m && inFunctionBody(si));
const bodiesAt = programs.map((p) => (p === undefined ? [] : codeBodies(p.name, p.args)));
const mayCallAt = argvs.map((argv) => functionCallOf(argv, functions) !== undefined);
const runsTextAt = argvs.map((argv) => { const pos = readProgramPosition(argv); const word = pos.program === undefined ? undefined : argv[pos.program.index]; return word !== undefined && (["eval", "source", "."].includes(pos.program.basename) || word.pieces.some((p) => p.expansion !== undefined || p.literal === undefined)); });
const callAt = (si, table) => functionCallOf(argvs[si], functions, (name) => table.functionState(name));
const callsUnread = (si, table) => functionCallOf(argvs[si], unread, (name) => table.functionState(name)) !== undefined;
return {
functions, unread, mayMove, mayCallAt, runsBodyAt: mayCallAt.map((c, si) => c || runsTextAt[si] || bodiesAt[si].length > 0), bodiesAt, bodyMoves, callAt, callsUnread,
physicalAt: programs.map(setsPhysical),
cdableFrom: programs.findIndex((p) => p?.name === "shopt" && p.args.includes("-s") && p.args.includes("cdable_vars")),
losesBaseAt: (si, table) => (callAt(si, table) !== undefined && bodyMoves) || callsUnread(si, table) || bodiesAt[si].some((b) => bodyMoves || textMayMove(b, functions)),
};
}