@v587d/capital-generation 2.9.1 → 2.10.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +6 -3
  3. package/chart-ui/README.md +3 -3
  4. package/chart-ui/package.json +1 -1
  5. package/config/package.json +1 -1
  6. package/lib/agents/bash-guard.d.ts +10 -6
  7. package/lib/agents/bash-guard.js +15 -12
  8. package/lib/agents/root-tool-policy.js +2 -0
  9. package/lib/agents/settled-wake.d.ts +43 -0
  10. package/lib/agents/settled-wake.js +81 -0
  11. package/lib/agents/wait-for-agents.d.ts +79 -0
  12. package/lib/agents/wait-for-agents.js +256 -0
  13. package/lib/chart/artifact-ref.d.ts +4 -1
  14. package/lib/chart/artifact-ref.js +2 -1
  15. package/lib/chart/delivery.d.ts +80 -0
  16. package/lib/chart/delivery.js +157 -0
  17. package/lib/chart/events.d.ts +3 -1
  18. package/lib/chart/events.js +52 -1
  19. package/lib/chart/html.js +16 -0
  20. package/lib/chart/present-tool.d.ts +8 -0
  21. package/lib/chart/present-tool.js +92 -22
  22. package/lib/chart/tool.js +63 -9
  23. package/lib/data-collector/dataset-tools.js +1 -1
  24. package/lib/data-collector/store.d.ts +1 -0
  25. package/lib/data-collector/store.js +2 -2
  26. package/lib/orchestration-entry.d.ts +11 -0
  27. package/lib/orchestration-entry.js +6 -0
  28. package/lib/present-entry.d.ts +13 -3
  29. package/lib/present-entry.js +123 -1
  30. package/lib/web-retriever/local-fetch.d.ts +1 -1
  31. package/lib/web-retriever/local-fetch.js +1 -1
  32. package/lib/web-retriever/wind-client.d.ts +1 -1
  33. package/lib/web-retriever/wind-client.js +1 -1
  34. package/package.json +8 -2
  35. package/preset/capital-generation/README.md +34 -16
  36. package/preset/capital-generation/agent.patch.yml +30 -34
  37. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +3 -2
  38. package/preset/capital-generation/skills/capital-data-profile/SKILL.md +9 -4
  39. package/preset/capital-generation/skills/capital-data-query/SKILL.md +47 -19
  40. package/preset/capital-generation/skills/capital-equity-research/SKILL.md +5 -1
  41. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +1 -1
  42. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +12 -2
  43. package/watchlist/package.json +1 -1
@@ -10,10 +10,18 @@
10
10
  import type { Context } from '@deepseek-ai/cordis';
11
11
  import type { ChartArtifactRegistry } from './artifact-ref.js';
12
12
  import { type ChartEventPublisher } from './events.js';
13
+ import type { ChartDelivery } from './delivery.js';
14
+ import type { SessionLike } from '../data-collector/store.js';
15
+ /** One card can carry many files (the client collapses past four); the cap only bounds one event. */
16
+ export declare const MAX_PRESENT_CHARTS = 32;
13
17
  export interface PresentChartsInput {
14
18
  artifacts: ChartArtifactRegistry;
15
19
  /** Lazy: the host projection may mount after this row; an absent publisher fails the call loudly. */
16
20
  chartEvents: () => ChartEventPublisher | undefined;
21
+ /** Records the selection; the closing stream chunk writes it (normal path). */
22
+ delivery?: ChartDelivery;
23
+ /** Fallback-only guard when the stream listener is unavailable. */
24
+ readiness?: (session: SessionLike) => Promise<string | undefined>;
17
25
  }
