Q39: runs load only model-compatible skills and state each skill's contract

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 07:22:08 -04:00
1 parent f7530ba65b
commit 645ce686bb
8 files changed
+142 -34

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
import type { SkillManifest } from "./skill-manifest";
/** The provider and model a run talks to. */
export interface RunModel {
providerKey: string;
model: string | null;
}
/** True when a compatible_models pattern ("openai/*", "gpt-5.4") matches the run's model; no patterns means any model. */
export function modelCompatible(patterns: string[], run: RunModel): boolean {
if (patterns.length === 0) return true;
if (!run.model) return false;
const names = [`${run.providerKey}/${run.model}`, run.model];
return patterns.some((pattern) => {
const regex = new RegExp(`^${pattern.split("*").map(escapeRegex).join(".*")}$`, "i");
return names.some((name) => regex.test(name));
});
}
export function runModelLabel(run: RunModel): string {
return run.model ? `${run.providerKey}/${run.model}` : `${run.providerKey} (no model set)`;
}
/** One skill's prompt section, its declared risk and contract first. */
export function skillPart(manifest: SkillManifest, version: string, body: string): string {
const header = [`# Skill: ${manifest.name}`, `Version: ${version}`, `Risk: ${manifest.riskLevel}`];
const inputs = declaration(manifest.inputs);
const outputs = declaration(manifest.outputs);
if (inputs) header.push(`Expects as input: ${inputs}`);
if (outputs) header.push(`Must produce: ${outputs}`);
return [header.join("\n"), body].join("\n\n");
}
function declaration(value: unknown): string | null {
if (typeof value === "string") return value.trim() || null;
if (typeof value === "object" && value !== null && Object.keys(value).length > 0) return JSON.stringify(value);
return null;
}
function escapeRegex(text: string): string {
return text.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
}
+20 -21
View File
@@ -10,6 +10,7 @@ import { loadSkillManifest, type SkillManifest } from "./skill-manifest";
import { orderByRelevance } from "./relevance";
import { runManifestArtifactTest, verifyRevisionStructure, type SkillArtifactRow, type SkillTestRow } from "./skill-tests";
import { estimateTokens } from "./context-budget";
import { modelCompatible, runModelLabel, skillPart } from "./skill-runtime";
function slugify(value: string): string {
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -280,14 +281,6 @@ export class SkillService {
return [];
}
private carriesRetiredSkill(revisionIds: string[]): boolean {
if (revisionIds.length === 0) return false;
return Boolean(this.db.prepare(`
select 1 from skill_revisions sr join skill_definitions sd on sd.id = sr.skill_id
where sd.status <> 'active' and sr.id in (${revisionIds.map(() => "?").join(", ")}) limit 1
`).get(...revisionIds));
}
private appLanguage(): string {
const row = this.db.prepare("select language from app_state where id = 1").get() as { language: string } | undefined;
return row?.language === "zh" ? "zh" : "en";
@@ -301,13 +294,6 @@ export class SkillService {
* prompt instead of silently overflowing the model's context.
*/
resolveRuntimeSnapshot(input: ResolveAgentSkillsInput): string {
// Idempotent per run: a re-queued run reuses its frozen skill set unless a skill in it was retired since.
const existingSnapshot = this.db.prepare("select id, source_revision_ids from skill_runtime_snapshots where agent_id = ? and run_id = ?")
.get(input.agentId, input.runId) as { id: string; source_revision_ids: string } | undefined;
if (existingSnapshot) {
if (!this.carriesRetiredSkill(JSON.parse(existingSnapshot.source_revision_ids) as string[])) return existingSnapshot.id;
this.db.prepare("delete from skill_runtime_snapshots where id = ?").run(existingSnapshot.id);
}
const forcedRows = this.forcedSkillRows(input.orgId);
const boundRows = this.db.prepare(`
select
@@ -340,15 +326,16 @@ export class SkillService {
const tools = new Set<string>();
const revisionIds: string[] = [];
const omitted: string[] = [];
const incompatible: string[] = [];
let compiledTokens = 0;
for (const row of rows) {
if (!existsSync(row.object_key)) throw new Error(`Skill artifact is missing: ${row.object_key}`);
const manifest = JSON.parse(row.manifest_json) as SkillManifest;
const part = [
`# Skill: ${manifest.name}`,
`Version: ${row.version_semver}`,
readFileSync(row.object_key, "utf8").trim(),
].join("\n\n");
if (!modelCompatible(manifest.compatibleModels, input.runModel)) {
incompatible.push(`${manifest.name} (${manifest.compatibleModels.join(", ")})`);
continue;
}
const part = skillPart(manifest, row.version_semver, readFileSync(row.object_key, "utf8").trim());
const partTokens = estimateTokens(part);
if (compiledTokens + partTokens > input.maxTokens) {
omitted.push(manifest.name);
@@ -359,12 +346,24 @@ export class SkillService {
compiledParts.push(part);
compiledTokens += partTokens;
}
if (incompatible.length > 0) {
console.warn(`[skills] run ${input.runId}: skipped skills incompatible with ${runModelLabel(input.runModel)}: ${incompatible.join("; ")}`);
compiledParts.push(
`# Skills not loaded\n\nThese granted skills do not support this run's model (${runModelLabel(input.runModel)}): ${incompatible.join("; ")}.`,
);
}
if (omitted.length > 0) {
compiledParts.push(
`# Skills omitted for size\n\nThese granted skills did not fit this run's prompt budget: ${omitted.join(", ")}. Ask the CEO to trim grants if they are essential.`,
);
}
const compiledPrompt = compiledParts.join("\n\n---\n\n");
// Idempotent per run: a re-queued run keeps its snapshot while the resolved skill set is unchanged.
const existing = this.db.prepare("select id, compiled_prompt from skill_runtime_snapshots where agent_id = ? and run_id = ?")
.get(input.agentId, input.runId) as { id: string; compiled_prompt: string } | undefined;
if (existing?.compiled_prompt === compiledPrompt) return existing.id;
if (existing) this.db.prepare("delete from skill_runtime_snapshots where id = ?").run(existing.id);
const snapshotId = createId("srs");
this.db.prepare(`
insert into skill_runtime_snapshots(
@@ -375,7 +374,7 @@ export class SkillService {
snapshotId,
input.agentId,
input.runId,
compiledParts.join("\n\n---\n\n"),
compiledPrompt,
JSON.stringify({ requiredTools: [...tools].sort() }),
JSON.stringify(revisionIds),
new Date().toISOString(),
+4 -3
View File
@@ -10,6 +10,7 @@ import { ChatService } from "./chat-service";
import { deliverChatReplyIfMissing } from "./chat-agent-wake-service";
import { SkillService } from "./skill-service";
import { runAgentLoop, runTools } from "./agent-run-loop";
import type { RunModel } from "./skill-runtime";
import type { AgentToolContext } from "./agent-tools";
import {
buildAgentInstructions,
@@ -147,7 +148,7 @@ export class TaskExecutionService {
let providerRequestId: string | null = null;
let skillRuntimeSnapshot: SkillRuntimeSnapshotRow | null = null;
try {
skillRuntimeSnapshot = this.resolveSkillRuntimeSnapshot(orgId, agent.id, runId, skillBudgetTokens(provider.context_window_tokens), `${task.title}\n${task.description}`);
skillRuntimeSnapshot = this.resolveSkillRuntimeSnapshot(orgId, agent.id, runId, skillBudgetTokens(provider.context_window_tokens), `${task.title}\n${task.description}`, { providerKey: provider.provider_key, model: provider.model });
// Watermark for reply delivery and mid-run message polling.
const runStartedAt = new Date().toISOString();
const chatContext = loadChatWakeContext(this.db, orgId, agent.id, isolation.secretScopeJson);
@@ -390,9 +391,9 @@ export class TaskExecutionService {
`).run(TASK_RUN_STATE.FAILED, providerRequestId, new Date().toISOString(), errorMessage, runId, orgId);
}
private resolveSkillRuntimeSnapshot(orgId: string, agentId: string, runId: string, maxTokens: number, taskText: string): SkillRuntimeSnapshotRow | null {
private resolveSkillRuntimeSnapshot(orgId: string, agentId: string, runId: string, maxTokens: number, taskText: string, runModel: RunModel): SkillRuntimeSnapshotRow | null {
if (!this.skills.hasRuntimeSkills(orgId, agentId)) return null;
const snapshotId = this.skills.resolveRuntimeSnapshot({ orgId, agentId, runId, maxTokens, taskText });
const snapshotId = this.skills.resolveRuntimeSnapshot({ orgId, agentId, runId, maxTokens, taskText, runModel });
const snapshot = this.db.prepare(`
select id, compiled_prompt, tools_manifest, source_revision_ids
from skill_runtime_snapshots