om-memory-system 4.34.0-next.190 → 4.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,13 +30,17 @@ const SYSTEM = [
30
30
  "When the sources do not answer the question, say so briefly and set insufficient to true.",
31
31
  "Write plain, short British English. Call the answer tool.",
32
32
  ].join(" ");
33
+ const SKILL_SYSTEM = "Sources marked K, such as [K1], are facts about one agent skill: uses, failed turns, whether it was changed, and its recent changes. Cite them like any other source.";
33
34
  export function numberSources(found) {
34
35
  return [
35
36
  ...found.memories.map((source, i) => ({ ...source, label: `S${i + 1}` })),
36
37
  ...found.profile.map((source, i) => ({ ...source, label: `P${i + 1}` })),
38
+ ...(found.skills ?? []).map((source, i) => ({ ...source, label: `K${i + 1}` })),
37
39
  ];
38
40
  }
39
41
  function describeSource(source) {
42
+ if (source.kind === "skill")
43
+ return `[${source.label}] ${source.text.slice(0, MAX_SOURCE_CHARS)}`;
40
44
  const meta = source.kind === "memory"
41
45
  ? [source.projectName, source.tags?.length ? `tags: ${source.tags.join(", ")}` : ""]
42
46
  : [source.section, source.category];
@@ -54,9 +58,12 @@ export function buildAnswerPrompt(question, turns, sources) {
54
58
  ]
55
59
  .filter(Boolean)
56
60
  .join("\n\n");
57
- return { system: SYSTEM, user };
61
+ const system = sources.some((source) => source.kind === "skill")
62
+ ? `${SYSTEM} ${SKILL_SYSTEM}`
63
+ : SYSTEM;
64
+ return { system, user };
58
65
  }
59
- const MARKER = /\s*\[([SP]\d+)\]/g;
66
+ const MARKER = /\s*\[([SPK]\d+)\]/g;
60
67
  /**
61
68
  * Removes `[S9]`-style markers that name a source that was not sent, and lists
62
69
  * the valid ones in order of first use. Other bracketed text is left alone.
@@ -1,8 +1,10 @@
1
- import type { ChatPlanRecord, ChatRole } from "./chat-types.js";
1
+ import type { ChatPlanRecord, ChatRole, SkillIntent } from "./chat-types.js";
2
2
  export declare const MAX_PLAN_TAGS = 200;
3
3
  export declare const MAX_PLAN_TURNS = 6;
4
4
  export declare const MAX_TURN_CHARS = 500;
5
5
  export declare const MAX_PLAN_QUERIES = 3;
6
+ /** Skill names offered to the plan model, most used first. */
7
+ export declare const MAX_PLAN_SKILLS = 100;
6
8
  export interface PlanInput {
7
9
  question: string;
8
10
  turns: Array<{
@@ -14,6 +16,8 @@ export interface PlanInput {
14
16
  count: number;
15
17
  }>;
16
18
  categories: string[];
19
+ /** Safe skill names, most used first. Absent while skill facts are off. */
20
+ skills?: string[];
17
21
  }
18
22
  export declare const PLAN_TOOL: {
19
23
  readonly type: "function";
@@ -49,6 +53,54 @@ export declare const PLAN_TOOL: {
49
53
  };
50
54
  };
51
55
  };
