import { getFrontmatterValue, getSkillNameFromPath, parseFrontmatter, } from "../../resources/metadata.js"; import { ensurePersonalDefaults, organizationIdFromResourceOwner, resourceGet, resourceGetByPath, resourceList, resourceListAccessible, SHARED_OWNER, sharedResourceOwner, WORKSPACE_OWNER, } from "../../resources/store.js"; import type { ContextGovernanceTier, ContextManifestSourceRef, ContextSystemProvenance, } from "../../shared/context-xray.js"; import { discoverAgents } from "../agent-discovery.js"; import { getRequestOrgId } from "../request-context.js"; import { parseSkillFrontmatter } from "./skill-frontmatter.js"; // --------------------------------------------------------------------------- // System-prompt resource loading: AGENTS.md, instructions/*.md, skills // summaries, and the shared/workspace resource index. Assembled by // `loadResourcesForPrompt`, the top-level orchestrator called once per // request to build the "here's what you should know" context block. // --------------------------------------------------------------------------- const SHARED_PROMPT_RESOURCE_MAX_CHARS = 30_000; export const COMPACT_PROMPT_RESOURCE_MAX_CHARS = 6_000; const COMPACT_PROMPT_RESOURCES_TOTAL_MAX_CHARS = 48_000; const PROMPT_CONTEXT_PROVIDER_MAX_CHARS = 8_000; export interface PromptContextProviderContext { owner: string; compact: boolean; selfAppId?: string; orgId: string | null; } export interface PromptContextProviderContribution { content: string; label?: string; provenance?: ContextSystemProvenance; governance?: ContextGovernanceTier; sourceRef?: ContextManifestSourceRef; } export interface PromptContextProvider { id: string; load( context: PromptContextProviderContext, ): | Promise | PromptContextProviderContribution | null | undefined; } const promptContextProviders = new Map(); /** Register a package-owned, request-scoped system-context contribution. */ export function registerPromptContextProvider( provider: PromptContextProvider, ): () => void { const id = provider.id.trim(); if (!/^[a-z][a-z0-9-]{1,63}$/.test(id)) { throw new Error( "Prompt context provider ids must be 2-64 lowercase letters, digits, or hyphens", ); } const registered = { ...provider, id }; promptContextProviders.set(id, registered); return () => { if (promptContextProviders.get(id) === registered) { promptContextProviders.delete(id); } }; } function xmlAttributeEscape(value: string): string { return value .replace(/&/g, "&") .replace(/"/g, """) .replace(//g, ">"); } async function loadPromptContextProviderBlocks( context: PromptContextProviderContext, ): Promise { const providers = [...promptContextProviders.values()].sort((a, b) => a.id.localeCompare(b.id), ); const settled = await Promise.allSettled( providers.map(async (provider) => ({ provider, contribution: await provider.load(context), })), ); const blocks: string[] = []; for (const result of settled) { if (result.status !== "fulfilled") continue; const { provider, contribution } = result.value; if (!contribution) continue; const content = contribution.content.trim(); if (!content) continue; const boundedContent = content.length <= PROMPT_CONTEXT_PROVIDER_MAX_CHARS ? content : `${content.slice(0, PROMPT_CONTEXT_PROVIDER_MAX_CHARS)}\n[truncated after ${PROMPT_CONTEXT_PROVIDER_MAX_CHARS.toLocaleString()} characters]`; const label = contribution.label?.trim() || provider.id; const provenance = contribution.provenance ?? "runtime-context"; const governance = contribution.governance ?? "inherited"; const sourceScope = contribution.sourceRef?.scope?.trim() || "provider"; const sourcePath = contribution.sourceRef?.path?.trim() || provider.id; blocks.push( `\n${boundedContent.replace(/<\/prompt-context-provider>/gi, "</prompt-context-provider>")}\n`, ); } return blocks; } export function compactPromptLine(value: string, maxChars: number): string { const line = value.replace(/\s+/g, " ").trim(); if (line.length <= maxChars) return line; return `${line.slice(0, maxChars - 1)}…`; } const SHARED_RESOURCE_INDEX_LIMIT = 40; const PROMPT_SKILL_SUMMARY_LIMIT = 40; const PROMPT_SKILL_METADATA_READ_LIMIT = 80; const PROMPT_INSTRUCTION_SUMMARY_LIMIT = 20; const PROMPT_SUMMARY_DESCRIPTION_MAX_CHARS = 180; export interface PromptResourceManifestSection { label: string; provenance: ContextSystemProvenance; governance: ContextGovernanceTier; content: string; sourceRef?: ContextManifestSourceRef; } function normalizeResourcePathForPrompt(path: string): string { return path.replace(/^\/+/, "").trim(); } function xmlAttribute(attrs: string, name: string): string | undefined { const match = new RegExp(`\\b${name}="([^"]*)"`).exec(attrs); return match?.[1]; } function resourceManifestClassification( scope: string, path: string, index: number, ): { provenance: ContextSystemProvenance; governance: ContextGovernanceTier; } { const normalizedPath = normalizeResourcePathForPrompt(path); if (scope === "template") { return { provenance: "template", governance: "inherited" }; } if (scope.startsWith("workspace")) { return { provenance: scope === "workspace" && index === 0 ? "enterprise-workspace-core" : "sql-workspace", governance: scope === "workspace" && index === 0 ? "required" : "inherited", }; } if (scope.startsWith("personal")) { return { provenance: normalizedPath === "memory/MEMORY.md" ? "memory" : "personal", governance: "user", }; } if (scope.startsWith("app-default")) { return { provenance: "legacy-app-default", governance: "inherited" }; } if (scope.startsWith("organization")) { return { provenance: "organization", governance: "inherited" }; } if (scope.startsWith("shared")) { return { provenance: normalizedPath === "LEARNINGS.md" ? "organization" : "legacy-app-default", governance: "inherited", }; } return { provenance: "template", governance: "inherited" }; } /** * Extracts bounded-in-memory metadata from the already-composed resource * prompt. The caller turns these inputs into hashed/previews before anything * is persisted; this helper never writes or returns the full manifest. */ export function promptResourceManifestSections( prompt: string, ): PromptResourceManifestSection[] { const sections: PromptResourceManifestSection[] = []; const resourcePattern = /]*)>([\s\S]*?)<\/resource>/g; let match: RegExpExecArray | null; let workspaceIndex = 0; let sharedIndex = 0; while ((match = resourcePattern.exec(prompt))) { const attrs = match[1] ?? ""; const scope = xmlAttribute(attrs, "scope") ?? "resource"; const path = xmlAttribute(attrs, "path") ?? xmlAttribute(attrs, "name") ?? ""; const name = xmlAttribute(attrs, "name") ?? (path || "resource"); const index = scope.startsWith("workspace") ? workspaceIndex++ : scope.startsWith("shared") ? sharedIndex++ : 0; const classification = resourceManifestClassification(scope, path, index); sections.push({ label: name, ...classification, content: match[2] ?? "", sourceRef: { ...(path ? { path } : {}), scope, }, }); } const providerPattern = /]*)>([\s\S]*?)<\/prompt-context-provider>/g; while ((match = providerPattern.exec(prompt))) { const attrs = match[1] ?? ""; const id = xmlAttribute(attrs, "id") ?? "prompt-context-provider"; const label = xmlAttribute(attrs, "label") ?? id; const provenanceValue = xmlAttribute(attrs, "provenance"); const governanceValue = xmlAttribute(attrs, "governance"); const provenance = ( [ "framework-core", "actions-prompt", "template", "enterprise-workspace-core", "sql-workspace", "legacy-app-default", "organization", "personal", "memory", "db-schema", "tools", "model-overlay", "runtime-context", ] as const ).includes(provenanceValue as ContextSystemProvenance) ? (provenanceValue as ContextSystemProvenance) : "runtime-context"; const governance = (["required", "inherited", "user"] as const).includes( governanceValue as ContextGovernanceTier, ) ? (governanceValue as ContextGovernanceTier) : "inherited"; sections.push({ label, provenance, governance, content: match[2] ?? "", sourceRef: { path: xmlAttribute(attrs, "path") ?? id, scope: xmlAttribute(attrs, "scope") ?? "provider", }, }); } const taggedSections = [ { tag: "skills-summary", label: "Workspace skills index", provenance: "template" as const, governance: "inherited" as const, }, { tag: "resource-skills", label: "Workspace resource skills", provenance: "template" as const, governance: "inherited" as const, }, { tag: "instruction-resources", label: "Instruction resources index", provenance: "sql-workspace" as const, governance: "inherited" as const, }, { tag: "workspace-resources", label: "Workspace reference resources", provenance: "sql-workspace" as const, governance: "inherited" as const, }, { tag: "context-note", label: "Resource availability note", provenance: "framework-core" as const, governance: "required" as const, }, { tag: "context-budget-note", label: "Context budget note", provenance: "framework-core" as const, governance: "required" as const, }, { tag: "available-apps", label: "Available workspace apps", provenance: "tools" as const, governance: "required" as const, }, ]; for (const tagged of taggedSections) { const pattern = new RegExp( `<${tagged.tag}\\b[^>]*>([\\s\\S]*?)`, "g", ); let taggedMatch: RegExpExecArray | null; while ((taggedMatch = pattern.exec(prompt))) { const taggedAttrs = taggedMatch[0]?.slice(tagged.tag.length + 1).split(">")[0] ?? ""; const taggedScope = xmlAttribute(taggedAttrs, "scope"); const taggedClassification = tagged.tag === "instruction-resources" && taggedScope ? resourceManifestClassification(taggedScope, "", 0) : { provenance: tagged.provenance, governance: tagged.governance, }; sections.push({ label: tagged.label, ...taggedClassification, content: taggedMatch[1] ?? "", ...(taggedScope ? { sourceRef: { scope: taggedScope } } : {}), }); } } return sections; } function resourceToolHint( action: "list" | "read" | "effective" | "write" | "delete" | "promote", extra?: string, ): string { return `Use the \`resources\` tool with \`action: "${action}"\`${extra ? `, ${extra}` : ""}.`; } function skillDocsSlug(name: string): string { return `skill-${name .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "")}`; } function ensureSentence(value: string): string { const trimmed = value.trim(); if (!trimmed) return ""; return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`; } function escapeXmlAttribute(value: string): string { return value.replace(/&/g, "&").replace(/"/g, """); } function truncatePromptResourceContent( content: string, path: string, maxChars = SHARED_PROMPT_RESOURCE_MAX_CHARS, readHint?: string, ): string { const trimmed = content.trim(); if (trimmed.length <= maxChars) return trimmed; const omitted = trimmed.length - maxChars; const hint = readHint ?? resourceToolHint( "read", `\`path: "${path}"\` and the resource's \`scope\` for the full content`, ); return `${trimmed.slice(0, maxChars)}\n\n[Resource ${path} truncated after ${maxChars.toLocaleString()} characters; ${omitted.toLocaleString()} characters omitted. ${hint}]`; } function promptResourceBlock(input: { name: string; scope: string; content: string; path?: string; maxChars?: number; readHint?: string; }): string | null { const normalizedPath = input.path ? normalizeResourcePathForPrompt(input.path) : undefined; const content = truncatePromptResourceContent( input.content, normalizedPath ?? input.name, input.maxChars, input.readHint, ); if (!content) return null; const pathAttr = normalizedPath ? ` path="${escapeXmlAttribute(normalizedPath)}"` : ""; return `\n${content}\n`; } /** * One assembled startup-context block plus the governance tier the context * manifest reports for it (`ContextGovernanceTier`). `required` is the tier * that already meant "not opt-out-able" in the manifest; the budget fitter * honours it as "reserve before anything else competes". */ export interface PromptSection { content: string; governance: ContextGovernanceTier; } export interface SkippedPromptSection { label: string; chars: number; } export interface PromptSectionBudgetResult { sections: string[]; skipped: SkippedPromptSection[]; /** Chars by which required sections alone exceeded `maxChars`, else 0. */ overflowChars: number; } const PROMPT_SECTION_SEPARATOR = "\n\n"; const TRIM_NOTE_LABELS_MAX_CHARS = 300; const TRIM_NOTE_MAX_CHARS = 700; function promptSectionLabel(content: string): string { const open = /^<([a-z][a-z0-9-]*)\b([^>]*)>/i.exec(content.trimStart()); if (!open) return "unlabelled context section"; const attrs = open[2] ?? ""; const name = xmlAttribute(attrs, "name") ?? xmlAttribute(attrs, "id"); const scope = xmlAttribute(attrs, "scope"); if (name) return scope ? `${name} (${scope})` : name; return scope ? `${open[1]} (${scope})` : (open[1] ?? "context section"); } function promptBudgetTrimNote( skipped: SkippedPromptSection[], maxChars: number, ): string { const labels = compactPromptLine( skipped.map((section) => section.label).join(", "), TRIM_NOTE_LABELS_MAX_CHARS, ); return `Your startup context was trimmed: ${skipped.length} section(s) did not fit the ${maxChars.toLocaleString()}-character first-request budget and were omitted (${labels}). Treat them as unread, not as absent — use \`resources\` with \`action: "list"\` or \`"read"\`, \`docs-search\`, and \`tool-search\` before telling the user something does not exist.`; } /** * Fits assembled startup context into `maxChars`, reserving `required` * sections before discretionary ones compete for the remainder, and reporting * every omission so an over-budget request is never silent. * * Required sections are never truncated and never dropped: a half-rendered * list of workspace apps or governance rules reads to the model as a complete * one, which is worse than an explicit note that a section is missing. If they * alone exceed the budget, the budget is exceeded deliberately, every * discretionary section is dropped, and `overflowChars` reports by how much. */ export function selectPromptSectionsWithinBudget( sections: PromptSection[], maxChars: number, ): PromptSectionBudgetResult { const required = sections.filter( (section) => section.governance === "required", ); const keep = new Set(); const skipped: SkippedPromptSection[] = []; let used = required.reduce( (total, section) => total + section.content.length, 0, ); let count = required.length; const requiredFit = used + count * PROMPT_SECTION_SEPARATOR.length + TRIM_NOTE_MAX_CHARS <= maxChars; for (const [index, section] of sections.entries()) { if (section.governance === "required") { keep.add(index); continue; } const projected = used + section.content.length + (count + 1) * PROMPT_SECTION_SEPARATOR.length + TRIM_NOTE_MAX_CHARS; if (requiredFit && projected <= maxChars) { keep.add(index); used += section.content.length; count++; } else { skipped.push({ label: promptSectionLabel(section.content), chars: section.content.length, }); } } const selected = sections .filter((_section, index) => keep.has(index)) .map((section) => section.content); if (skipped.length > 0) { selected.push(promptBudgetTrimNote(skipped, maxChars)); } const totalChars = selected.reduce((total, section) => total + section.length, 0) + Math.max(0, selected.length - 1) * PROMPT_SECTION_SEPARATOR.length; return { sections: selected, skipped, overflowChars: requiredFit ? 0 : Math.max(0, totalChars - maxChars), }; } function isAutoLoadedInstructionPath(path: string): boolean { const normalized = normalizeResourcePathForPrompt(path); return normalized.startsWith("instructions/") && normalized.endsWith(".md"); } function isSpecialPromptResourcePath(path: string): boolean { const normalized = normalizeResourcePathForPrompt(path); return ( normalized === "AGENTS.md" || normalized === "LEARNINGS.md" || normalized.startsWith("instructions/") || normalized.startsWith("skills/") || normalized.startsWith("agents/") || normalized.startsWith("remote-agents/") || normalized.startsWith("jobs/") || normalized.startsWith("memory/") ); } function isTextLikeResource(mimeType: string): boolean { return ( mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/yaml" || mimeType === "application/x-yaml" ); } function getResourceSummaryFromContent(content: string): string | null { const frontmatter = parseFrontmatter(content); const title = getFrontmatterValue(frontmatter, "title") || getFrontmatterValue(frontmatter, "name"); const description = getFrontmatterValue(frontmatter, "description"); if (title && description) return `${title}: ${description}`; if (title) return title; if (description) return description; const heading = content .split(/\r?\n/) .map((line) => line.trim()) .find((line) => /^#{1,3}\s+\S/.test(line)); if (heading) return heading.replace(/^#{1,3}\s+/, "").trim(); return null; } export function resourceScopeForOwner( owner: string, currentOwner?: string, ): string { if (owner === WORKSPACE_OWNER) return "workspace"; if (owner === SHARED_OWNER || organizationIdFromResourceOwner(owner)) { return "shared"; } if (currentOwner && owner === currentOwner) return "personal"; return "resource"; } async function loadAgentsResourceForPrompt( owner: string, scope: string, maxChars = SHARED_PROMPT_RESOURCE_MAX_CHARS, ): Promise { try { const agents = await resourceGetByPath(owner, "AGENTS.md"); if (!agents?.content?.trim()) return null; return promptResourceBlock({ name: "AGENTS.md", scope, path: "AGENTS.md", content: agents.content, maxChars, }); } catch { return null; } } async function loadInstructionResourcesForPrompt( owner: string, scope: string, maxChars = SHARED_PROMPT_RESOURCE_MAX_CHARS, summaryOnly = false, ): Promise { try { const resources = await resourceList(owner, "instructions/"); const sorted = resources .filter((resource) => isAutoLoadedInstructionPath(resource.path)) .sort((a, b) => a.path.localeCompare(b.path)); if (summaryOnly) { if (sorted.length === 0) return []; const resourceScope = scope.startsWith("workspace") ? "workspace" : scope.startsWith("personal") ? "personal" : "shared"; const listed = sorted.slice(0, PROMPT_INSTRUCTION_SUMMARY_LIMIT); const lines = listed.map( (resource) => `- \`${resource.path}\` - ${resourceToolHint("read", `\`path: "${resource.path}"\` and \`scope: "${resourceScope}"\` when it applies`)}`, ); if (sorted.length > listed.length) { lines.push( `- ...${sorted.length - listed.length} more instruction files. ${resourceToolHint("list", `\`scope: "${resourceScope}"\` and \`prefix: "instructions/"\``)}`, ); } return [ `\nDetailed instruction files are loaded on demand so the first model request stays compact. Read a relevant file before following its workflow.\n\n${lines.join("\n")}\n`, ]; } const fullResources = await Promise.all( sorted.map((resource) => resourceGet(resource.id).catch(() => null)), ); const blocks: string[] = []; for (let index = 0; index < sorted.length; index++) { const resource = sorted[index]!; const full = fullResources[index]; if (!full?.content?.trim()) continue; const block = promptResourceBlock({ name: resource.path, scope, path: resource.path, content: full.content, maxChars, }); if (block) blocks.push(block); } return blocks; } catch { return []; } } async function loadResourceSkillsPromptBlock( owner: string, orgId?: string | null, ): Promise { try { const organizationOwner = sharedResourceOwner(orgId); const resources = owner === SHARED_OWNER ? [ ...(await resourceList(SHARED_OWNER, "skills/")), ...(await resourceList(WORKSPACE_OWNER, "skills/")), ] : await resourceListAccessible(owner, "skills/", { orgId }); const sorted = resources.sort((a, b) => { const ownerOrder = (a.owner === owner ? 0 : a.owner === organizationOwner ? 1 : a.owner === SHARED_OWNER ? 2 : a.owner === WORKSPACE_OWNER ? 3 : 4) - (b.owner === owner ? 0 : b.owner === organizationOwner ? 1 : b.owner === SHARED_OWNER ? 2 : b.owner === WORKSPACE_OWNER ? 3 : 4); if (ownerOrder !== 0) return ownerOrder; return a.path.localeCompare(b.path); }); const skillCandidates = sorted.slice(0, PROMPT_SKILL_METADATA_READ_LIMIT); const loaded = await Promise.all( skillCandidates.map(async (resource) => ({ resource, full: await resourceGet(resource.id).catch(() => null), })), ); const seen = new Set(); const lines: string[] = []; for (const { resource, full } of loaded) { if (!full?.content) continue; const meta = parseSkillFrontmatter(full.content); if (meta.userInvocable === false) continue; if (meta.scope === "dev") continue; const name = meta.name || getSkillNameFromPath(resource.path); if (!name || seen.has(name)) continue; seen.add(name); const scope = resourceScopeForOwner(resource.owner, owner); const description = compactPromptLine( meta.description || "(no description)", PROMPT_SUMMARY_DESCRIPTION_MAX_CHARS, ); lines.push( `- \`${name}\` at resource \`${resource.path}\` (${scope}) - ${ensureSentence(description)} ${resourceToolHint( "read", `\`path: "${resource.path}"\` and \`scope: "${scope}"\` before starting a task it applies to`, )}`, ); if (lines.length >= PROMPT_SKILL_SUMMARY_LIMIT) break; } if (lines.length === 0) return null; if ( sorted.length > skillCandidates.length || loaded.length > PROMPT_SKILL_SUMMARY_LIMIT ) { lines.push( `- ...more skills omitted from the startup summary. ${resourceToolHint("list", '`prefix: "skills/"` to inspect the full catalog')}`, ); } return `\nThe following workspace skills are available in addition to codebase skills. They may come from SQL resources, Dispatch workspace resources, or local file mode. Read a matching skill before starting a task it applies to.\n\n${lines.join("\n")}\n`; } catch { return null; } } async function loadResourceIndexForPrompt( owner: string, scope: "workspace" | "shared", ): Promise { try { const resources = (await resourceList(owner)) .filter( (resource) => !isSpecialPromptResourcePath(resource.path) && isTextLikeResource(resource.mimeType), ) .sort((a, b) => a.path.localeCompare(b.path)); if (resources.length === 0) return null; const listed = resources.slice(0, SHARED_RESOURCE_INDEX_LIMIT); const lines: string[] = []; const fullResources = await Promise.all( listed.map((resource) => resourceGet(resource.id).catch(() => null)), ); for (let index = 0; index < listed.length; index++) { const resource = listed[index]!; const full = fullResources[index]; const summary = full?.content ? getResourceSummaryFromContent(full.content) : null; lines.push(`- \`${resource.path}\`${summary ? ` - ${summary}` : ""}`); } if (resources.length > listed.length) { lines.push( `- ...${resources.length - listed.length} more ${scope} resources. ${resourceToolHint( "list", `\`scope: "${scope}"\` to inspect them`, )}`, ); } const label = scope === "workspace" ? "Workspace reference resources are inherited by every app and are available for company, brand, positioning, persona, product, or domain context." : "Shared app/organization reference resources are available for app-specific or team context."; return `\n${label} ${resourceToolHint( "read", `\`path: \` and \`scope: "${scope}"\` when a task may depend on them`, )} Do not assume their contents without reading the relevant file.\n\n${lines.join("\n")}\n`; } catch { return null; } } /** * Pre-load the agent's context: AGENTS.md (workspace/template/runtime * instructions), the skills index, shared LEARNINGS.md (team notes), a shared * resource index, and memory/MEMORY.md (personal structured memory index). * These all get appended to the system prompt so the agent has everything it * needs from the first turn. * * Six sources are layered: * * 1. `` — AGENTS.md from the enterprise workspace core. * 2. `