UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

282 lines (281 loc) • 13.7 kB
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)), }; }