UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

467 lines (466 loc) • 22.1 kB
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)); }