Fix runtime skill injection across adapters

This commit is contained in:
Dotta
2026-03-15 07:05:01 -05:00
parent 82f253c310
commit 7675fd0856
27 changed files with 506 additions and 222 deletions

View File

@@ -4,6 +4,8 @@ import { fileURLToPath } from "node:url";
import { and, asc, eq } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { companySkills } from "@paperclipai/db";
import { readPaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils";
import type { PaperclipSkillEntry } from "@paperclipai/adapter-utils/server-utils";
import type {
CompanySkill,
CompanySkillCreateRequest,
@@ -20,7 +22,6 @@ import type {
CompanySkillUsageAgent,
} from "@paperclipai/shared";
import { normalizeAgentUrlKey } from "@paperclipai/shared";
import { readPaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils";
import { findServerAdapter } from "../adapters/index.js";
import { resolvePaperclipInstanceRoot } from "../home-paths.js";
import { notFound, unprocessable } from "../errors.js";
@@ -959,11 +960,15 @@ export function companySkillService(db: Db) {
agent.companyId,
agent.adapterConfig as Record<string, unknown>,
);
const runtimeSkillEntries = await listRuntimeSkillEntries(agent.companyId);
const snapshot = await adapter.listSkills({
agentId: agent.id,
companyId: agent.companyId,
adapterType: agent.adapterType,
config: runtimeConfig,
config: {
...runtimeConfig,
paperclipRuntimeSkills: runtimeSkillEntries,
},
});
actualState = snapshot.entries.find((entry) => entry.name === slug)?.state
?? (snapshot.supported ? "missing" : "unsupported");
@@ -1219,6 +1224,56 @@ export function companySkillService(db: Db) {
return skillDir;
}
async function materializeRuntimeSkillFiles(companyId: string, skill: CompanySkill) {
const runtimeRoot = path.resolve(resolveManagedSkillsRoot(companyId), "__runtime__");
const skillDir = path.resolve(runtimeRoot, skill.slug);
await fs.rm(skillDir, { recursive: true, force: true });
await fs.mkdir(skillDir, { recursive: true });
for (const entry of skill.fileInventory) {
const detail = await readFile(companyId, skill.id, entry.path).catch(() => null);
if (!detail) continue;
const targetPath = path.resolve(skillDir, entry.path);
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.writeFile(targetPath, detail.content, "utf8");
}
return skillDir;
}
async function listRuntimeSkillEntries(companyId: string): Promise<PaperclipSkillEntry[]> {
await ensureBundledSkills(companyId);
const rows = await db
.select()
.from(companySkills)
.where(eq(companySkills.companyId, companyId))
.orderBy(asc(companySkills.name), asc(companySkills.slug));
const out: PaperclipSkillEntry[] = [];
for (const row of rows) {
const skill = toCompanySkill(row);
const sourceKind = asString(getSkillMeta(skill).sourceKind);
let source = normalizeSkillDirectory(skill);
if (!source) {
source = await materializeRuntimeSkillFiles(companyId, skill).catch(() => null);
}
if (!source) continue;
const required = sourceKind === "paperclip_bundled";
out.push({
name: skill.slug,
source,
required,
requiredReason: required
? "Bundled Paperclip skills are always available for local adapters."
: null,
});
}
out.sort((left, right) => left.name.localeCompare(right.name));
return out;
}
async function importPackageFiles(companyId: string, files: Record<string, string>): Promise<CompanySkill[]> {
await ensureBundledSkills(companyId);
const normalizedFiles = normalizePackageFileMap(files);
@@ -1330,5 +1385,6 @@ export function companySkillService(db: Db) {
importFromSource,
importPackageFiles,
installUpdate,
listRuntimeSkillEntries,
};
}

View File

@@ -22,6 +22,7 @@ import type { AdapterExecutionResult, AdapterInvocationMeta, AdapterSessionCodec
import { createLocalAgentJwt } from "../agent-auth-jwt.js";
import { parseObject, asBoolean, asNumber, appendWithCap, MAX_EXCERPT_BYTES } from "../adapters/utils.js";
import { costService } from "./costs.js";
import { companySkillService } from "./company-skills.js";
import { secretService } from "./secrets.js";
import { resolveDefaultAgentWorkspaceDir } from "../home-paths.js";
import { summarizeHeartbeatRunResultJson } from "./heartbeat-run-summary.js";
@@ -555,6 +556,7 @@ function resolveNextSessionState(input: {
export function heartbeatService(db: Db) {
const runLogStore = getRunLogStore();
const secretsSvc = secretService(db);
const companySkills = companySkillService(db);
const issuesSvc = issueService(db);
const activeRunExecutions = new Set<string>();
@@ -1463,6 +1465,11 @@ export function heartbeatService(db: Db) {
agent.companyId,
mergedConfig,
);
const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(agent.companyId);
const runtimeConfig = {
...resolvedConfig,
paperclipRuntimeSkills: runtimeSkillEntries,
};
const issueRef = issueId
? await db
.select({
@@ -1761,7 +1768,7 @@ export function heartbeatService(db: Db) {
runId: run.id,
agent,
runtime: runtimeForAdapter,
config: resolvedConfig,
config: runtimeConfig,
context,
onLog,
onMeta: onAdapterMeta,