@v587d/capital-generation 2.10.0 → 2.10.1
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.
- package/CHANGELOG.md +40 -0
- package/README.md +5 -4
- package/chart-ui/package.json +1 -1
- package/config/client.js +8 -6
- package/config/client.src.cjs +74 -34
- package/config/index.js +8 -2
- package/config/package.json +1 -1
- package/lib/agents/selected-skills-provider.d.ts +29 -10
- package/lib/agents/selected-skills-provider.js +262 -139
- package/lib/agents/skill-usage.d.ts +22 -0
- package/lib/agents/skill-usage.js +92 -0
- package/lib/capital-settings.d.ts +12 -1
- package/lib/capital-settings.js +23 -3
- package/lib/index.d.ts +6 -3
- package/lib/index.js +10 -19
- package/lib/root-extras-entry.d.ts +2 -5
- package/lib/root-extras-entry.js +1 -3
- package/lib/selected-skills-entry.d.ts +14 -0
- package/lib/selected-skills-entry.js +43 -0
- package/lib/web-retriever/local-fetch.d.ts +1 -1
- package/lib/web-retriever/local-fetch.js +1 -1
- package/lib/web-retriever/wind-client.d.ts +1 -1
- package/lib/web-retriever/wind-client.js +1 -1
- package/package.json +10 -2
- package/preset/capital-generation/agent.patch.yml +34 -16
- package/preset/capital-generation/skills/capital-data-query/SKILL.md +19 -0
- package/preset/capital-generation/skills/capital-equity-research/SKILL.md +4 -0
- package/preset/capital-generation/skills/capital-event-impact/SKILL.md +3 -0
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +7 -9
- package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +3 -0
- package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -0
- package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +3 -0
- package/selected-skills/README.md +5 -4
- package/selected-skills/ROUTING.md +88 -0
- package/selected-skills/catalog.js +32 -121
- package/selected-skills/routing.js +1036 -0
- package/watchlist/package.json +1 -1
|
@@ -1,135 +1,243 @@
|
|
|
1
1
|
import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem';
|
|
2
|
-
import {
|
|
3
|
-
import { join } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
4
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { readPresetClaim } from '../preset-claim.js';
|
|
5
|
+
import { ROLE_PRESET_IDS, ROOT_PRESET_ID } from '../preset-ids.js';
|
|
5
6
|
import { agentSessionId, isRootAgent } from './root-tool-policy.js';
|
|
6
|
-
|
|
7
|
+
import { clearSelectedSkillUsage, recordSelectedSkillUsage, resetSelectedSkillUsage, skillUsageNamesForSession, skillUsageRows, trackSelectedSkillUsage, } from './skill-usage.js';
|
|
8
|
+
// Catalog/routing are package data outside the TS root; runtime imports keep lib resolving beside pinned snapshots.
|
|
7
9
|
// @ts-expect-error catalog.js is intentionally maintained as package data.
|
|
8
|
-
import { SELECTED_SKILL_CATALOG as CATALOG, SELECTED_SKILL_SKILL_DIRS
|
|
10
|
+
import { SELECTED_SKILL_CATALOG as CATALOG, SELECTED_SKILL_SKILL_DIRS } from '../../selected-skills/catalog.js';
|
|
11
|
+
// @ts-expect-error routing.js is intentionally maintained as package data.
|
|
12
|
+
import { SELECTED_SKILL_ROLES, SELECTED_SKILL_ROUTING as ROUTING, selectedSkillNamesForRole as routingNamesForRole, selectedSkillRoleKey, selectedSkillUseWhen } from '../../selected-skills/routing.js';
|
|
9
13
|
export const SELECTED_SKILL_CATALOG = CATALOG;
|
|
14
|
+
/** Roles a preset row may declare. data_collector / visualization have no method set. */
|
|
15
|
+
export const SELECTED_SKILL_ENTRY_ROLES = [
|
|
16
|
+
'main', 'data_junior', 'web_retriever', 'data_analyst', 'data_collector', 'visualization_specialist',
|
|
17
|
+
];
|
|
18
|
+
const ROUTING_BY_NAME = ROUTING;
|
|
10
19
|
const PROVIDER_NAME = 'capital-selected-skills';
|
|
11
|
-
const
|
|
12
|
-
|
|
20
|
+
const BRIEF_HEADING = '# 外部精选方法(用户已启用)';
|
|
21
|
+
const AUTO_BRIEF_HEADING = '# 外部精选方法(自动模式:本角色全部方法)';
|
|
22
|
+
/** The role prompt is a router, not the 68-skill catalog; the full list stays in the host catalog. */
|
|
23
|
+
const BRIEF_MAX = 12;
|
|
24
|
+
/** Derive roots from the catalog: a hardcoded list silently hides checked boxes (upstream layouts differ). */
|
|
13
25
|
const SKILL_ROOTS = Object.entries(SELECTED_SKILL_SKILL_DIRS).map(([repository, skillsDir]) => fileURLToPath(new URL(`../../selected-skills/${repository}/${skillsDir}`, import.meta.url)));
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return undefined;
|
|
31
|
-
const fields = new Map();
|
|
32
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
33
|
-
const field = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(line);
|
|
34
|
-
if (field !== null)
|
|
35
|
-
fields.set(field[1], field[2].trim());
|
|
36
|
-
}
|
|
37
|
-
return { fields, body: raw.slice(match[0].length) };
|
|
26
|
+
const ROLE_PRESET = {
|
|
27
|
+
main: ROOT_PRESET_ID,
|
|
28
|
+
data_junior: ROLE_PRESET_IDS.junior,
|
|
29
|
+
web_retriever: ROLE_PRESET_IDS.retriever,
|
|
30
|
+
data_analyst: ROLE_PRESET_IDS.analyst,
|
|
31
|
+
data_collector: ROLE_PRESET_IDS.collector,
|
|
32
|
+
visualization_specialist: ROLE_PRESET_IDS.specialist,
|
|
33
|
+
};
|
|
34
|
+
const ENTRY_BY_KEY = new Map(SELECTED_SKILL_CATALOG.map((entry) => [entry.key, entry]));
|
|
35
|
+
const ROLE_LABELS = SELECTED_SKILL_ROLES;
|
|
36
|
+
const CATALOG_NAMES = new Set(SELECTED_SKILL_CATALOG.map(({ name }) => name));
|
|
37
|
+
const shortSession = (id) => id.replace(/^session-/, '').slice(0, 8);
|
|
38
|
+
const roleLabel = (role) => ROLE_LABELS[role]?.zh ?? role;
|
|
39
|
+
/** Names this role may load, independent of the user's switches. One filter lives in routing.js. */
|
|
40
|
+
export function selectedSkillNamesForRole(role) {
|
|
41
|
+
return new Set(routingNamesForRole(role, SELECTED_SKILL_CATALOG));
|
|
38
42
|
}
|
|
39
|
-
/**
|
|
40
|
-
function
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
bases.set(name, { name, description, content: parsed.body.trim(), directory: join(root, name) });
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
}
|
|
43
|
+
/** Enabled names for one role, read at call time (settings are a live preference). */
|
|
44
|
+
export function enabledSelectedSkillNames(role, selection) {
|
|
45
|
+
const allowed = selectedSkillNamesForRole(role);
|
|
46
|
+
const names = new Set();
|
|
47
|
+
for (const [key, enabled] of Object.entries(selection)) {
|
|
48
|
+
if (enabled !== true)
|
|
49
|
+
continue;
|
|
50
|
+
const entry = ENTRY_BY_KEY.get(key);
|
|
51
|
+
if (entry !== undefined && allowed.has(entry.name))
|
|
52
|
+
names.add(entry.name);
|
|
53
53
|
}
|
|
54
|
-
return
|
|
54
|
+
return names;
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (summaries.length === 0)
|
|
59
|
-
return undefined;
|
|
60
|
-
return [
|
|
61
|
-
INDEX_HEADING,
|
|
62
|
-
'',
|
|
63
|
-
...INDEX_FRAME,
|
|
64
|
-
'',
|
|
65
|
-
...summaries.map((entry) => `- \`${entry.name}\`: ${entry.description}`),
|
|
66
|
-
].join('\n');
|
|
56
|
+
export function readSelectedSkillSelection(source) {
|
|
57
|
+
return typeof source === 'function' ? source() : source;
|
|
67
58
|
}
|
|
68
|
-
function
|
|
69
|
-
return
|
|
59
|
+
function isAccess(value) {
|
|
60
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && 'selection' in value;
|
|
70
61
|
}
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
export function readSelectedSkillsMode(access) {
|
|
63
|
+
if (!isAccess(access))
|
|
64
|
+
return 'manual';
|
|
65
|
+
const mode = typeof access.mode === 'function' ? access.mode() : access.mode;
|
|
66
|
+
return mode === 'auto' ? 'auto' : 'manual';
|
|
75
67
|
}
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
/** Names the role may load right now: auto = the whole role set; manual = switches only. */
|
|
69
|
+
export function availableSelectedSkillNames(role, access) {
|
|
70
|
+
return readSelectedSkillsMode(access) === 'auto'
|
|
71
|
+
? selectedSkillNamesForRole(role)
|
|
72
|
+
: enabledSelectedSkillNames(role, readSelectedSkillSelection(isAccess(access) ? access.selection : access));
|
|
78
73
|
}
|
|
79
|
-
|
|
74
|
+
/** Fingerprint only the catalog key set: refresh the cache only when the selection changes. */
|
|
80
75
|
function selectionFingerprint(selection) {
|
|
81
76
|
return SELECTED_SKILL_CATALOG.map(({ key }) => (selection[key] === true ? '1' : '0')).join('');
|
|
82
77
|
}
|
|
78
|
+
/** Fingerprint the mode separately: switches cannot change an auto catalog. */
|
|
79
|
+
function accessFingerprint(access) {
|
|
80
|
+
if (readSelectedSkillsMode(access) === 'auto')
|
|
81
|
+
return 'auto';
|
|
82
|
+
return `manual:${selectionFingerprint(readSelectedSkillSelection(isAccess(access) ? access.selection : access))}`;
|
|
83
|
+
}
|
|
83
84
|
// `agent/pre-step` belongs to dsh-agent; attach via the injected Agent context so scope filtering admits it.
|
|
84
85
|
function onPreStepEvent(ctx, listener) {
|
|
85
86
|
const events = ctx;
|
|
86
87
|
return events.on?.('agent/pre-step', listener);
|
|
87
88
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
function createFilesystemSkillProvider(ctx, control, config) {
|
|
90
|
+
return new FileSystemSkillProvider(ctx, control, config);
|
|
91
|
+
}
|
|
92
|
+
function catalogPath(entry) {
|
|
93
|
+
const dir = SELECTED_SKILL_SKILL_DIRS[entry.repository];
|
|
94
|
+
if (dir === undefined)
|
|
95
|
+
return '';
|
|
96
|
+
return fileURLToPath(new URL(`../../selected-skills/${entry.repository}/${dir}/${entry.name}/SKILL.md`, import.meta.url));
|
|
97
|
+
}
|
|
98
|
+
/** Read-time router for one role: only cards whose snapshot actually exists are promised to the model. */
|
|
99
|
+
export function selectedSkillEntriesForRole(role, access) {
|
|
100
|
+
const available = availableSelectedSkillNames(role, access);
|
|
101
|
+
const rank = (name) => {
|
|
102
|
+
const routing = ROUTING_BY_NAME[name];
|
|
103
|
+
const canonical = (routing?.canonicalFor.length ?? 0) > 0 ? 0 : 2;
|
|
104
|
+
const primary = routing?.primaryRole === selectedSkillRoleKey(role) ? 0 : 1;
|
|
105
|
+
return canonical + primary;
|
|
106
|
+
};
|
|
107
|
+
return SELECTED_SKILL_CATALOG
|
|
108
|
+
.filter((entry) => available.has(entry.name) && existsSync(catalogPath(entry)))
|
|
109
|
+
// Canonical, primary-role methods first: the 12-line prompt is a router, not a catalog order sample.
|
|
110
|
+
.sort((left, right) => rank(left.name) - rank(right.name));
|
|
111
|
+
}
|
|
112
|
+
/** Usage is read from the tool-result ledger, never from the model's own claim. */
|
|
113
|
+
function usageSection(role, agent) {
|
|
114
|
+
if (agent === undefined)
|
|
115
|
+
return [];
|
|
116
|
+
const sessionId = agentSessionId(agent);
|
|
117
|
+
if (sessionId === undefined)
|
|
118
|
+
return [];
|
|
119
|
+
if (selectedSkillRoleKey(role) === 'main') {
|
|
120
|
+
const rows = skillUsageRows(sessionId);
|
|
121
|
+
if (rows.length === 0)
|
|
122
|
+
return [];
|
|
123
|
+
return [
|
|
124
|
+
'## 本次已实际加载的精选方法(程序记录)',
|
|
125
|
+
'以下名单来自成功返回正文的加载记录(工具结果或 `/name` 注入),包含子 Agent 实际加载的方法;不依赖模型自报。',
|
|
126
|
+
...rows.map((row) => {
|
|
127
|
+
const who = row.role === 'main' ? roleLabel(row.role) : `${roleLabel(row.role)}(${shortSession(row.sessionId)})`;
|
|
128
|
+
return `- ${who}:${row.names.map((name) => `\`${name}\``).join('、')}`;
|
|
129
|
+
}),
|
|
130
|
+
'最终答复必须有一节「使用的方法」,逐条点名以上每一个方法,并说明它支撑了哪个判断;'
|
|
131
|
+
+ '不得点名未加载的条目,也不得用「参考了相关方法」代替。本轮没有加载就不要写这一节。',
|
|
132
|
+
];
|
|
93
133
|
}
|
|
94
|
-
|
|
95
|
-
|
|
134
|
+
const names = skillUsageNamesForSession(sessionId);
|
|
135
|
+
if (names.length === 0)
|
|
136
|
+
return [];
|
|
137
|
+
return [
|
|
138
|
+
`本次你已实际加载:${names.map((name) => `\`${name}\``).join('、')}。`,
|
|
139
|
+
`回传结论时在末尾附一行 \`skills_used: ${names.join(', ')}\`;主 Agent 以程序记录为准,这一行是给读者看的摘要。`,
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
/** Role prompt: a short, imperative router. No upstream prose and no variable interpolation. */
|
|
143
|
+
export function renderSelectedSkillsBrief(role, access, agent) {
|
|
144
|
+
// The main row is mounted in the root preset; a generic child may inherit that composition.
|
|
145
|
+
// Main methods stay on the root Agent, so its prompt must not promise them to a child either.
|
|
146
|
+
if (role === 'main' && agent !== undefined && !isRootAgent(agent))
|
|
147
|
+
return '';
|
|
148
|
+
const mode = readSelectedSkillsMode(access);
|
|
149
|
+
const entries = selectedSkillEntriesForRole(role, access);
|
|
150
|
+
const usage = usageSection(role, agent);
|
|
151
|
+
// A child may have loaded a method while this role has none enabled; the citation
|
|
152
|
+
// feedback still has to reach the root, so usage alone is enough to render.
|
|
153
|
+
if (entries.length === 0 && usage.length === 0)
|
|
154
|
+
return '';
|
|
155
|
+
const key = selectedSkillRoleKey(role);
|
|
156
|
+
const roleRule = key === 'main'
|
|
157
|
+
? '需要计算、建模或统计时交给 data_junior;你的目录只含主角色可用的方法,只属于 data_junior / web_retriever 的方法不在其中,不代表不存在——用户点名时交给对应角色按名加载,不要判为不存在,也不要改用通用方法自己算。'
|
|
158
|
+
: key === 'data_junior'
|
|
159
|
+
? '只执行方法中的计算、表格与统计;结论与最终写作留给主 Agent。'
|
|
160
|
+
: '只做取证、文档深读与核验;不做投资判断。';
|
|
161
|
+
const shown = entries.slice(0, BRIEF_MAX);
|
|
162
|
+
const layerRule = '这些是方法层(金融分析框架),不是流程协议:内置 `capital-*` 协议负责流程、证据门、工具委派与输出形状,'
|
|
163
|
+
+ '方法层负责分析步骤、公式与检查清单。两者正交,任务同时命中时都要加载——先用协议定流程,再用方法定分析;'
|
|
164
|
+
+ '不得用协议替代方法,也不得用方法改写协议。';
|
|
165
|
+
const lines = [mode === 'auto' ? AUTO_BRIEF_HEADING : BRIEF_HEADING, ''];
|
|
166
|
+
if (entries.length > 0) {
|
|
167
|
+
lines.push(mode === 'auto'
|
|
168
|
+
? '自动模式已按你的执行角色开放全部方法。进入分析或取证前,先按每条末尾的「何时使用」判断本任务需要哪几条;'
|
|
169
|
+
+ '匹配的用 `skill` 按名加载正文,再按它的步骤执行;只加载与本任务直接相关的 1–2 条,不要一次搬空目录。'
|
|
170
|
+
: '以下条目是用户在设置里显式启用的方法。进入分析或取证前先逐条判断本任务是否需要这类方法;'
|
|
171
|
+
+ '匹配的先用 `skill` 按名加载正文,再按它的步骤执行;只加载与本任务直接相关的 1–2 条。', layerRule, '加载后不得把摘要当全文,也不得声称用过未加载的条目;真正用过的要在最终答复里点名并说明用途。'
|
|
172
|
+
+ '全都不匹配就不要提,也不要为了用而用。', roleRule, '', ...shown.map((entry) => `- \`${entry.name}\`(${entry.label.zh}):${ROUTING_BY_NAME[entry.name]?.useWhen.zh ?? entry.name}`));
|
|
173
|
+
if (entries.length > shown.length) {
|
|
174
|
+
lines.push(mode === 'auto'
|
|
175
|
+
? `- 本角色共 ${entries.length} 颗可用,这里按常用度列前 ${shown.length} 颗;完整目录见本次会话的技能目录。`
|
|
176
|
+
: `- 另有 ${entries.length - shown.length} 颗已启用,见本次会话的技能目录。`);
|
|
177
|
+
}
|
|
178
|
+
else if (mode === 'auto') {
|
|
179
|
+
lines.push(`- 本角色共 ${entries.length} 颗可用,已全部列出。`);
|
|
180
|
+
}
|
|
96
181
|
}
|
|
182
|
+
if (usage.length > 0)
|
|
183
|
+
lines.push('', ...usage);
|
|
184
|
+
return lines.join('\n');
|
|
97
185
|
}
|
|
98
|
-
/** Registers
|
|
99
|
-
export function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
186
|
+
/** Registers the role-scoped method catalog on the configured Capital preset Agent only. */
|
|
187
|
+
export function registerSelectedSkills(ctx, role, access, createFilesystemProvider = createFilesystemSkillProvider) {
|
|
188
|
+
const presetId = ROLE_PRESET[role];
|
|
189
|
+
const allowed = selectedSkillNamesForRole(role);
|
|
190
|
+
// data_collector / visualization own no methods; an empty provider would only add noise.
|
|
191
|
+
if (allowed.size === 0)
|
|
192
|
+
return;
|
|
193
|
+
// Static callers keep the all-off fast path; a live reader (the plugin) must register even when
|
|
194
|
+
// every switch is off now, or a later enable/auto flip would never surface without a new Agent.
|
|
195
|
+
const staticAccess = typeof access === 'function'
|
|
196
|
+
? undefined
|
|
197
|
+
: isAccess(access)
|
|
198
|
+
? (typeof access.selection === 'function' || typeof access.mode === 'function' ? undefined : access)
|
|
199
|
+
: access;
|
|
200
|
+
if (staticAccess !== undefined && readSelectedSkillsMode(staticAccess) === 'manual'
|
|
201
|
+
&& availableSelectedSkillNames(role, staticAccess).size === 0)
|
|
103
202
|
return;
|
|
104
203
|
const listenCtx = ctx.root ?? ctx;
|
|
105
204
|
if (typeof listenCtx.on !== 'function')
|
|
106
205
|
return;
|
|
107
|
-
// One registration per
|
|
206
|
+
// One registration per Agent: both event paths can observe the same Agent.
|
|
108
207
|
const registered = new WeakSet();
|
|
109
208
|
const registerFor = async (agent) => {
|
|
110
209
|
if (!agent || registered.has(agent))
|
|
111
210
|
return;
|
|
112
|
-
|
|
211
|
+
// The row is mounted only in this role preset, but the root listener sees every Agent.
|
|
212
|
+
const claim = readPresetClaim(ctx, agent);
|
|
213
|
+
if (claim.claim !== presetId)
|
|
214
|
+
return;
|
|
215
|
+
// Main methods stay on the root Agent: a generic child may inherit the root preset claim.
|
|
216
|
+
if (role === 'main' && !isRootAgent(agent))
|
|
113
217
|
return;
|
|
114
218
|
registered.add(agent);
|
|
219
|
+
const usageRole = selectedSkillRoleKey(role);
|
|
220
|
+
const rootAgent = role === 'main' && isRootAgent(agent);
|
|
221
|
+
const sessionId = agentSessionId(agent);
|
|
222
|
+
trackSelectedSkillUsage(agent, usageRole);
|
|
115
223
|
try {
|
|
116
224
|
const agentCtx = agent.ctx;
|
|
117
225
|
if (!agentCtx || typeof agentCtx.inject !== 'function') {
|
|
118
226
|
registered.delete(agent);
|
|
119
|
-
ctx.logger?.warn?.('capital-generation: selected skills are unavailable (
|
|
227
|
+
ctx.logger?.warn?.('capital-generation: selected skills are unavailable (agent cannot inject skills)');
|
|
120
228
|
return;
|
|
121
229
|
}
|
|
122
230
|
// agent/created is serial: DSH awaits listeners, so this inject lands before the first prompt.
|
|
123
231
|
await agentCtx.inject(['skills'], (injectedCtx) => {
|
|
124
232
|
const skills = injectedCtx.skills;
|
|
125
233
|
if (typeof skills?.registerProvider !== 'function') {
|
|
126
|
-
ctx.logger?.warn?.('capital-generation: selected skills are unavailable (
|
|
234
|
+
ctx.logger?.warn?.('capital-generation: selected skills are unavailable (agent has no skills registry)');
|
|
127
235
|
return;
|
|
128
236
|
}
|
|
129
237
|
let filesystemProvider;
|
|
130
238
|
let control;
|
|
131
|
-
// Register even when all toggles are off;
|
|
132
|
-
let served =
|
|
239
|
+
// Register even when all toggles are off; a later enable must surface without a new Agent.
|
|
240
|
+
let served = accessFingerprint(access);
|
|
133
241
|
injectedCtx.effect(() => {
|
|
134
242
|
const unregister = skills.registerProvider((registration) => {
|
|
135
243
|
control = registration;
|
|
@@ -139,64 +247,25 @@ export function registerRootSelectedSkills(ctx, selection, createFilesystemProvi
|
|
|
139
247
|
customSkillDirs: SKILL_ROOTS,
|
|
140
248
|
watch: true,
|
|
141
249
|
});
|
|
142
|
-
const taskBases = loadTaskSkillBases();
|
|
143
|
-
// Summaries from the last `list()`; `get()` reuses them instead of rescanning dirs.
|
|
144
|
-
let runtimeSummaries = [];
|
|
145
|
-
const indexFor = (taskId) => {
|
|
146
|
-
const byName = new Map(runtimeSummaries.map((entry) => [entry.name, entry]));
|
|
147
|
-
const summaries = selectedSkillsForTask(taskId, [...byName.keys()])
|
|
148
|
-
.map((entry) => byName.get(entry.name))
|
|
149
|
-
.filter((entry) => entry !== undefined);
|
|
150
|
-
return renderTaskSkillIndex(taskId, summaries);
|
|
151
|
-
};
|
|
152
250
|
const provider = {
|
|
153
251
|
name: PROVIDER_NAME,
|
|
154
252
|
async list(options) {
|
|
155
253
|
// Read at call time: toggles are settings, not fixed at Agent creation.
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
if (enabled.size === 0) {
|
|
159
|
-
runtimeSummaries = [];
|
|
254
|
+
const available = availableSelectedSkillNames(role, access);
|
|
255
|
+
if (available.size === 0)
|
|
160
256
|
return [];
|
|
161
|
-
}
|
|
162
257
|
const result = await filesystemProvider.list(options);
|
|
163
258
|
const candidates = Array.isArray(result) ? result : result.candidates;
|
|
164
|
-
const selected = candidates
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
.filter((base) => indexFor(base.name) !== undefined)
|
|
169
|
-
.map((base) => ({
|
|
170
|
-
name: base.name,
|
|
171
|
-
description: base.description,
|
|
172
|
-
invocation: { modelInvocable: true, userInvocable: true },
|
|
173
|
-
provider: PROVIDER_NAME,
|
|
174
|
-
source: 'custom',
|
|
175
|
-
rank: TASK_SKILL_SHADOW_RANK,
|
|
176
|
-
locator: { kind: 'task-skill', name: base.name },
|
|
177
|
-
resourceBase: { kind: 'directory', path: base.directory },
|
|
178
|
-
}));
|
|
179
|
-
const merged = [...selected, ...shadows];
|
|
180
|
-
return Array.isArray(result) ? merged : { ...result, candidates: merged };
|
|
259
|
+
const selected = candidates
|
|
260
|
+
.filter((candidate) => available.has(candidate.name))
|
|
261
|
+
.map((candidate) => ({ ...candidate, description: selectedSkillUseWhen(candidate.name, 'zh') || candidate.description }));
|
|
262
|
+
return Array.isArray(result) ? selected : { ...result, candidates: selected };
|
|
181
263
|
},
|
|
182
264
|
get(candidate, options) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return Promise.resolve({
|
|
188
|
-
name: base.name,
|
|
189
|
-
description: base.description,
|
|
190
|
-
invocation: { modelInvocable: true, userInvocable: true },
|
|
191
|
-
source: 'custom',
|
|
192
|
-
provider: PROVIDER_NAME,
|
|
193
|
-
resourceBase: { kind: 'directory', path: base.directory },
|
|
194
|
-
path: join(base.directory, 'SKILL.md'),
|
|
195
|
-
content,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
if (!selectedNames(readSelection(selection)).has(candidate.name)) {
|
|
199
|
-
throw new Error(`selected skill is not enabled: ${candidate.name}`);
|
|
265
|
+
// The catalog filter and the body read must use the same role/selection gate.
|
|
266
|
+
const available = availableSelectedSkillNames(role, access);
|
|
267
|
+
if (!available.has(candidate.name)) {
|
|
268
|
+
throw new Error(`selected skill is not enabled for ${role}: ${candidate.name}`);
|
|
200
269
|
}
|
|
201
270
|
return filesystemProvider.get(candidate, options);
|
|
202
271
|
},
|
|
@@ -204,10 +273,9 @@ export function registerRootSelectedSkills(ctx, selection, createFilesystemProvi
|
|
|
204
273
|
return provider;
|
|
205
274
|
});
|
|
206
275
|
// Invalidate before next() so this turn's snapshot sees the change regardless of listener order.
|
|
207
|
-
// A settings read failure only degrades this hook.
|
|
208
276
|
const stopPreStep = onPreStepEvent(injectedCtx, (_payload, next) => {
|
|
209
277
|
try {
|
|
210
|
-
const fingerprint =
|
|
278
|
+
const fingerprint = accessFingerprint(access);
|
|
211
279
|
if (fingerprint !== served) {
|
|
212
280
|
served = fingerprint;
|
|
213
281
|
control?.invalidate();
|
|
@@ -218,14 +286,69 @@ export function registerRootSelectedSkills(ctx, selection, createFilesystemProvi
|
|
|
218
286
|
}
|
|
219
287
|
return next();
|
|
220
288
|
});
|
|
289
|
+
// The ledger is the only source for `skills_used`: a name is recorded after the host
|
|
290
|
+
// returned the body, so a rejected or disabled name never gets cited.
|
|
291
|
+
const usageCtx = injectedCtx;
|
|
292
|
+
if (typeof usageCtx.on !== 'function') {
|
|
293
|
+
ctx.logger?.warn?.('capital-generation: selected skill usage cannot be recorded (agent context has no event API)');
|
|
294
|
+
}
|
|
295
|
+
const stopToolResult = usageCtx.on?.('tools/result', (exec, result) => {
|
|
296
|
+
if (exec?.name !== 'skill')
|
|
297
|
+
return;
|
|
298
|
+
const outcome = result;
|
|
299
|
+
if (outcome?.isError === true || outcome?.value?.provider !== PROVIDER_NAME)
|
|
300
|
+
return;
|
|
301
|
+
if (typeof outcome?.value?.name === 'string' && CATALOG_NAMES.has(outcome.value.name)) {
|
|
302
|
+
recordSelectedSkillUsage(agent, usageRole, outcome.value.name);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
const stopSession = usageCtx.on?.('session/event', (session, event) => {
|
|
306
|
+
if (sessionId !== undefined && session?.id !== sessionId)
|
|
307
|
+
return;
|
|
308
|
+
const type = event?.type;
|
|
309
|
+
const data = event?.data;
|
|
310
|
+
if (type === 'user/message') {
|
|
311
|
+
const source = data?.source;
|
|
312
|
+
// A new user turn starts a new citation window; child settlements do not.
|
|
313
|
+
if (source?.kind === 'user' && rootAgent && sessionId !== undefined)
|
|
314
|
+
resetSelectedSkillUsage(sessionId);
|
|
315
|
+
if (source?.kind === 'skill-invocation' && typeof source.name === 'string' && CATALOG_NAMES.has(source.name)) {
|
|
316
|
+
recordSelectedSkillUsage(agent, usageRole, source.name);
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (type !== 'tool/ptc-dispatch')
|
|
321
|
+
return;
|
|
322
|
+
if (data?.name !== 'skill' || data.isError === true)
|
|
323
|
+
return;
|
|
324
|
+
let args = data.arguments;
|
|
325
|
+
if (typeof args === 'string') {
|
|
326
|
+
try {
|
|
327
|
+
args = JSON.parse(args);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const name = args?.name;
|
|
334
|
+
if (typeof name === 'string' && CATALOG_NAMES.has(name)) {
|
|
335
|
+
recordSelectedSkillUsage(agent, usageRole, name);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
221
338
|
return () => {
|
|
222
339
|
if (typeof stopPreStep === 'function')
|
|
223
340
|
stopPreStep();
|
|
341
|
+
if (typeof stopToolResult === 'function')
|
|
342
|
+
stopToolResult();
|
|
343
|
+
if (typeof stopSession === 'function')
|
|
344
|
+
stopSession();
|
|
345
|
+
if (rootAgent && sessionId !== undefined)
|
|
346
|
+
clearSelectedSkillUsage(sessionId);
|
|
224
347
|
unregister();
|
|
225
348
|
void filesystemProvider?.dispose();
|
|
226
349
|
filesystemProvider = undefined;
|
|
227
350
|
};
|
|
228
|
-
}, `capital-generation: selected skills for ${agentSessionId(agent) ?? '
|
|
351
|
+
}, `capital-generation: selected skills for ${role} / ${agentSessionId(agent) ?? 'agent'}`);
|
|
229
352
|
});
|
|
230
353
|
}
|
|
231
354
|
catch (error) {
|
|
@@ -235,11 +358,11 @@ export function registerRootSelectedSkills(ctx, selection, createFilesystemProvi
|
|
|
235
358
|
}
|
|
236
359
|
};
|
|
237
360
|
const stop = listenCtx.on('agent/created', ({ agent }) => registerFor(agent));
|
|
238
|
-
//
|
|
239
|
-
//
|
|
361
|
+
// GUI creates with the default preset, then selects Capital. `agent/created` already fired and
|
|
362
|
+
// apply() runs inside that remount, so `agent-preset/selected` arrives after mount.
|
|
240
363
|
const onPresetEvent = listenCtx.on;
|
|
241
364
|
const stopPreset = onPresetEvent?.('agent-preset/selected', (sessionId, agentPreset) => {
|
|
242
|
-
if (agentPreset !==
|
|
365
|
+
if (agentPreset !== presetId)
|
|
243
366
|
return;
|
|
244
367
|
const agents = ctx.get?.('agents');
|
|
245
368
|
const agent = agents?.get?.(String(sessionId));
|
|
@@ -253,5 +376,5 @@ export function registerRootSelectedSkills(ctx, selection, createFilesystemProvi
|
|
|
253
376
|
stop();
|
|
254
377
|
if (typeof stopPreset === 'function')
|
|
255
378
|
stopPreset();
|
|
256
|
-
}, 'capital-generation: selected skill
|
|
379
|
+
}, 'capital-generation: selected skill role listeners');
|
|
257
380
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic selected-skill ledger: a load counts only after the host returned the
|
|
3
|
+
* selected provider's body. The root prompt reads the tree; the report script re-derives
|
|
4
|
+
* the same facts from durable session logs, so model self-report is never the source.
|
|
5
|
+
*/
|
|
6
|
+
import { type AgentLike } from './root-tool-policy.js';
|
|
7
|
+
export interface SkillUsageRow {
|
|
8
|
+
readonly sessionId: string;
|
|
9
|
+
readonly parentSession?: string;
|
|
10
|
+
readonly role: string;
|
|
11
|
+
readonly names: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
/** Register a role Agent before it loads anything, so the tree edge survives an empty session. */
|
|
14
|
+
export declare function trackSelectedSkillUsage(agent: AgentLike | undefined, role: string): void;
|
|
15
|
+
export declare function recordSelectedSkillUsage(agent: AgentLike | undefined, role: string, name: string): void;
|
|
16
|
+
/** A new user turn drops the previous turn's names but keeps the tree edges. */
|
|
17
|
+
export declare function resetSelectedSkillUsage(rootSessionId: string): void;
|
|
18
|
+
/** Agent teardown: the whole subtree's records are process-local and must not outlive it. */
|
|
19
|
+
export declare function clearSelectedSkillUsage(rootSessionId: string): void;
|
|
20
|
+
export declare function clearAllSelectedSkillUsage(): void;
|
|
21
|
+
export declare function skillUsageRows(rootSessionId: string): SkillUsageRow[];
|
|
22
|
+
export declare function skillUsageNamesForSession(sessionId: string): string[];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic selected-skill ledger: a load counts only after the host returned the
|
|
3
|
+
* selected provider's body. The root prompt reads the tree; the report script re-derives
|
|
4
|
+
* the same facts from durable session logs, so model self-report is never the source.
|
|
5
|
+
*/
|
|
6
|
+
import { agentSessionId } from './root-tool-policy.js';
|
|
7
|
+
const records = new Map();
|
|
8
|
+
const bareSession = (id) => id.replace(/^session-/, '');
|
|
9
|
+
function recordFor(agent, role) {
|
|
10
|
+
const id = agentSessionId(agent);
|
|
11
|
+
if (id === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
const key = bareSession(id);
|
|
14
|
+
const parent = agent?.session?.header?.parentSession;
|
|
15
|
+
const parentId = typeof parent === 'string' && parent.length > 0 ? bareSession(parent) : undefined;
|
|
16
|
+
const existing = records.get(key);
|
|
17
|
+
if (existing !== undefined) {
|
|
18
|
+
existing.role = role;
|
|
19
|
+
if (parentId !== undefined)
|
|
20
|
+
existing.parentSession = parentId;
|
|
21
|
+
return existing;
|
|
22
|
+
}
|
|
23
|
+
const created = {
|
|
24
|
+
sessionId: key,
|
|
25
|
+
...(parentId === undefined ? {} : { parentSession: parentId }),
|
|
26
|
+
role,
|
|
27
|
+
names: [],
|
|
28
|
+
};
|
|
29
|
+
records.set(key, created);
|
|
30
|
+
return created;
|
|
31
|
+
}
|
|
32
|
+
/** Register a role Agent before it loads anything, so the tree edge survives an empty session. */
|
|
33
|
+
export function trackSelectedSkillUsage(agent, role) {
|
|
34
|
+
recordFor(agent, role);
|
|
35
|
+
}
|
|
36
|
+
export function recordSelectedSkillUsage(agent, role, name) {
|
|
37
|
+
if (name.length === 0)
|
|
38
|
+
return;
|
|
39
|
+
const record = recordFor(agent, role);
|
|
40
|
+
if (record === undefined)
|
|
41
|
+
return;
|
|
42
|
+
if (!record.names.includes(name))
|
|
43
|
+
record.names.push(name);
|
|
44
|
+
}
|
|
45
|
+
function rootOf(sessionId) {
|
|
46
|
+
let cursor = records.get(sessionId);
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
while (cursor?.parentSession !== undefined && !seen.has(cursor.sessionId)) {
|
|
49
|
+
seen.add(cursor.sessionId);
|
|
50
|
+
const parent = records.get(cursor.parentSession);
|
|
51
|
+
if (parent === undefined)
|
|
52
|
+
break;
|
|
53
|
+
cursor = parent;
|
|
54
|
+
}
|
|
55
|
+
return cursor?.sessionId ?? sessionId;
|
|
56
|
+
}
|
|
57
|
+
/** A new user turn drops the previous turn's names but keeps the tree edges. */
|
|
58
|
+
export function resetSelectedSkillUsage(rootSessionId) {
|
|
59
|
+
const root = bareSession(rootSessionId);
|
|
60
|
+
for (const record of records.values()) {
|
|
61
|
+
if (rootOf(record.sessionId) === root)
|
|
62
|
+
record.names.length = 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Agent teardown: the whole subtree's records are process-local and must not outlive it. */
|
|
66
|
+
export function clearSelectedSkillUsage(rootSessionId) {
|
|
67
|
+
const root = bareSession(rootSessionId);
|
|
68
|
+
// Resolve every member BEFORE deleting: removing the root first would orphan its children
|
|
69
|
+
// and the remaining loop would no longer see them.
|
|
70
|
+
const doomed = [...records.values()]
|
|
71
|
+
.filter((record) => rootOf(record.sessionId) === root)
|
|
72
|
+
.map((record) => record.sessionId);
|
|
73
|
+
for (const id of doomed)
|
|
74
|
+
records.delete(id);
|
|
75
|
+
}
|
|
76
|
+
export function clearAllSelectedSkillUsage() {
|
|
77
|
+
records.clear();
|
|
78
|
+
}
|
|
79
|
+
export function skillUsageRows(rootSessionId) {
|
|
80
|
+
const root = bareSession(rootSessionId);
|
|
81
|
+
return [...records.values()]
|
|
82
|
+
.filter((record) => rootOf(record.sessionId) === root && record.names.length > 0)
|
|
83
|
+
.map((record) => ({
|
|
84
|
+
sessionId: record.sessionId,
|
|
85
|
+
...(record.parentSession === undefined ? {} : { parentSession: record.parentSession }),
|
|
86
|
+
role: record.role,
|
|
87
|
+
names: [...record.names],
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
export function skillUsageNamesForSession(sessionId) {
|
|
91
|
+
return [...(records.get(bareSession(sessionId))?.names ?? [])];
|
|
92
|
+
}
|