18
26
  export interface PresentChartsReceipt {
19
27
  presented: Array<{
@@ -1,5 +1,7 @@
1
1
  import { MAX_CHARTS_PER_TURN } from './events.js';
2
2
  import { callerSession } from '../tool-exec.js';
3
+ /** One card can carry many files (the client collapses past four); the cap only bounds one event. */
4
+ export const MAX_PRESENT_CHARTS = 32;
3
5
  const jsonObject = (properties = {}, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
4
6
  const parameters = jsonObject({
5
7
  chart_refs: {
@@ -41,8 +43,8 @@ function normalizeRefs(value) {
41
43
  if (!refs.includes(ref))
42
44
  refs.push(ref);
43
45
  }
44
- if (refs.length > MAX_CHARTS_PER_TURN) {
45
- throw new Error(`present_charts 一次最多登记 ${MAX_CHARTS_PER_TURN} 张图(收到 ${refs.length} 个 chart_ref)`);
46
+ if (refs.length > MAX_PRESENT_CHARTS) {
47
+ throw new Error(`present_charts 一次最多登记 ${MAX_PRESENT_CHARTS} 张图(收到 ${refs.length} 个 chart_ref)`);
46
48
  }
47
49
  return refs;
48
50
  }
@@ -53,6 +55,52 @@ function normalizeTaskId(value) {
53
55
  throw new Error('present_charts 的 task_id 必须是非空字符串');
54
56
  return value.trim();
55
57
  }
58
+ const TITLE_SUBJECT_CODE = /(?<!\d)(\d{6})(?!\d)/g;
59
+ function titleSubjectCodes(title) {
60
+ return [...title.matchAll(TITLE_SUBJECT_CODE)].map((match) => match[1]);
61
+ }
62
+ /** Strip six-digit codes and punctuation so “同花顺 300033 日K” and “同花顺 300803 日K” share a key. */
63
+ function titleDedupeKey(title) {
64
+ return title
65
+ .normalize('NFKC')
66
+ .toLowerCase()
67
+ .replace(TITLE_SUBJECT_CODE, '')
68
+ .replace(/[\s\p{P}\p{S}]+/gu, '');
69
+ }
70
+ function artifactQualityFailure(artifact) {
71
+ const codes = titleSubjectCodes(artifact.title);
72
+ if (codes.length === 0 || artifact.subject_code === null)
73
+ return undefined;
74
+ const mismatched = [...new Set(codes.filter((code) => code !== artifact.subject_code))];
75
+ if (mismatched.length === 0)
76
+ return undefined;
77
+ return `标题里的股票代码 ${mismatched.join('、')} 与 dataset 对应的 ${artifact.subject_code} 不一致;请改正标题后重绘,未登记这张图`;
78
+ }
79
+ function dedupeArtifacts(artifacts) {
80
+ const kept = [];
81
+ const failed = [];
82
+ const seen = new Map();
83
+ for (const artifact of artifacts) {
84
+ const qualityFailure = artifactQualityFailure(artifact);
85
+ if (qualityFailure !== undefined) {
86
+ failed.push({ chart_ref: artifact.chart_ref, reason: qualityFailure });
87
+ continue;
88
+ }
89
+ const titleKey = titleDedupeKey(artifact.title);
90
+ const key = `${artifact.dataset_id ?? 'no-dataset'}|${artifact.kind}|${titleKey.length > 0 ? titleKey : artifact.chart_ref}`;
91
+ const previous = seen.get(key);
92
+ if (previous !== undefined) {
93
+ failed.push({
94
+ chart_ref: artifact.chart_ref,
95
+ reason: `重复图表:与 ${previous.chart_ref} 的 dataset/kind/标题相同;只登记第一张`,
96
+ });
97
+ continue;
98
+ }
99
+ seen.set(key, artifact);
100
+ kept.push(artifact);
101
+ }
102
+ return { kept, failed };
103
+ }
56
104
  /** Present refs first; otherwise present the whole task so compaction cannot strand the charts. */
57
105
  function selectArtifacts(input, session, refs, taskId) {
58
106
  const failed = [];
@@ -75,9 +123,9 @@ function selectArtifacts(input, session, refs, taskId) {
75
123
  const all = input.artifacts.list({ session, task_id: taskId });
76
124
  if (all.length === 0)
77
125
  failed.push({ chart_ref: '(task)', reason: `本会话该 task_id 没有可登记的图表:${taskId}` });
78
- const selected = all.slice(0, MAX_CHARTS_PER_TURN);
79
- if (all.length > MAX_CHARTS_PER_TURN) {
80
- failed.push({ chart_ref: '(task)', reason: `该 task_id 有 ${all.length} 张图,本轮最多登记 ${MAX_CHARTS_PER_TURN} 张;其余未展示` });
126
+ const selected = all.slice(0, MAX_PRESENT_CHARTS);
127
+ if (all.length > MAX_PRESENT_CHARTS) {
128
+ failed.push({ chart_ref: '(task)', reason: `该 task_id 有 ${all.length} 张图,单次最多登记 ${MAX_PRESENT_CHARTS} 张;其余未展示` });
81
129
  }
82
130
  return { selected, failed };
83
131
  }
@@ -87,10 +135,11 @@ export function registerPresentChartsTool(ctx, input) {
87
135
  return;
88
136
  const definition = {
89
137
  name: 'present_charts',
90
- description: '把本会话已生成的图表登记为**最终答复的交付物**(官方 deliverables/presented):用户会在本轮答复末尾的「本轮文件改动 / 交付」行看到卡片,点开即渲染自包含 chart.html。'
91
- + '**只由主 Agent 在写最终答复前调用一次**:参数是 data_junior 回传的 chart_refs(一次全部传入,最多 8 个);refs 已丢失时改传 task_id 登记该任务的全部图表;调用后立刻写最终答复。'
92
- + '不要在过程轮 / 等待行前调用,不要分批重复调用;不要传文件路径或 HTML(路径由宿主解析)。'
93
- + '不调用本工具,图表不会出现在对话里。',
138
+ description: '把本会话已生成的图表登记为**最终答复的交付物**(官方 deliverables/presented):用户会在答复末尾的「本轮文件改动 / 交付」行看到卡片,点开即渲染自包含 HTML(文件名带代码/标题或图形类型,如 `300803-kline-<短id>.html`)。'
139
+ + '**由主 Agent 调用一次**:参数是 data_junior 回传的 chart_refs(一次传入,最多 32 个);refs 已丢失时改传 task_id 登记该任务的全部图表。'
140
+ + '登记前会按 dataset + kind + 标题去重,并校验标题里的 6 位股票代码与 dataset 一致;重复或错标题的 ref 会进 failed,不会出现在交付卡片里。'
141
+ + '这里只记录本轮要交付的图,宿主会在**最终答复之前**统一写入交付行——提前调用不会落进中间轮,不必等“最后一刻”,也不要在过程轮反复调用。'
142
+ + '不要传文件路径或 HTML(路径由宿主解析);不调用时宿主仍会在收尾时自动登记本 exchange 已生成的图表。',
94
143
  parameters,
95
144
  async execute(args, exec) {
96
145
  const session = callerSession(exec);
@@ -99,23 +148,44 @@ export function registerPresentChartsTool(ctx, input) {
99
148
  if (refs.length === 0 && taskId === undefined) {
100
149
  throw new Error('present_charts 需要 chart_refs 或 task_id:两者都不给就无法确定要登记哪几张图');
101
150
  }
151
+ const { selected: selectedRaw, failed } = selectArtifacts(input, session, refs, taskId);
152
+ const { kept: selected, failed: qualityFailed } = dedupeArtifacts(selectedRaw);
153
+ failed.push(...qualityFailed);
154
+ const receipt = { presented: [], failed };
155
+ if (selected.length === 0)
156
+ return receipt;
157
+ const inputs = selected.map((artifact) => ({
158
+ ownerSessionId: session.id,
159
+ chart_id: artifact.chart_id,
160
+ title: artifact.title,
161
+ html_path: artifact.html_path,
162
+ }));
163
+ // Normal path: remember the selection; the closing stream chunk writes it before the answer.
164
+ if (input.delivery?.streaming() === true) {
165
+ input.delivery.request(session, inputs);
166
+ for (const artifact of selected)
167
+ receipt.presented.push({ chart_ref: artifact.chart_ref, title: artifact.title });
168
+ return receipt;
169
+ }
170
+ // Fallback path (stream listener unavailable): write now, but only when the turn is idle.
171
+ const blocked = await input.readiness?.(session);
172
+ if (blocked !== undefined)
173
+ throw new Error(blocked);
102
174
  const publisher = input.chartEvents();
103
175
  if (publisher === undefined) {
104
- throw new Error('图表交付通道不可用(sessions / sessionProjections 解析不到):图表已落盘,可在 workspace 打开 chart.html');
176
+ throw new Error('图表交付通道不可用(sessions / sessionProjections 解析不到):图表已落盘,可在 workspace 打开 html_path 指向的 HTML');
105
177
  }
106
- const { selected, failed } = selectArtifacts(input, session, refs, taskId);
107
- const receipt = { presented: [], failed };
108
- for (const artifact of selected) {
109
- const accepted = publisher.publish({
110
- ownerSessionId: session.id,
111
- chart_id: artifact.chart_id,
112
- title: artifact.title,
113
- html_path: artifact.html_path,
114
- });
115
- if (accepted)
178
+ const accepted = publisher.publishMany !== undefined
179
+ ? publisher.publishMany(session.id, inputs)
180
+ : inputs.every((item) => publisher.publish(item));
181
+ if (accepted) {
182
+ for (const artifact of selected)
116
183
  receipt.presented.push({ chart_ref: artifact.chart_ref, title: artifact.title });
117
- else
118
- receipt.failed.push({ chart_ref: artifact.chart_ref, reason: `本轮登记已达 ${MAX_CHARTS_PER_TURN} 张上限或回合已关闭,未写入交付行` });
184
+ }
185
+ else {
186
+ for (const artifact of selected) {
187
+ receipt.failed.push({ chart_ref: artifact.chart_ref, reason: `本轮登记已达 ${MAX_CHARTS_PER_TURN} 次上限或回合已关闭,未写入交付行` });
188
+ }
119
189
  }
120
190
  return receipt;
121
191
  },
package/lib/chart/tool.js CHANGED
@@ -16,6 +16,49 @@ function newChartId() {
16
16
  const suffix = cryptoLike?.randomUUID ? cryptoLike.randomUUID() : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
17
17
  return `ch_${suffix}`;
18
18
  }
19
+ const SUBJECT_KEYS = ['thscode', 'ts_code', 'ticker', 'symbol', 'stock_code', 'sec_code', 'code'];
20
+ /** Six-digit A-share/HK/US ticker; `thscode` wins over generic API status fields such as `code`. */
21
+ function subjectCodeOf(value) {
22
+ if (!value || typeof value !== 'object' || Array.isArray(value))
23
+ return null;
24
+ const record = value;
25
+ for (const key of SUBJECT_KEYS) {
26
+ const raw = record[key];
27
+ if (typeof raw === 'string') {
28
+ const match = /(?<!\d)(\d{6})(?!\d)/.exec(raw);
29
+ if (match)
30
+ return match[1];
31
+ }
32
+ if (typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 100_000 && raw <= 999_999) {
33
+ return String(raw);
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ function chartSubjectCode(envelope, rows) {
39
+ const fromEnvelope = subjectCodeOf(envelope);
40
+ if (fromEnvelope !== null)
41
+ return fromEnvelope;
42
+ for (const row of rows.slice(0, 3)) {
43
+ const fromRow = subjectCodeOf(row);
44
+ if (fromRow !== null)
45
+ return fromRow;
46
+ }
47
+ return null;
48
+ }
49
+ /** Readable, collision-resistant filename: ASCII title slug + short chart id. The store's artifact
50
+ * name gate is deliberately ASCII, so Chinese titles fall back to the chart kind (code stays when
51
+ * the title/envelope carries one). */
52
+ function chartHtmlName(title, kind, chartId, subjectCode) {
53
+ const kindSlug = kind === 'candlestick' ? 'kline' : kind === 'bar' ? 'ohlc' : kind === 'column' ? 'column' : kind === 'area' ? 'area' : 'line';
54
+ const words = title.normalize('NFKC').match(/[A-Za-z0-9]+/g) ?? [];
55
+ const parts = subjectCode !== null && !words.includes(subjectCode) ? [subjectCode, ...words] : [...words];
56
+ if (parts.length === 0 || (subjectCode !== null && parts.every((part) => part === subjectCode)))
57
+ parts.push(kindSlug);
58
+ const safe = parts.join('-').replace(/-+/g, '-').replace(/^[.\-]+|[.\-]+$/g, '').slice(0, 48);
59
+ const suffix = chartId.replace(/^ch_/, '').slice(0, 8);
60
+ return `${safe.length > 0 ? safe : kindSlug}-${suffix}.html`;
61
+ }
19
62
  function sessionOf(exec) {
20
63
  const session = exec.agent?.session;
21
64
  if (!session || typeof session.id !== 'string' || session.id.length === 0) {
@@ -125,18 +168,27 @@ export async function renderChart(input, args, exec) {
125
168
  spec.title = args.title.trim().slice(0, 200);
126
169
  // The token carries the direct data-agent session; the nested child never sees raw rows.
127
170
  let rows;
171
+ let subjectCode;
128
172
  let meta;
129
173
  if (datasetId !== undefined) {
130
174
  const readSession = tokenSource?.sourceSession ?? session;
131
- const { ref, rows: datasetRows } = await input.store.readPresentationRows({ session: readSession, dataset_id: datasetId, signal: exec.signal });
175
+ const { ref, rows: datasetRows, envelope } = await input.store.readPresentationRows({ session: readSession, dataset_id: datasetId, signal: exec.signal });
132
176
  rows = datasetRows;
133
- meta = { source_kind: 'dataset', dataset_id: ref.dataset_id, source_label: ref.source_label, captured_at: ref.captured_at };
177
+ subjectCode = chartSubjectCode(envelope, datasetRows);
178
+ meta = {
179
+ source_kind: 'dataset',
180
+ dataset_id: ref.dataset_id,
181
+ source_label: ref.source_label,
182
+ captured_at: ref.captured_at,
183
+ ...(subjectCode === null ? {} : { subject_code: subjectCode }),
184
+ };
134
185
  }
135
186
  else {
136
187
  const parsed = await input.store.readWorkspaceJson({ session, path: path, signal: exec.signal });
137
188
  const extracted = extractRows(parsed);
138
189
  rows = extracted.rows;
139
- meta = { source_kind: 'path' };
190
+ subjectCode = chartSubjectCode(parsed, extracted.rows);
191
+ meta = { source_kind: 'path', ...(subjectCode === null ? {} : { subject_code: subjectCode }) };
140
192
  }
141
193
  const chartId = newChartId();
142
194
  const payload = buildChartPayload({ chart_id: chartId, spec, rows, meta });
@@ -144,13 +196,14 @@ export async function renderChart(input, args, exec) {
144
196
  const generatedAt = now();
145
197
  const html = buildStandaloneHtml({ payload, library, generatedAt });
146
198
  const dir = `${CHART_ARTIFACT_DIR}/${chartId}`;
199
+ const htmlName = chartHtmlName(payload.title, payload.kind, chartId, subjectCode);
147
200
  const written = await input.store.writeWorkspaceFiles({
148
201
  session,
149
202
  dir,
150
203
  files: [
151
- { name: 'spec.json', content: `${JSON.stringify({ chart_id: chartId, created_at: generatedAt, source: { dataset_id: meta.dataset_id ?? null, path: path ?? null }, spec }, null, 2)}\n` },
204
+ { name: 'spec.json', content: `${JSON.stringify({ chart_id: chartId, created_at: generatedAt, source: { dataset_id: meta.dataset_id ?? null, path: path ?? null, subject_code: subjectCode }, spec }, null, 2)}\n` },
152
205
  { name: 'series.json', content: `${JSON.stringify(payload)}\n` },
153
- { name: 'chart.html', content: html },
206
+ { name: htmlName, content: html },
154
207
  ],
155
208
  signal: exec.signal,
156
209
  });
@@ -175,13 +228,14 @@ export async function renderChart(input, args, exec) {
175
228
  dataset_id: meta.dataset_id ?? null,
176
229
  source_label: meta.source_label ?? null,
177
230
  captured_at: meta.captured_at ?? null,
231
+ subject_code: subjectCode,
178
232
  kind: payload.kind,
179
233
  axis: payload.axis,
180
234
  title: payload.title,
181
235
  chart_url: chartUrl,
182
236
  spec_path: pathOf('spec.json'),
183
237
  series_path: pathOf('series.json'),
184
- html_path: pathOf('chart.html'),
238
+ html_path: pathOf(htmlName),
185
239
  created_at: generatedAt,
186
240
  });
187
241
  // Unit callers without the registry still get a stable receipt.
@@ -202,7 +256,7 @@ export async function renderChart(input, args, exec) {
202
256
  ownerSessionId,
203
257
  chart_id: chartId,
204
258
  title: payload.title,
205
- html_path: pathOf('chart.html'),
259
+ html_path: pathOf(htmlName),
206
260
  });
207
261
  deliverableTrace = `publish=${accepted ? '受理' : '拒绝'} ownerSessionId=${ownerSessionId} | ${publisher.lastTrace?.() ?? '(无轨迹)'}`;
208
262
  }
@@ -242,7 +296,7 @@ export async function renderChart(input, args, exec) {
242
296
  chart_url: chartUrl,
243
297
  spec_path: pathOf('spec.json'),
244
298
  series_path: pathOf('series.json'),
245
- html_path: pathOf('chart.html'),
299
+ html_path: pathOf(htmlName),
246
300
  warnings: payload.meta.warnings,
247
301
  };
248
302
  }
@@ -252,7 +306,7 @@ export function registerChartTool(ctx, options) {
252
306
  return;
253
307
  const definition = {
254
308
  name: 'render_chart',
255
- description: `把已经取到的数据渲染成一张可交互图,产物落在 workspace 的 ${CHART_ARTIFACT_DIR}/<chart_id>/(chart.html 可离线打开)。` +
309
+ description: `把已经取到的数据渲染成一张可交互图,产物落在 workspace 的 ${CHART_ARTIFACT_DIR}/<chart_id>/(HTML 以标题/代码命名,如「600519-kline-<短id>.html」,可离线打开)。` +
256
310
  '**只回一条小回执,不回任何原始数据行**:序列走旁路,不进入上下文。' +
257
311
  '来源三选一:dataset_id(已授权的 DatasetRef)、path(workspace 相对路径的 JSON,支持顶层数组或含行数组的 envelope),或 chart_source_ref(宿主签发给 visualization_specialist 的短期 token;使用它时还要提供 task_id)。' +
258
312
  'spec 是自由对象,至少给出 kind(line / area / column / candlestick / bar;后两者要 ohlc 四字段与真实时间列)与字段选择(series 或 ohlc),' +
@@ -364,7 +364,7 @@ export function registerDatasetTools(ctx, store, options = {}) {
364
364
  withIsoTimeColumns(result, prepared.axis);
365
365
  return result;
366
366
  }),
367
- tool('describe_dataset', `读取一份 Dataset 的**默认入口**:一次调用完成 inspect(元数据与可读性/形状)、profile(宿主算基础质量与四类事实,写入 profile_ref)与受控 query(可选);原始 rows 不进入模型上下文。一次只处理一个 dataset_id——本轮有多份数据时,请在**同一条消息里一次发出多个调用**(本工具可并发执行,结果会一起返回),不要一份一份等。行集合返回:① 元数据 artifact_ref/capability/source_label/captured_at/retention_until 与 query_access.shape(array / envelope_item = 行集合;document = 顶层文档对象,只能 profile,queries 由宿主跳过);② profile 四类事实——statistics(count/sum/min/max/mean/分位数)、categories(distinct + 最多 5 个高频取值)、time_facts(覆盖范围与首行/末行取值;回答"最新值、区间涨跌"必须用它,不要用 min/max 代替)、structure(嵌套路径摘要);③ queries——每条的 QueryResult,或该条自己的 error(单条失败不影响其余)。columns_of_interest 只对这些列返回 statistics/categories/schema(并同样收窄 time_facts 的首末值投影,时间列永远保留),是收窄结果体积的唯一手段。结果超过 ${SAFE_RESULT_CHARS} 码点时宿主不返回超长载荷,而返回 {status:'too_large', profile_ref, columns, bytes, facts_bytes, queries_bytes, hint}:facts_bytes/queries_bytes 是体积归因(哪边大砍哪边),按 hint 重发一次即可——queries 大就先去掉 queries(聚合改单独走 query_dataset),facts 大就用 columns_of_interest 收窄列(或减少 queries,最多 ${MAX_DESCRIBE_QUERIES} 条)。单点复核仍可用 inspect_dataset / profile_dataset / query_dataset;不接受任何文件路径、脚本内容或自定义表达式。`, describeParametersSchema, describeResultSchema, async (args, exec) => describeDataset({
367
+ tool('describe_dataset', `读取一份 Dataset 的**默认入口**:一次调用完成 inspect(元数据与可读性/形状)、profile(宿主算基础质量与四类事实,写入 profile_ref)与受控 query(可选);原始 rows 不进入模型上下文。一次只处理一个 dataset_id——本轮有多份数据时请并发调用本工具(PTC 在同一个 run_code 程序里用 Promise.all,native 在同一条消息里一次发出多个调用),不要一份一份等。行集合返回:① 元数据 artifact_ref/capability/source_label/captured_at/retention_until 与 query_access.shape(array / envelope_item = 行集合;document = 顶层文档对象,只能 profile,queries 由宿主跳过);② profile 四类事实——statistics(count/sum/min/max/mean/分位数)、categories(distinct + 最多 5 个高频取值)、time_facts(覆盖范围与首行/末行取值;回答"最新值、区间涨跌"必须用它,不要用 min/max 代替)、structure(嵌套路径摘要);③ queries——每条的 QueryResult,或该条自己的 error(单条失败不影响其余)。columns_of_interest 只对这些列返回 statistics/categories/schema(并同样收窄 time_facts 的首末值投影,时间列永远保留),是收窄结果体积的唯一手段。结果超过 ${SAFE_RESULT_CHARS} 码点时宿主不返回超长载荷,而返回 {status:'too_large', profile_ref, columns, bytes, facts_bytes, queries_bytes, hint}:facts_bytes/queries_bytes 是体积归因(哪边大砍哪边),按 hint 重发一次即可——queries 大就先去掉 queries(聚合改单独走 query_dataset),facts 大就用 columns_of_interest 收窄列(或减少 queries,最多 ${MAX_DESCRIBE_QUERIES} 条)。单点复核仍可用 inspect_dataset / profile_dataset / query_dataset;不接受任何文件路径、脚本内容或自定义表达式。`, describeParametersSchema, describeResultSchema, async (args, exec) => describeDataset({
368
368
  store,
369
369
  session: authorizedSession(ctx, exec, 'describe_dataset', DATASET_READING_PRESET_IDS),
370
370
  request: normalizeDescribeArgs(args),
@@ -353,6 +353,7 @@ export declare class WorkspaceDatasetStore {
353
353
  }): Promise<{
354
354
  ref: DatasetRef;
355
355
  rows: unknown[];
356
+ envelope?: Record<string, unknown>;
356
357
  }>;
357
358
  /**
358
359
  * Host-internal workspace text read; with readWorkspaceBytes it shares containment and size
@@ -332,7 +332,7 @@ export class WorkspaceDatasetStore {
332
332
  if (raw.rows.length > maxRows) {
333
333
  throw new DatasetStoreError('dataset_too_large', `Dataset has ${raw.rows.length} rows; presentation reads are capped at ${maxRows}`);
334
334
  }
335
- return { ref, rows: raw.rows };
335
+ return { ref, rows: raw.rows, ...(raw.envelope === undefined ? {} : { envelope: raw.envelope }) };
336
336
  }
337
337
  /**
338
338
  * Host-internal workspace text read; with readWorkspaceBytes it shares containment and size
@@ -500,7 +500,7 @@ export class WorkspaceDatasetStore {
500
500
  if (isRecord(value)) {
501
501
  const rows = rowsAtPath(value, rowKey ?? 'item');
502
502
  if (rows !== undefined)
503
- return { kind: 'rows', rows, shape: 'envelope_item' };
503
+ return { kind: 'rows', rows, shape: 'envelope_item', envelope: value };
504
504
  }
505
505
  if (isRecord(value))
506
506
  return { kind: 'document', value };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `tools/orchestration` row: the root-only `wait_for_agents` tool.
3
+ *
4
+ * Leaves don't mount this row: a child has no delegated tree to wait for and must use send_message
5
+ * to report upward. The tool also rejects the subagent origin at call time, so a mis-mounted row
6
+ * cannot turn into a child-side polling loop.
7
+ */
8
+ import type { Context } from '@deepseek-ai/cordis';
9
+ export declare const name = "capital-orchestration-tools";
10
+ export declare const inject: string[];
11
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,6 @@
1
+ import { registerWaitForAgentsTool } from './agents/wait-for-agents.js';
2
+ export const name = 'capital-orchestration-tools';
3
+ export const inject = ['tools'];
4
+ export function apply(ctx) {
5
+ registerWaitForAgentsTool(ctx);
6
+ }
@@ -1,9 +1,19 @@
1
1
  /**
2
- * `tools/present` row: present_charts only. The root Agent's final-turn delivery step — the
3
- * auto-mode chart pipeline returns refs upward and the main Agent redeems them here right before
4
- * writing the closing answer (rationale: docs/dev/chart-delivery-events.md §6.3).
2
+ * `tools/present` row: present_charts + the closing-answer delivery listener.
3
+ *
4
+ * The model only records which charts belong to the answer. The actual first-party
5
+ * `deliverables/presented` append happens in the terminal assistant-stream chunk, before the closing
6
+ * message commits, gated on an idle root (no actionable inbox input, no running direct children). That is the only
7
+ * point where "the final answer" is knowable, so early/no `present_charts` calls cannot strand the
8
+ * card in a folded process turn (rationale: docs/dev/chart-delivery-events.md §6.3).
5
9
  */
6
10
  import type { Context } from '@deepseek-ai/cordis';
11
+ import type { SessionLike } from './data-collector/store.js';
7
12
  export declare const name = "capital-present-tools";
8
13
  export declare const inject: string[];
14
+ /**
15
+ * Fallback only: when the stream listener never fires, immediate delivery still has to avoid a
16
+ * pending turn. The normal path defers to the closing stream chunk and does not consult this.
17
+ */
18
+ export declare function chartDeliveryBlocker(ctx: Context, session: SessionLike): Promise<string | undefined>;
9
19
  export declare function apply(ctx: Context): void;
@@ -1,13 +1,135 @@
1
+ import { createChartDelivery } from './chart/delivery.js';
1
2
  import { createChartEventPublisher } from './chart/events.js';
2
3
  import { chartEventContextOf } from './chart/event-context.js';
3
4
  import { registerPresentChartsTool } from './chart/present-tool.js';
5
+ import { createSettledWakeGuard } from './agents/settled-wake.js';
4
6
  import { capitalSharedRuntimeOf } from './chart-source-entry.js';
5
7
  export const name = 'capital-present-tools';
6
8
  export const inject = ['tools'];
9
+ /**
10
+ * Fallback only: when the stream listener never fires, immediate delivery still has to avoid a
11
+ * pending turn. The normal path defers to the closing stream chunk and does not consult this.
12
+ */
13
+ export async function chartDeliveryBlocker(ctx, session) {
14
+ const projections = ctx.get('sessionProjections');
15
+ try {
16
+ const inbox = projections?.stateOf(session, 'inbox');
17
+ // A settlement notice can race the closing answer; it must not block the card. Any relay or
18
+ // human input still means another model step is pending.
19
+ const pending = [...(inbox?.['next-turn'] ?? []), ...(inbox?.['next-step'] ?? [])]
20
+ .filter((message) => {
21
+ const source = message?.source;
22
+ return source?.kind !== 'subagent-settled';
23
+ }).length;
24
+ if (pending > 0) {
25
+ return `还有 ${pending} 条待处理消息(子 Agent 回传 / 结算):现在登记会让卡片留在被折叠的中间轮。`
26
+ + '等这些消息处理完、写最终答复的那一步再调用 present_charts(refs 记不清就传 task_id)。';
27
+ }
28
+ }
29
+ catch {
30
+ // Projection unavailable: delivery availability matters more than the guard.
31
+ }
32
+ const subagents = ctx.get('subagents');
33
+ if (typeof subagents?.listDescendants === 'function') {
34
+ try {
35
+ const rows = await subagents.listDescendants(session.id);
36
+ const running = rows.filter((row) => row?.kind === 'child' && row?.activity === 'running').length;
37
+ if (running > 0) {
38
+ return `还有 ${running} 个子 Agent 在运行:现在登记会让卡片留在被折叠的中间轮。`
39
+ + '等它们全部结算、写最终答复的那一步再调用 present_charts(refs 记不清就传 task_id)。';
40
+ }
41
+ }
42
+ catch {
43
+ // Subagent service unavailable: don't block delivery for a missing optional check.
44
+ }
45
+ }
46
+ return undefined;
47
+ }
48
+ function inboxOf(ctx) {
49
+ const projections = ctx.get('sessionProjections');
50
+ return (session) => {
51
+ try {
52
+ return projections?.stateOf(session, 'inbox');
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ };
58
+ }
7
59
  export function apply(ctx) {
8
60
  const shared = capitalSharedRuntimeOf(ctx);
61
+ const publisherOf = () => createChartEventPublisher(chartEventContextOf(ctx));
62
+ const eventCtx = ctx;
63
+ const wakeGuard = createSettledWakeGuard();
64
+ const delivery = createChartDelivery({
65
+ artifacts: shared.artifacts,
66
+ publish: (sessionId, inputs) => publisherOf()?.publishMany(sessionId, inputs) ?? false,
67
+ inboxOf: inboxOf(ctx),
68
+ onClosing: (_session, agent) => wakeGuard.acceptClosing(agent),
69
+ });
70
+ // The preset row is applied in the root agent's scope: these events are the root's own.
71
+ const disposers = [];
72
+ if (typeof eventCtx.on === 'function') {
73
+ disposers.push(eventCtx.on('agent/assistant-stream', (...args) => {
74
+ const payload = args[0];
75
+ if (payload?.frame === undefined)
76
+ return;
77
+ delivery.acceptStream(payload.agent?.session, payload.frame, payload.agent);
78
+ }));
79
+ disposers.push(eventCtx.on('subagent/start', (...args) => {
80
+ const info = args[0];
81
+ delivery.acceptSubagentStart(info ?? {});
82
+ wakeGuard.acceptSubagentStart(info ?? {});
83
+ }));
84
+ disposers.push(eventCtx.on('subagent/end', (...args) => delivery.acceptSubagentEnd(args[0])));
85
+ // Scoped to the root agent: only the root inbox is inspected. A settled notice inserted after
86
+ // the closing frame would otherwise make the loop open a step for a redundant "done" message.
87
+ disposers.push(eventCtx.on('agent/inbox/inserted', (...args) => {
88
+ const payload = args[0];
89
+ if (payload?.agent === undefined || payload.message === undefined)
90
+ return;
91
+ wakeGuard.acceptInboxInserted(payload.agent, payload.message);
92
+ }));
93
+ }
94
+ // Session events are not agent-scoped: listen on the unscoped root and filter by payload.
95
+ const listen = (eventCtx.root ?? ctx);
96
+ if (typeof listen.on === 'function') {
97
+ disposers.push(listen.on('session/event', (...args) => {
98
+ const session = args[0];
99
+ const event = args[1];
100
+ if (event?.type !== 'user/message')
101
+ return;
102
+ const source = event.data?.source;
103
+ // The settlement notice is authoritative even when the lifecycle edge was missed; clearing
104
+ // here keeps the stream-time idle check from waiting on a child that is already done.
105
+ if (source?.kind === 'subagent-settled') {
106
+ if (typeof source.senderSessionId === 'string')
107
+ delivery.acceptSubagentEnd({ id: source.senderSessionId });
108
+ return;
109
+ }
110
+ if (typeof session?.id !== 'string' || source === undefined)
111
+ return;
112
+ if (source.kind === 'user') {
113
+ delivery.acceptUserMessage(session.id, typeof event.time === 'number' ? event.time : Date.now());
114
+ wakeGuard.acceptUserMessage(session, source);
115
+ return;
116
+ }
117
+ // `user/message` is committed only when admitted, so this is exactly the "child already
118
+ // reported its result" marker the closing guard needs.
119
+ if (source.kind === 'agent-message')
120
+ wakeGuard.acceptUserMessage(session, source);
121
+ }));
122
+ }
123
+ ctx.effect(() => () => {
124
+ for (const dispose of disposers) {
125
+ if (typeof dispose === 'function')
126
+ dispose();
127
+ }
128
+ }, 'capital-generation.chart-delivery()');
9
129
  registerPresentChartsTool(ctx, {
10
130
  artifacts: shared.artifacts,
11
- chartEvents: () => createChartEventPublisher(chartEventContextOf(ctx)),
131
+ chartEvents: publisherOf,
132
+ readiness: (session) => chartDeliveryBlocker(ctx, session),
133
+ delivery,
12
134
  });
13
135
  }
@@ -3,7 +3,7 @@
3
3
  * Known limit: DNS resolves per request but the IP is not pinned to the connection,
4
4
  * so a rebinding window remains (same trade-off as dsh-search-first).
5
5
  */
6
- export declare const LOCAL_FETCH_CLIENT_VERSION = "2.9.1";
6
+ export declare const LOCAL_FETCH_CLIENT_VERSION = "2.10.0";
7
7
  export declare const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
8
8
  export type LocalFetchErrorCode = 'ABORTED' | 'TIMEOUT' | 'INVALID_URL' | 'BLOCKED_URL' | 'DNS' | 'REDIRECT' | 'HTTP' | 'TOO_LARGE' | 'UNSUPPORTED_CONTENT_TYPE';
9
9
  export declare class LocalFetchError extends Error {
@@ -4,7 +4,7 @@
4
4
  * so a rebinding window remains (same trade-off as dsh-search-first).
5
5
  */
6
6
  import { htmlToMarkdown, stripInvisibleText } from './html-markdown.js';
7
- export const LOCAL_FETCH_CLIENT_VERSION = '2.9.1';
7
+ export const LOCAL_FETCH_CLIENT_VERSION = '2.10.0';
8
8
  export const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
9
9
  export class LocalFetchError extends Error {
10
10
  code;
@@ -35,7 +35,7 @@ export interface WindCallResult {
35
35
  code?: WindErrorCode;
36
36
  }
37
37
  /** Must match `package.json` version; a test pins the two together. */
38
- export declare const WIND_CLIENT_VERSION = "2.9.1";
38
+ export declare const WIND_CLIENT_VERSION = "2.10.0";
39
39
  export interface WindClient {
40
40
  callTool(toolName: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<WindCallResult>;
41
41
  }
@@ -19,7 +19,7 @@ export const WIND_SERVER_ENDPOINTS = {
19
19
  analytics_data: 'https://mcp.wind.com.cn/vserver_analytics_data/mcp/',
20
20
  };
21
21
  /** Must match `package.json` version; a test pins the two together. */
22
- export const WIND_CLIENT_VERSION = '2.9.1';
22
+ export const WIND_CLIENT_VERSION = '2.10.0';
23
23
  function errorText(error) {
24
24
  return error instanceof Error ? error.message : String(error);
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/capital-generation",
3
- "version": "2.9.1",
3
+ "version": "2.10.0",
4
4
  "description": "Capital Generation:面向中国散户的 AI 投资助手插件(自动模式 Agent Preset)",
5
5
  "icon": "assets/cg-icon.png",
6
6
  "author": "v587d",
@@ -57,6 +57,11 @@
57
57
  "import": "./lib/ocr-entry.js",
58
58
  "default": "./lib/ocr-entry.js"
59
59
  },
60
+ "./tools/orchestration": {
61
+ "types": "./lib/orchestration-entry.d.ts",
62
+ "import": "./lib/orchestration-entry.js",
63
+ "default": "./lib/orchestration-entry.js"
64
+ },
60
65
  "./tools/present": {
61
66
  "types": "./lib/present-entry.d.ts",
62
67
  "import": "./lib/present-entry.js",
@@ -140,7 +145,8 @@
140
145
  "contract:probe": "node scripts/contract-probe.mjs",
141
146
  "docs:capabilities": "npm run build && node scripts/gen-capability-table.mjs",
142
147
  "verify:sessions": "node scripts/verify-sessions.mjs",
143
- "audit:tool-errors": "node scripts/audit-tool-errors.mjs"
148
+ "audit:tool-errors": "node scripts/audit-tool-errors.mjs",
149
+ "ptc:report": "node scripts/ptc-session-report.mjs"
144
150
  },
145
151
  "peerDependencies": {
146
152
  "@deepseek-ai/cordis": "^4.0.2",