UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

160 lines (159 loc) • 9.42 kB
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 }; }