56
+ /** The plan tool while skill facts are on: two optional fields on top of `PLAN_TOOL`. */
57
+ export declare const PLAN_TOOL_WITH_SKILLS: {
58
+ readonly type: "function";
59
+ readonly function: {
60
+ readonly parameters: {
61
+ readonly properties: {
62
+ readonly skills: {
63
+ readonly type: "array";
64
+ readonly items: {
65
+ readonly type: "string";
66
+ };
67
+ readonly description: "Skills from the given list that the question names or is about";
68
+ };
69
+ readonly skillIntent: {
70
+ readonly type: "string";
71
+ readonly enum: readonly SkillIntent[];
72
+ readonly description: "needs-work for skills that fail or need care, most-used for skills used most, otherwise none";
73
+ };
74
+ readonly queries: {
75
+ readonly type: "array";
76
+ readonly items: {
77
+ readonly type: "string";
78
+ };
79
+ readonly description: "1 to 3 short search phrases";
80
+ };
81
+ readonly tags: {
82
+ readonly type: "array";
83
+ readonly items: {
84
+ readonly type: "string";
85
+ };
86
+ readonly description: "Tags from the given list that match the question";
87
+ };
88
+ readonly profileCategories: {
89
+ readonly type: "array";
90
+ readonly items: {
91
+ readonly type: "string";
92
+ };
93
+ readonly description: "Profile categories from the given list that match the question";
94
+ };
95
+ };
96
+ readonly type: "object";
97
+ readonly required: readonly ["queries", "tags", "profileCategories"];
98
+ };
99
+ readonly name: "plan_search";
100
+ readonly description: "Choose how to search the user's stored memories and profile";
101
+ };
102
+ };
103
+ export declare function planTool(withSkills: boolean): typeof PLAN_TOOL | typeof PLAN_TOOL_WITH_SKILLS;
52
104
  export declare function buildPlanPrompt(input: PlanInput): {
53
105
  system: string;
54
106
  user: string;
@@ -2,6 +2,9 @@ export const MAX_PLAN_TAGS = 200;
2
2
  export const MAX_PLAN_TURNS = 6;
3
3
  export const MAX_TURN_CHARS = 500;
4
4
  export const MAX_PLAN_QUERIES = 3;
5
+ /** Skill names offered to the plan model, most used first. */
6
+ export const MAX_PLAN_SKILLS = 100;
7
+ const SKILL_INTENTS = ["none", "needs-work", "most-used"];
5
8
  export const PLAN_TOOL = {
6
9
  type: "function",
7
10
  function: {
@@ -30,6 +33,32 @@ export const PLAN_TOOL = {
30
33
  },
31
34
  },
32
35
  };
36
+ /** The plan tool while skill facts are on: two optional fields on top of `PLAN_TOOL`. */
37
+ export const PLAN_TOOL_WITH_SKILLS = {
38
+ type: "function",
39
+ function: {
40
+ ...PLAN_TOOL.function,
41
+ parameters: {
42
+ ...PLAN_TOOL.function.parameters,
43
+ properties: {
44
+ ...PLAN_TOOL.function.parameters.properties,
45
+ skills: {
46
+ type: "array",
47
+ items: { type: "string" },
48
+ description: "Skills from the given list that the question names or is about",
49
+ },
50
+ skillIntent: {
51
+ type: "string",
52
+ enum: [...SKILL_INTENTS],
53
+ description: "needs-work for skills that fail or need care, most-used for skills used most, otherwise none",
54
+ },
55
+ },
56
+ },
57
+ },
58
+ };
59
+ export function planTool(withSkills) {
60
+ return withSkills ? PLAN_TOOL_WITH_SKILLS : PLAN_TOOL;
61
+ }
33
62
  const SYSTEM = [
34
63
  "You plan a search over one user's saved memories of past coding sessions and their profile.",
35
64
  "Return 1 to 3 short search phrases that would find memories answering the question.",
@@ -37,6 +66,11 @@ const SYSTEM = [
37
66
  "Pick tags and profile categories only from the lists given. Pick none when none fit.",
38
67
  "Call plan_search. Do not answer the question.",
39
68
  ].join(" ");
69
+ const SKILL_SYSTEM = [
70
+ "The question may also be about the user's agent skills.",
71
+ "Pick skills only from the Skills list. Set skillIntent to needs-work for questions about skills that fail or need care,",
72
+ "to most-used for questions about the skills used most, and to none otherwise.",
73
+ ].join(" ");
40
74
  function topTags(tags) {
41
75
  return [...tags].sort((a, b) => b.count - a.count).slice(0, MAX_PLAN_TAGS);
42
76
  }
@@ -46,13 +80,15 @@ export function buildPlanPrompt(input) {
46
80
  .map((turn) => `${turn.role}: ${turn.content.slice(0, MAX_TURN_CHARS)}`);
47
81
  const tags = topTags(input.tags).map((tag) => `${tag.keyword} (${tag.count})`);
48
82
  const categories = [...new Set(input.categories)];
83
+ const skills = input.skills ? [...new Set(input.skills)].slice(0, MAX_PLAN_SKILLS) : null;
49
84
  const user = [
50
85
  turns.length ? `Earlier turns:\n${turns.join("\n")}` : "Earlier turns: none",
51
86
  `Tags (count): ${tags.length ? tags.join(", ") : "none"}`,
52
87
  `Profile categories: ${categories.length ? categories.join(", ") : "none"}`,
88
+ ...(skills ? [`Skills: ${skills.length ? skills.join(", ") : "none"}`] : []),
53
89
  `Question: ${input.question}`,
54
90
  ].join("\n\n");
55
- return { system: SYSTEM, user };
91
+ return { system: skills ? `${SYSTEM} ${SKILL_SYSTEM}` : SYSTEM, user };
56
92
  }
57
93
  function strings(value) {
58
94
  if (!Array.isArray(value))
@@ -83,10 +119,15 @@ export function validatePlan(raw, input) {
83
119
  if (queries.length === 0) {
84
120
  return { queries: [input.question], tags: [], profileCategories: [], skipped: true };
85
121
  }
86
- return {
122
+ const record = {
87
123
  queries,
88
124
  tags: known(strings(plan.tags), topTags(input.tags).map((tag) => tag.keyword)),
89
125
  profileCategories: known(strings(plan.profileCategories), input.categories),
90
126
  skipped: false,
91
127
  };
128
+ if (input.skills) {
129
+ record.skills = known(strings(plan.skills), input.skills.slice(0, MAX_PLAN_SKILLS));
130
+ record.skillIntent = SKILL_INTENTS.find((intent) => intent === plan.skillIntent) ?? "none";
131
+ }
132
+ return record;
92
133
  }
@@ -31,6 +31,8 @@ export interface RetrieveDeps {
31
31
  export interface RetrievedSources {
32
32
  memories: ChatSource[];
33
33
  profile: ChatSource[];
34
+ /** Skill facts. Added by the service while skill facts are on; retrieval never sets it. */
35
+ skills?: ChatSource[];
34
36
  }
35
37
  /** Memory and profile sources for one question, searched in parallel. */
36
38
  export declare function retrieveSources(question: string, plan: ChatPlanRecord, scopeTag: string | null, deps: RetrieveDeps): Promise<RetrievedSources>;
@@ -1,6 +1,6 @@
1
1
  import { tryExtractScopeFromContainerTag } from "../memory-scope.js";
2
2
  import { log } from "../logger.js";
3
- import { chatServiceDeps, chatStore, existingMemoryIds, } from "./chat-runtime.js";
3
+ import { chatServiceDeps, chatStore, existingMemoryIds, listedSkillNames, } from "./chat-runtime.js";
4
4
  import { askQuestion } from "./chat-service.js";
5
5
  import { chatStatus } from "./chat-status.js";
6
6
  import { ChatStoreError } from "./chat-types.js";
@@ -24,6 +24,21 @@ function folderId(value) {
24
24
  const CONVERSATION = /^\/api\/chat\/conversations\/([^/]+)$/;
25
25
  const MESSAGES = /^\/api\/chat\/conversations\/([^/]+)\/messages$/;
26
26
  const FOLDER = /^\/api\/chat\/folders\/([^/]+)$/;
27
+ /**
28
+ * The `K` source names that the current ranking no longer lists. Empty while
29
+ * skill learning is off, and empty, with no error, when the ranking cannot be read.
30
+ */
31
+ async function missingSkillNames(messages, config) {
32
+ if (config.skillLearning !== true)
33
+ return [];
34
+ const cited = [
35
+ ...new Set(messages.flatMap((m) => m.sources.filter((s) => s.kind === "skill").map((s) => s.id))),
36
+ ];
37
+ if (cited.length === 0)
38
+ return [];
39
+ const listed = await listedSkillNames(config).catch(() => null);
40
+ return listed ? cited.filter((name) => !listed.has(name)) : [];
41
+ }
27
42
  /**
28
43
  * The chat routes under `/api/chat/`. Returns undefined for any other path.
29
44
  * Store errors map to 400, 404 or 409; anything else is logged by code only.
@@ -103,6 +118,7 @@ async function dispatch(path, method, readBody, config) {
103
118
  return ok({
104
119
  ...found,
105
120
  missingMemoryIds: cited.filter((memoryId) => !existing.has(memoryId)),
121
+ missingSkillNames: await missingSkillNames(found.messages, config),
106
122
  });
107
123
  }
108
124
  if (method === "PATCH") {
@@ -1,9 +1,11 @@
1
1
  import type { ChatServiceDeps } from "./chat-service.js";
2
2
  import { ChatStore } from "./chat-store.js";
3
+ import { type ChatSkillsConfig } from "./chat-skills-runtime.js";
4
+ export { listedSkillNames } from "./chat-skills-runtime.js";
3
5
  /** Each model call gets this long before chat gives up on it. */
4
6
  export declare const CHAT_MODEL_TIMEOUT_MS = 60000;
5
7
  /** The settings chat reads. The caller passes `CONFIG`. */
6
- export interface ChatRuntimeConfig {
8
+ export interface ChatRuntimeConfig extends Partial<Omit<ChatSkillsConfig, "storagePath">> {
7
9
  storagePath: string;
8
10
  memoryProvider: string;
9
11
  memoryModel?: string;
@@ -1,6 +1,8 @@
1
1
  import { join } from "node:path";
2
2
  import { log } from "../logger.js";
3
3
  import { ChatStore } from "./chat-store.js";
4
+ import { chatSkillsDep } from "./chat-skills-runtime.js";
5
+ export { listedSkillNames } from "./chat-skills-runtime.js";
4
6
  /** Each model call gets this long before chat gives up on it. */
5
7
  export const CHAT_MODEL_TIMEOUT_MS = 60_000;
6
8
  const stores = new Map();
@@ -74,8 +76,10 @@ export async function existingMemoryIds(ids) {
74
76
  /** Real dependencies for `askQuestion`, built from the settings passed in. */
75
77
  export async function chatServiceDeps(config) {
76
78
  const store = await chatStore(config);
79
+ const skills = await chatSkillsDep(config);
77
80
  return {
78
81
  store,
82
+ ...(skills ? { skills } : {}),
79
83
  model: config.memoryModel ?? config.memoryProvider,
80
84
  callTool: (system, user, tool) => callTool(config, system, user, tool),
81
85
  retrieve: {
@@ -1,11 +1,12 @@
1
1
  import type { ToolCallResult } from "../ai/providers/base-provider.js";
2
2
  import { ANSWER_TOOL } from "./chat-answer.js";
3
- import { PLAN_TOOL, type PlanInput } from "./chat-plan.js";
3
+ import { planTool, type PlanInput } from "./chat-plan.js";
4
4
  import { type RetrieveDeps } from "./chat-retrieve.js";
5
+ import { type ChatSkillsDep } from "./chat-skills.js";
5
6
  import type { ChatStore } from "./chat-store.js";
6
7
  import type { ChatMessage } from "./chat-types.js";
7
8
  export declare const MAX_QUESTION_CHARS = 4000;
8
- type Tool = typeof PLAN_TOOL | typeof ANSWER_TOOL;
9
+ type Tool = ReturnType<typeof planTool> | typeof ANSWER_TOOL;
9
10
  export interface ChatServiceDeps {
10
11
  store: ChatStore;
11
12
  model: string;
@@ -14,6 +15,8 @@ export interface ChatServiceDeps {
14
15
  retrieve: RetrieveDeps;
15
16
  listTags: (scopeTag: string | null) => Promise<PlanInput["tags"]>;
16
17
  profileCategories: () => Promise<string[]>;
18
+ /** Skill facts. Present only while `skillLearning` is on; absent means today's behaviour. */
19
+ skills?: ChatSkillsDep;
17
20
  /** Metadata only: sizes, counts, codes, timings. Never question or answer text. */
18
21
  log: (message: string, meta: Record<string, unknown>) => void;
19
22
  }
@@ -1,7 +1,8 @@
1
1
  import { isFullyPrivate, stripPrivateContent } from "../privacy.js";
2
2
  import { ANSWER_TOOL, buildAnswerPrompt, finaliseAnswer, numberSources } from "./chat-answer.js";
3
- import { PLAN_TOOL, buildPlanPrompt, validatePlan } from "./chat-plan.js";
3
+ import { buildPlanPrompt, planTool, validatePlan } from "./chat-plan.js";
4
4
  import { retrieveSources } from "./chat-retrieve.js";
5
+ import { readSkillRows, skillSourcesFor, failureCode } from "./chat-skills.js";
5
6
  export const MAX_QUESTION_CHARS = 4000;
6
7
  const TITLE_CHARS = 60;
7
8
  const DEFAULT_TITLE = "New chat";
@@ -26,7 +27,7 @@ async function safeCall(deps, system, user, tool) {
26
27
  }
27
28
  async function plan(deps, input) {
28
29
  const prompt = buildPlanPrompt(input);
29
- const result = await safeCall(deps, prompt.system, prompt.user, PLAN_TOOL);
30
+ const result = await safeCall(deps, prompt.system, prompt.user, planTool(!!input.skills));
30
31
  return validatePlan(result?.success ? result.data : null, input);
31
32
  }
32
33
  /**
@@ -77,10 +78,36 @@ export async function askQuestion(conversationId, input, deps) {
77
78
  deps.listTags(input.scopeTag).catch(() => []),
78
79
  deps.profileCategories().catch(() => []),
79
80
  ]);
80
- const searchPlan = await plan(deps, { question, turns, tags, categories });
81
+ const read = deps.skills ? await readSkillRows(deps.skills) : null;
82
+ const rows = read && "rows" in read ? read.rows : null;
83
+ const searchPlan = await plan(deps, {
84
+ question,
85
+ turns,
86
+ tags,
87
+ categories,
88
+ ...(rows ? { skills: rows.map((row) => row.name) } : {}),
89
+ });
90
+ const markUnavailable = (reason) => {
91
+ searchPlan.skillFacts = "unavailable";
92
+ deps.log("chat: skill facts unavailable", { conversationId, reason });
93
+ };
94
+ if (read && "failure" in read)
95
+ markUnavailable(read.failure);
96
+ let skillSources = [];
97
+ if (deps.skills && rows) {
98
+ try {
99
+ skillSources = await skillSourcesFor(deps.skills, rows, searchPlan, question);
100
+ }
101
+ catch (error) {
102
+ markUnavailable(failureCode(error));
103
+ }
104
+ }
81
105
  let sources;
82
106
  try {
83
- sources = numberSources(await retrieveSources(question, searchPlan, input.scopeTag, deps.retrieve));
107
+ sources = numberSources({
108
+ ...(await retrieveSources(question, searchPlan, input.scopeTag, deps.retrieve)),
109
+ skills: skillSources,
110
+ });
84
111
  }
85
112
  catch {
86
113
  return save(deps, conversationId, { errorCode: "search_failed", plan: searchPlan }, started);
@@ -121,6 +148,9 @@ async function save(deps, conversationId, out, started) {
121
148
  tags: out.plan.tags.length,
122
149
  categories: out.plan.profileCategories.length,
123
150
  sources: out.sources?.length ?? 0,
151
+ ...(deps.skills
152
+ ? { skillSources: out.sources?.filter((s) => s.kind === "skill").length ?? 0 }
153
+ : {}),
124
154
  cited: out.cited ?? 0,
125
155
  answerChars: out.content?.length ?? 0,
126
156
  ms: Date.now() - started,
@@ -0,0 +1,21 @@
1
+ import { type ChatSkillsDep } from "./chat-skills.js";
2
+ /** The settings the skill reader needs. The caller passes `CONFIG`. */
3
+ export interface ChatSkillsConfig {
4
+ storagePath: string;
5
+ skillLearning?: boolean;
6
+ skillStoragePath?: string;
7
+ skillFolders?: string[];
8
+ claudeConfigDir?: string;
9
+ memoryApiKey?: string;
10
+ embeddingApiKey?: string;
11
+ webServerApiToken?: string;
12
+ webServerAuthPassword?: string;
13
+ }
14
+ /**
15
+ * The skill reader for the chat, or undefined while `skillLearning` is off.
16
+ * It opens `skills.db` with `existingDb()` and never calls `init()`, so it
17
+ * never creates or changes the file. A newer schema version is an error.
18
+ */
19
+ export declare function chatSkillsDep(config: ChatSkillsConfig): Promise<ChatSkillsDep | undefined>;
20
+ /** The names the current ranking lists, or null when the ranking cannot be read. */
21
+ export declare function listedSkillNames(config: ChatSkillsConfig): Promise<Set<string> | null>;
@@ -0,0 +1,47 @@
1
+ import { makeScrubContext, SkillFactsError, } from "./chat-skills.js";
2
+ import { MAX_HISTORY_ENTRIES } from "./chat-skills.js";
3
+ /**
4
+ * The skill reader for the chat, or undefined while `skillLearning` is off.
5
+ * It opens `skills.db` with `existingDb()` and never calls `init()`, so it
6
+ * never creates or changes the file. A newer schema version is an error.
7
+ */
8
+ export async function chatSkillsDep(config) {
9
+ if (config.skillLearning !== true)
10
+ return undefined;
11
+ const [{ SkillStore, skillsDbPath, SKILL_SCHEMA_VERSION }, folders, indexModule, ranking, hist] = await Promise.all([
12
+ import("../skill-learning/store.js"),
13
+ import("../skill-learning/skill-folders.js"),
14
+ import("../skill-learning/skill-index.js"),
15
+ import("../skill-learning/ranking.js"),
16
+ import("../skill-learning/history.js"),
17
+ ]);
18
+ const store = new SkillStore(skillsDbPath(folders.skillStoragePath(config)));
19
+ const watched = folders.watchedSkillFolders(config);
20
+ return {
21
+ scrub: makeScrubContext(config),
22
+ rank: async () => {
23
+ const db = await store.existingDb();
24
+ if (db) {
25
+ const row = await db.get("SELECT value FROM skill_meta WHERE key = 'schema_version'");
26
+ if (row && Number(row.value) > SKILL_SCHEMA_VERSION) {
27
+ throw new SkillFactsError("newer_schema");
28
+ }
29
+ }
30
+ const now = Date.now();
31
+ return ranking.rankSkills(store, indexModule.getSkillIndex(watched, now), now, "uses");
32
+ },
33
+ history: async (row) => (await hist.skillHistory({ store, folders: watched }, { path: row.path ?? undefined, name: row.name }, MAX_HISTORY_ENTRIES, 0)).entries,
34
+ };
35
+ }
36
+ /** The names the current ranking lists, or null when the ranking cannot be read. */
37
+ export async function listedSkillNames(config) {
38
+ const skills = await chatSkillsDep(config);
39
+ if (!skills)
40
+ return null;
41
+ try {
42
+ return new Set((await skills.rank()).map((row) => row.name));
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
@@ -0,0 +1,85 @@
1
+ import { type CaptureDiagnosticsConfig } from "../capture-diagnostics.js";
2
+ import type { SkillHistoryEntry } from "../skill-learning/history.js";
3
+ import type { SkillRanking } from "../skill-learning/ranking.js";
4
+ import { MAX_PLAN_SKILLS } from "./chat-plan.js";
5
+ import type { ChatSource, SkillIntent } from "./chat-types.js";
6
+ export { MAX_PLAN_SKILLS };
7
+ export type { SkillIntent };
8
+ /** Skill sources per question. Memories (12) and profile items (6) have their own limits. */
9
+ export declare const MAX_SKILL_SOURCES = 6;
10
+ export declare const MAX_SUMMARY_CHARS = 200;
11
+ export declare const MAX_HISTORY_ENTRIES = 5;
12
+ /** The only names the chat offers, accepts or saves. */
13
+ export declare const SAFE_SKILL_NAME: RegExp;
14
+ /** The part of a ranking row that the chat uses. The `path` is read here and never copied out. */
15
+ export type SkillRow = Pick<SkillRanking, "name" | "path" | "uses" | "failedTurns" | "attention" | "modified" | "lastAppliedAt">;
16
+ type HistoryInput = Pick<SkillHistoryEntry, "kind" | "at" | "summary" | "appliedBy">;
17
+ /** Why skill facts could not be read. Logged as a code; never as text. */
18
+ export type SkillFactsFailure = "newer_schema" | "read_failed";
19
+ export declare class SkillFactsError extends Error {
20
+ readonly code: SkillFactsFailure;
21
+ constructor(code: SkillFactsFailure);
22
+ }
23
+ /** How the chat reads skill facts. Built only while `skillLearning` is on. */
24
+ export interface ChatSkillsDep {
25
+ scrub: ScrubContext;
26
+ /** Ranking rows in any order. Throws when `skills.db` is newer or unreadable. */
27
+ rank: () => Promise<SkillRow[]>;
28
+ /** History entries of one skill, newest first. */
29
+ history: (row: SkillRow) => Promise<HistoryInput[]>;
30
+ }
31
+ export interface ScrubContext {
32
+ home: string;
33
+ user: string;
34
+ diagnostics: CaptureDiagnosticsConfig;
35
+ }
36
+ /** Reads the home folder and OS user name when none are given. Pass config values in; do not import `CONFIG`. */
37
+ export declare function makeScrubContext(diagnostics: CaptureDiagnosticsConfig, names?: {
38
+ home?: string;
39
+ user?: string;
40
+ }): ScrubContext;
41
+ /**
42
+ * A summary made safe to send and to save: no `<private>` text, no secrets, no
43
+ * home folder, OS user name, absolute path or email address, and at most 200
44
+ * characters. It runs whatever `skillLearningRedact` says.
45
+ */
46
+ export declare function scrubSummary(text: string, ctx: ScrubContext): string;
47
+ /**
48
+ * Rows with a safe name, one per name, most used first, at most 100. When two
49
+ * rows share a name, the one with the most uses wins; the first wins a tie.
50
+ */
51
+ export declare function safeSkillRows<T extends SkillRow>(rows: readonly T[], ctx: ScrubContext): T[];
52
+ /**
53
+ * Up to six rows: named skills first in plan order, then the rows the intent
54
+ * picks. `rows` must come from `safeSkillRows`.
55
+ */
56
+ export declare function selectSkills<T extends SkillRow>(rows: readonly T[], named: readonly string[], intent: SkillIntent): T[];
57
+ /**
58
+ * Skill names that appear in the question as a whole token, ignoring case, in
59
+ * order of first mention. Names under 3 characters are skipped. A name is not
60
+ * matched inside a longer name such as `s-web-search-v2`.
61
+ */
62
+ export declare function matchSkillNames(question: string, names: readonly string[]): string[];
63
+ /**
64
+ * The `K` source for one skill. The label comes later from `numberSources`.
65
+ * `history` is newest first; the newest 5 entries are kept. No path, version
66
+ * id or undo flag leaves this function.
67
+ */
68
+ export declare function buildSkillSource(row: SkillRow, history: readonly HistoryInput[], ctx: ScrubContext): ChatSource;
69
+ /** The reason code for a failed read. */
70
+ export declare function failureCode(error: unknown): SkillFactsFailure;
71
+ /** The safe skill rows, or the reason they could not be read. */
72
+ export declare function readSkillRows(skills: ChatSkillsDep): Promise<{
73
+ rows: SkillRow[];
74
+ } | {
75
+ failure: SkillFactsFailure;
76
+ }>;
77
+ /**
78
+ * The `K` sources for one question. The plan's names come first, or the names
79
+ * found in the question when the plan was skipped. Throws when history cannot be read.
80
+ */
81
+ export declare function skillSourcesFor(skills: ChatSkillsDep, rows: readonly SkillRow[], plan: {
82
+ skipped: boolean;
83
+ skills?: string[];
84
+ skillIntent?: SkillIntent;
85
+ }, question: string): Promise<ChatSource[]>;