Session history was silently clipped to the first 4000 characters of the last 12 messages, so follow-ups could not see timing or audit tables. Keep an append-only tail plus a checkpoint summary, retry overflow in the same request, and expose cache hit rate in admin usage. Co-authored-by: Cursor <cursoragent@cursor.com>
177 lines
7.5 KiB
TypeScript
177 lines
7.5 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import type { Processor } from "@mastra/core/processors";
|
|
import {
|
|
resolveLiveJyotishSkill,
|
|
resolveLiveJyotishSkillRuntimePath,
|
|
} from "../lib/skill-package-registry.ts";
|
|
import { sharedConsultationMethodMarkdown } from "../lib/consultation-methodology.ts";
|
|
|
|
const skill = resolveLiveJyotishSkill();
|
|
|
|
export const jyotishSkillPackage = skill;
|
|
|
|
/**
|
|
* Mastra-named alias of the live SKILL.md / references / scripts / assets.
|
|
* Not a hashed snapshot: leftover `versions/` trees stay off this path.
|
|
*/
|
|
export const jyotishSkillRuntimePath = resolveLiveJyotishSkillRuntimePath(skill);
|
|
|
|
/**
|
|
* Headings from the commercial SKILL.md that govern answering a natal chart.
|
|
* The rest of that file is a local-agent / maintainer manual (CLI indexes,
|
|
* construction rules, celebrity case catalogs) and stays in the package for
|
|
* skill_read rather than being stuffed into every consultation prompt.
|
|
*/
|
|
const RUNTIME_METHOD_HEADINGS = [
|
|
"商业运行时路由",
|
|
"关联技法完整调取",
|
|
"强制工作流",
|
|
"五层硬约束",
|
|
"强制规则",
|
|
"当前最硬的未闭环点",
|
|
"核心方法论",
|
|
"强制规范速查",
|
|
"注意事项",
|
|
] as const;
|
|
|
|
const DROPPED_RUNTIME_SUBHEADINGS = ["开源复用边界冻结"] as const;
|
|
|
|
function publishedSkillBody(): string {
|
|
const raw = readFileSync(resolve(skill.resolvedPath, "SKILL.md"), "utf8");
|
|
const frontmatter = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(raw);
|
|
return (frontmatter ? raw.slice(frontmatter[0].length) : raw).trim();
|
|
}
|
|
|
|
function dropSubheadings(section: string, dropped: readonly string[]): string {
|
|
const lines = section.split("\n");
|
|
const kept: string[] = [];
|
|
let skipping = false;
|
|
for (const line of lines) {
|
|
if (line.startsWith("### ")) {
|
|
skipping = dropped.some((heading) => line.includes(heading));
|
|
} else if (line.startsWith("## ")) {
|
|
skipping = false;
|
|
}
|
|
if (!skipping) kept.push(line);
|
|
}
|
|
return kept.join("\n").trim();
|
|
}
|
|
|
|
/**
|
|
* Mastra answers a skill activation with the entrypoint *and* a flat listing of
|
|
* every file in the package, which for this package measured 129,651 bytes: the
|
|
* method is 30,507 of them and the remaining 99,144 are 1,592 bare paths with no
|
|
* description. Nothing in a request carries over to the next one - the agent is
|
|
* rebuilt per request, no thread memory is configured, and the replayed history
|
|
* is plain question/answer text - so the model re-activated the skill on every
|
|
* turn and paid for that listing every time, twice more whenever a retry opened
|
|
* a fresh model loop.
|
|
*
|
|
* Binding the runtime excerpt into the system prompt is what the listing was
|
|
* standing in for. It also removes an activation the model could forget, which
|
|
* is what `runtime_contract_incomplete` was mostly reporting. The excerpt is
|
|
* taken from the live commercial entrypoint the operator maintains.
|
|
*/
|
|
function boundMethod(): string {
|
|
const body = publishedSkillBody();
|
|
if (body.length === 0) {
|
|
throw new Error(`Skill ${skill.name} has no method body to bind`);
|
|
}
|
|
|
|
const lines = body.split("\n");
|
|
const preamble: string[] = [];
|
|
const sections: string[][] = [];
|
|
let current: string[] | null = null;
|
|
for (const line of lines) {
|
|
if (line.startsWith("## ")) {
|
|
if (current) sections.push(current);
|
|
current = [line];
|
|
} else if (current) {
|
|
current.push(line);
|
|
} else {
|
|
preamble.push(line);
|
|
}
|
|
}
|
|
if (current) sections.push(current);
|
|
|
|
const kept = sections.flatMap((section) => {
|
|
const heading = section[0] ?? "";
|
|
if (!RUNTIME_METHOD_HEADINGS.some((wanted) => heading.includes(wanted))) return [];
|
|
const text = dropSubheadings(section.join("\n"), DROPPED_RUNTIME_SUBHEADINGS);
|
|
return text.length > 0 ? [text] : [];
|
|
});
|
|
|
|
const excerpt = [...(preamble.join("\n").trim() ? [preamble.join("\n").trim()] : []), ...kept].join("\n\n");
|
|
if (excerpt.length === 0) {
|
|
throw new Error(`Skill ${skill.name} has no runtime method sections to bind`);
|
|
}
|
|
return excerpt;
|
|
}
|
|
|
|
const BOUND_METHOD_MARKER = `<jyotish-skill name="${skill.name}">`;
|
|
|
|
export const jyotishSkillMethodBlock = `The jyotish-vedic-astrology skill is already loaded. Its runtime method is quoted below from the live skill the operator maintains; there is no activation step, no hashed package, and no tool that loads it. Follow this method and its truth boundaries. For career, wealth, marriage, and family answers, present its Level 2 report template in the chat body after a 3-6 sentence spoken reply with no heading (raw structure, six-step houses, Yoga table, timing, synthesis, Technique Audit Table, then a short modern wrap). Construction notes, CLI indexes, and case catalogs stay in the skill tree and are not part of this block.
|
|
<jyotish-skill name="${skill.name}">
|
|
${boundMethod()}
|
|
</jyotish-skill>
|
|
<jyotish-shared-method>
|
|
${sharedConsultationMethodMarkdown()}
|
|
</jyotish-shared-method>`;
|
|
|
|
/**
|
|
* Withdraw the activation tools while keeping `skill_read`.
|
|
*
|
|
* `providesSkillDiscovery: "on-demand"` is Mastra's declaration that the caller
|
|
* owns skill discovery and instruction loading, which is now literally true.
|
|
* Measured against this package: `skills` alone yields skill, skill_search and
|
|
* skill_read; with this marker present it yields skill_read only, so the long
|
|
* tail of references the method names by path stays reachable.
|
|
*
|
|
* The declaration is the point, but a processor has to do something, so it does
|
|
* the thing that only it is positioned to do: confirm the method really did
|
|
* arrive in the system prompt. Withdrawing the activation tool means an agent
|
|
* assembled without the method block would no longer fail loudly - it would
|
|
* simply answer without method, which is the failure this change exists to
|
|
* remove. A prompt that cannot be read is left alone; only one that can be read
|
|
* and has no method in it stops the run.
|
|
*
|
|
* Every step is checked rather than the first one: the chart-answering agents
|
|
* that bind this skill share this object, so remembering that one of them had
|
|
* its method would stop the others from ever being asked.
|
|
*/
|
|
export const jyotishSkillBoundProcessor: Processor & { processInputStep: NonNullable<Processor["processInputStep"]> } = {
|
|
id: "jyotish-skill-bound",
|
|
name: "Jyotish Skill Bound",
|
|
providesSkillDiscovery: "on-demand",
|
|
processInputStep({ messageList, abort }) {
|
|
const system = messageList?.getAllSystemMessages?.();
|
|
if (!Array.isArray(system) || system.length === 0) return;
|
|
if (!collectStrings(system).includes(BOUND_METHOD_MARKER)) {
|
|
abort(`Jyotish skill method is not bound into the system prompt for ${skill.name}`);
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Every string anywhere in a system message, joined. A system message's content
|
|
* is a string in some shapes and a list of parts in others, and serialising the
|
|
* whole thing instead would escape the quotes in the marker and never match it.
|
|
*/
|
|
function collectStrings(value: unknown, found: string[] = []): string {
|
|
if (typeof value === "string") found.push(value);
|
|
else if (Array.isArray(value)) for (const item of value) collectStrings(item, found);
|
|
else if (value && typeof value === "object") for (const item of Object.values(value)) collectStrings(item, found);
|
|
return found.join("\n");
|
|
}
|
|
|
|
/** Chart-answering agents bind the runtime excerpt this way. General and
|
|
* onboarding agents do not: they cannot make natal claims. Fresh arrays per
|
|
* agent, because the loader takes ownership of the ones it is handed. */
|
|
export function jyotishSkillBinding() {
|
|
return {
|
|
skills: [jyotishSkillRuntimePath],
|
|
inputProcessors: [jyotishSkillBoundProcessor],
|
|
};
|
|
}
|