@sema-agent/core
Version:
Stateless, task-oriented AI agent core
58 lines (57 loc) • 3 kB
JavaScript
import { readSkillTable } from "../skill-grant.js";
import { assertNoRetiredKeys, RETIRED_TASK_SPEC_KEYS } from "../retired-keys.js";
import { skillBaseDirOf } from "./skill-base-dir.js";
const SKILLS_SEAT = "TaskSpec.skills";
const kindOf = (v) => (v === null ? "null" : Array.isArray(v) ? "an array" : typeof v);
function skillsSeatRefusal(field, problem) {
const e = new Error(`${field} ${problem} — an entry the engine cannot read is refused rather than dropped.`);
e.code = "config.invalid_argument";
e.detail = { field };
return e;
}
function readSkillFiles(files, at) {
if (files === undefined)
return undefined;
if (!Array.isArray(files))
throw skillsSeatRefusal(at, `must be a list of { path, content } attachments (got ${kindOf(files)})`);
const out = [];
for (const [j, f] of files.entries()) {
if (typeof f !== "object" || f === null || Array.isArray(f))
throw skillsSeatRefusal(`${at}[${j}]`, `must be an attachment object { path, content } (got ${kindOf(f)})`);
const { path, content } = f;
if (typeof path !== "string")
throw skillsSeatRefusal(`${at}[${j}].path`, `must be a string (got ${kindOf(path)})`);
if (typeof content !== "string")
throw skillsSeatRefusal(`${at}[${j}].content`, `must be a string (got ${kindOf(content)})`);
out.push({ path, content });
}
return out;
}
export function readSkillsSeat(skills) {
if (skills === undefined)
return [];
if (!Array.isArray(skills))
throw skillsSeatRefusal(SKILLS_SEAT, `must be a list (got ${kindOf(skills)})`);
assertNoRetiredKeys({ skills }, RETIRED_TASK_SPEC_KEYS, "TaskSpec");
const seen = new Set();
const out = [];
for (const [i, raw] of skills.entries()) {
const at = `${SKILLS_SEAT}[${i}]`;
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
throw skillsSeatRefusal(at, `must be an object { name, description, content } (got ${kindOf(raw)})`);
const { name, description, content, allowedTools, disallowedTools, files: rawFiles, baseDir, ...rest } = raw;
void rest;
for (const [member, value] of [["name", name], ["description", description], ["content", content]]) {
if (typeof value !== "string")
throw skillsSeatRefusal(`${at}.${member}`, `must be a string (got ${kindOf(value)})`);
}
const files = readSkillFiles(rawFiles, `${at}.files`);
skillBaseDirOf(baseDir);
if (seen.has(name))
continue;
seen.add(name);
const [allowed, denied] = [readSkillTable(allowedTools), readSkillTable(disallowedTools)];
out.push({ name, description, content, ...(allowed !== undefined ? { allowedTools: allowed } : {}), ...(denied !== undefined ? { disallowedTools: denied } : {}), ...(files !== undefined ? { files } : {}), ...(baseDir !== undefined ? { baseDir } : {}) });
}
return out;
}