@sema-agent/core
Version:
Stateless, task-oriented AI agent core
160 lines (159 loc) • 9.42 kB
JavaScript
import { PROJECT_CONTEXT_FRAMING } from "../../prompts/default.js";
import { MEMORY_ENGINE_TOOL_NAMES } from "../memory-engine/tools.js";
import { composeMemoryBlock } from "../memory.js";
import { hasConversationContent } from "../session.js";
import { createSharedMemoryTools } from "../shared-memory/tools.js";
import { SHARED_MEMORY_TOOL_NAMES } from "../shared-memory/types.js";
import { defineTool } from "../tools.js";
import { readFrozenSkillGrants, seedSkillGrants, skillSeatReader } from "../skill-grant.js";
import { allowedToolsNotGrantedNotice, seedAllowEntryTables } from "../allow-entry.js";
import { deliverEngineNotice } from "../engine-notice.js";
import { SKILL_CONTENT_MAX_CHARS, SKILL_TOOL_NAME, createSkillTool, normalizeSkills } from "./synthetic-tools.js";
import { skillLoadReading } from "./skill-base-dir.js";
import { RosterBuilder } from "../tool-roster.js";
import { asThrownError } from "../thrown-value.js";
export async function prepareProjectContext(input) {
const { deps, spec, internals, session, sessionId, hostTaskId, taskRootFinal, handsEnabled, delegation, seedContextFiles, memoryBlockFromEngine, skillGrants, allowedTools, allowedToolTables, roster, toolFaceSnapshot, onceLedger, memoryTools, memoryEngineSession, memorySearchToolsPlanned, memoryPairExcludedName, memoryPairOccupiedName, resume, skillListingBudget } = input;
const tools = roster.tools;
let memoryBlock = memoryBlockFromEngine;
let instructionSources;
let projectInstructionContent;
if (deps.loadProjectMemory) {
let projectMemoryPhase = spec.sessionId ? "resume" : "fresh";
if (spec.sessionId) {
try {
const branch = await session.getBranch();
const hasConversation = hasConversationContent(branch);
if (!hasConversation)
projectMemoryPhase = "fresh";
else {
for (let i = branch.length - 1; i >= 0; i--) {
const e = branch[i];
if (e.type === "compaction") {
projectMemoryPhase = "post-compact";
break;
}
if (e.type === "message" && e.message.role === "user")
break;
}
}
}
catch {
}
}
let loaded = null;
try {
loaded = await Promise.resolve(deps.loadProjectMemory({
cwd: taskRootFinal,
handsEnabled,
isSubagent: delegation.isNonForkChild,
...(internals?.agentName ? { agentName: internals.agentName } : {}),
sessionId,
phase: projectMemoryPhase,
}));
}
catch (err) {
deps.onError?.(asThrownError(err), { phase: "config", sessionId });
}
const projectMem = loaded !== null && typeof loaded === "object" ? loaded.content : loaded;
const seededFiles = loaded !== null && typeof loaded === "object" ? loaded.seededFiles : undefined;
const declaredSources = loaded !== null && typeof loaded === "object" ? loaded.instructionSources : undefined;
if (declaredSources !== undefined && declaredSources.length > 0)
instructionSources = declaredSources;
if (seededFiles?.length && seedContextFiles) {
try {
await seedContextFiles(seededFiles);
}
catch (err) {
deps.onError?.(asThrownError(err), { phase: "config", sessionId });
}
}
if (projectMem != null && projectMem.trim()) {
projectInstructionContent = projectMem;
const projectBlock = `${PROJECT_CONTEXT_FRAMING}\n\n${composeMemoryBlock(projectMem, "project")}`;
memoryBlock = memoryBlock ? `${memoryBlock}\n\n${projectBlock}` : projectBlock;
}
}
const declaredSkillRank = new Map();
for (const s of spec.skills ?? [])
if (!declaredSkillRank.has(s.name))
declaredSkillRank.set(s.name, declaredSkillRank.size);
const skillSpecs = normalizeSkills(spec.skills ?? []).filter((s) => {
const { loads, delivered } = skillLoadReading(s, SKILL_CONTENT_MAX_CHARS);
if (loads)
return true;
deps.onError?.(new Error(`Skill "${s.name}" content is ${delivered} chars${delivered === s.content.length ? "" : ` once its base directory resolves (${s.content.length} as written)`}, over the ${SKILL_CONTENT_MAX_CHARS}-char load limit — skill not loaded (skills are never truncated; shrink the body or move material to attachments/files).`), { phase: "config", sessionId });
return false;
});
const frozenSkillGrants = readFrozenSkillGrants(resume?.seed.skillGrants);
if (frozenSkillGrants === undefined)
throw new Error("the resumed checkpoint's skill-grant section does not read as grant rows — refusing to seed the run without the refusals it carries (the resume screen refuses this row before the claim)");
const inert = seedAllowEntryTables(allowedToolTables, { skillInert: seedSkillGrants(skillGrants, { skills: skillSpecs, userInvokedSkills: spec.userInvokedSkills, inherited: [...(internals?.inheritedSkillGrants ?? []), ...frozenSkillGrants] }), inherited: internals?.inheritedAllowedTools, request: allowedTools, deployment: deps.allowedTools });
if (inert.length > 0)
deliverEngineNotice(deps.onNotice, allowedToolsNotGrantedNotice(inert, sessionId));
if (skillSpecs.length > 0) {
if (tools.some((t) => t.name === SKILL_TOOL_NAME)) {
const e = new Error(`Tool name "${SKILL_TOOL_NAME}" is reserved when spec.skills is present.`);
e.code = "config.reserved_tool_name";
throw e;
}
roster.mount(createSkillTool(skillSpecs, skillGrants), { source: "builtin", mountedBy: "optional" });
}
const skillSeatOf = skillSeatReader(skillSpecs);
const sharedMemoryProvider = deps.sharedMemoryStores;
let sharedMemoryPairMounted = false;
if (sharedMemoryProvider !== undefined) {
const excluded = new Set(toolFaceSnapshot.exclude ?? []);
const mountedNameDomain = new Set();
for (const t of tools) {
mountedNameDomain.add(t.name);
for (const alias of t.aliases ?? [])
mountedNameDomain.add(alias);
}
const excludedName = SHARED_MEMORY_TOOL_NAMES.find((n) => excluded.has(n));
const occupiedName = SHARED_MEMORY_TOOL_NAMES.find((n) => mountedNameDomain.has(n));
if (excludedName !== undefined || occupiedName !== undefined) {
onceLedger.onError(new Error(`Shared memory tools ${SHARED_MEMORY_TOOL_NAMES.join("/")} were NOT mounted: ` +
(excludedName !== undefined
? `excludeTools removes "${excludedName}"`
: `this task already declares a tool named "${occupiedName}" (the built-in yields to it)`) +
" — the pair mounts together or not at all."), { phase: "config", sessionId, classification: "shared-memory-not-mounted" });
}
else {
roster.mountAll(createSharedMemoryTools({
provider: sharedMemoryProvider,
context: { sessionId, taskId: hostTaskId, ...(spec.principal !== undefined ? { principal: spec.principal } : {}) },
}).map((s) => defineTool(s)), { source: "builtin", mountedBy: "memory" });
sharedMemoryPairMounted = true;
}
}
let memoryEnginePairMounted = false;
if (memoryTools !== undefined && memoryTools.length > 0) {
roster.mountAll(memoryTools.map((s) => defineTool(s)), { source: "builtin", mountedBy: "memory" });
memoryEnginePairMounted = true;
}
else if (memoryEngineSession !== undefined && !memorySearchToolsPlanned) {
onceLedger.onError(new Error(`Memory tools ${MEMORY_ENGINE_TOOL_NAMES.join("/")} were NOT mounted: ` +
(memoryPairExcludedName !== undefined
? `excludeTools removes "${memoryPairExcludedName}"`
: `this task already declares a tool named "${memoryPairOccupiedName}" (the built-in yields to it)`) +
" — these tools mount together or not at all."), { phase: "config", sessionId, classification: "memory-tools-not-mounted" });
}
const skillsListing = skillSpecs.length > 0
? {
entries: skillSpecs.map((s) => ({
name: s.name,
description: s.description,
...(s.files !== undefined ? { files: s.files.map((f) => ({ path: f.path })) } : {}),
...(declaredSkillRank.get(s.name) !== undefined ? { declaredRank: declaredSkillRank.get(s.name) } : {}),
})),
seedAnnounced: resume !== undefined || spec.sessionId !== undefined,
budget: skillListingBudget,
}
:
(resume?.seed.announcedListings?.skills?.length ?? 0) > 0 || spec.sessionId !== undefined
?
{ entries: [], seedAnnounced: true, budget: skillListingBudget }
: undefined;
return { memoryBlock, instructionSources, projectInstructionContent, skillSpecs, skillSeatOf, skillsListing, sharedMemoryPairMounted, memoryEnginePairMounted };
}