@v587d/capital-generation 2.8.2 → 2.9.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 (164) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +41 -20
  3. package/chart-ui/README.md +1 -1
  4. package/chart-ui/client.src.cjs +7 -16
  5. package/chart-ui/index.js +12 -33
  6. package/chart-ui/package.json +2 -2
  7. package/config/client.js +18 -39
  8. package/config/client.src.cjs +79 -217
  9. package/config/index.js +22 -72
  10. package/config/package.json +2 -2
  11. package/cordis.patch.yml +46 -51
  12. package/lib/agents/bash-guard.d.ts +9 -55
  13. package/lib/agents/bash-guard.js +27 -67
  14. package/lib/agents/preset-roster.d.ts +22 -0
  15. package/lib/agents/preset-roster.js +87 -0
  16. package/lib/agents/role-tool-policy.d.ts +46 -0
  17. package/lib/agents/role-tool-policy.js +205 -0
  18. package/lib/agents/root-tool-policy.d.ts +28 -124
  19. package/lib/agents/root-tool-policy.js +164 -237
  20. package/lib/agents/selected-skills-provider.d.ts +2 -6
  21. package/lib/agents/selected-skills-provider.js +27 -84
  22. package/lib/capital-settings.d.ts +27 -0
  23. package/lib/capital-settings.js +23 -0
  24. package/lib/chart/artifact-ref.d.ts +3 -8
  25. package/lib/chart/artifact-ref.js +3 -8
  26. package/lib/chart/downsample.d.ts +2 -5
  27. package/lib/chart/downsample.js +3 -8
  28. package/lib/chart/errors.d.ts +3 -7
  29. package/lib/chart/errors.js +1 -2
  30. package/lib/chart/event-context.d.ts +8 -0
  31. package/lib/chart/event-context.js +39 -0
  32. package/lib/chart/events.d.ts +21 -78
  33. package/lib/chart/events.js +40 -189
  34. package/lib/chart/html.d.ts +2 -5
  35. package/lib/chart/html.js +8 -15
  36. package/lib/chart/runtime.d.ts +5 -18
  37. package/lib/chart/runtime.js +14 -34
  38. package/lib/chart/series.d.ts +10 -17
  39. package/lib/chart/series.js +22 -47
  40. package/lib/chart/source-token.d.ts +5 -13
  41. package/lib/chart/source-token.js +18 -17
  42. package/lib/chart/spec.d.ts +6 -12
  43. package/lib/chart/spec.js +7 -31
  44. package/lib/chart/tool.d.ts +6 -29
  45. package/lib/chart/tool.js +9 -23
  46. package/lib/chart-render-entry.d.ts +9 -0
  47. package/lib/chart-render-entry.js +17 -0
  48. package/lib/chart-source-entry.d.ts +13 -0
  49. package/lib/chart-source-entry.js +20 -0
  50. package/lib/collect-entry.d.ts +9 -0
  51. package/lib/collect-entry.js +10 -0
  52. package/lib/collector-runtime.d.ts +22 -0
  53. package/lib/collector-runtime.js +97 -0
  54. package/lib/data-collector/args.d.ts +6 -24
  55. package/lib/data-collector/args.js +6 -24
  56. package/lib/data-collector/dataset-tools.d.ts +7 -1
  57. package/lib/data-collector/dataset-tools.js +27 -56
  58. package/lib/data-collector/describe.d.ts +8 -33
  59. package/lib/data-collector/describe.js +17 -70
  60. package/lib/data-collector/hub.d.ts +27 -85
  61. package/lib/data-collector/hub.js +19 -85
  62. package/lib/data-collector/query-time.d.ts +7 -26
  63. package/lib/data-collector/query-time.js +8 -27
  64. package/lib/data-collector/store.d.ts +40 -117
  65. package/lib/data-collector/store.js +32 -111
  66. package/lib/data-collector/time-axis.d.ts +16 -76
  67. package/lib/data-collector/time-axis.js +25 -111
  68. package/lib/data-collector/tools.d.ts +6 -6
  69. package/lib/data-collector/tools.js +34 -43
  70. package/lib/dataset-read-entry.d.ts +9 -0
  71. package/lib/dataset-read-entry.js +7 -0
  72. package/lib/index.d.ts +7 -117
  73. package/lib/index.js +43 -422
  74. package/lib/net/eastmoney-client.d.ts +13 -65
  75. package/lib/net/eastmoney-client.js +12 -53
  76. package/lib/net/throttle.d.ts +2 -13
  77. package/lib/net/throttle.js +3 -14
  78. package/lib/ocr/client.d.ts +12 -21
  79. package/lib/ocr/client.js +23 -40
  80. package/lib/ocr/markdown.d.ts +20 -26
  81. package/lib/ocr/markdown.js +23 -36
  82. package/lib/ocr/store.d.ts +14 -47
  83. package/lib/ocr/store.js +15 -45
  84. package/lib/ocr-entry.d.ts +9 -0
  85. package/lib/ocr-entry.js +10 -0
  86. package/lib/policy-entry.d.ts +17 -0
  87. package/lib/policy-entry.js +22 -0
  88. package/lib/preset-claim.d.ts +22 -0
  89. package/lib/preset-claim.js +24 -0
  90. package/lib/preset-ids.d.ts +38 -0
  91. package/lib/preset-ids.js +46 -0
  92. package/lib/retriever-config.d.ts +37 -0
  93. package/lib/retriever-config.js +36 -0
  94. package/lib/retriever-runtime.d.ts +31 -0
  95. package/lib/retriever-runtime.js +82 -0
  96. package/lib/root-extras-entry.d.ts +17 -0
  97. package/lib/root-extras-entry.js +25 -0
  98. package/lib/runtime-entry.d.ts +15 -0
  99. package/lib/runtime-entry.js +5 -0
  100. package/lib/runtime.d.ts +21 -0
  101. package/lib/runtime.js +33 -0
  102. package/lib/search-entry.d.ts +9 -0
  103. package/lib/search-entry.js +9 -0
  104. package/lib/sources/eastmoney-http.js +20 -68
  105. package/lib/sources/ftshare-rest.d.ts +6 -31
  106. package/lib/sources/ftshare-rest.js +1194 -225
  107. package/lib/sources/fuyao-core.d.ts +8 -18
  108. package/lib/sources/fuyao-core.js +10 -21
  109. package/lib/sources/fuyao-rest.d.ts +2 -6
  110. package/lib/sources/fuyao-rest.js +23 -102
  111. package/lib/sources/security-code.d.ts +6 -12
  112. package/lib/sources/security-code.js +6 -12
  113. package/lib/sources/security-identity.d.ts +1 -4
  114. package/lib/sources/security-identity.js +3 -8
  115. package/lib/sources/tencent-http.js +14 -28
  116. package/lib/sources/tencent-public-core.d.ts +8 -60
  117. package/lib/sources/tencent-public-core.js +12 -77
  118. package/lib/sources/wind-mcp.d.ts +0 -5
  119. package/lib/sources/wind-mcp.js +23 -78
  120. package/lib/sources-entry.d.ts +9 -0
  121. package/lib/sources-entry.js +9 -0
  122. package/lib/time/tools.d.ts +8 -31
  123. package/lib/time/tools.js +39 -64
  124. package/lib/time-entry.d.ts +9 -0
  125. package/lib/time-entry.js +7 -0
  126. package/lib/tool-exec.d.ts +45 -11
  127. package/lib/tool-exec.js +59 -6
  128. package/lib/user-customization.d.ts +14 -0
  129. package/lib/user-customization.js +47 -0
  130. package/lib/watchlist/tools.d.ts +3 -13
  131. package/lib/watchlist/tools.js +10 -30
  132. package/lib/watchlist-entry.d.ts +9 -0
  133. package/lib/watchlist-entry.js +6 -0
  134. package/lib/web-retriever/engines.d.ts +6 -11
  135. package/lib/web-retriever/engines.js +9 -14
  136. package/lib/web-retriever/html-markdown.d.ts +14 -34
  137. package/lib/web-retriever/html-markdown.js +39 -75
  138. package/lib/web-retriever/local-fetch.d.ts +22 -58
  139. package/lib/web-retriever/local-fetch.js +33 -77
  140. package/lib/web-retriever/retriever.d.ts +6 -18
  141. package/lib/web-retriever/retriever.js +7 -7
  142. package/lib/web-retriever/sources.d.ts +37 -124
  143. package/lib/web-retriever/sources.js +64 -196
  144. package/lib/web-retriever/tools.d.ts +43 -27
  145. package/lib/web-retriever/tools.js +99 -137
  146. package/lib/web-retriever/wind-client.d.ts +14 -31
  147. package/lib/web-retriever/wind-client.js +16 -32
  148. package/locale/zh.json +1 -1
  149. package/package.json +67 -7
  150. package/preset/capital-generation/README.md +87 -53
  151. package/preset/capital-generation/agent.patch.yml +526 -337
  152. package/preset/capital-generation/skills/capital-data-profile/SKILL.md +2 -2
  153. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +1 -1
  154. package/preset/capital-generation/skills/capital-equity-research/SKILL.md +1 -1
  155. package/preset/capital-generation/skills/capital-event-impact/SKILL.md +1 -1
  156. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +15 -19
  157. package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +1 -1
  158. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +9 -12
  159. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -1
  160. package/watchlist/client.js +15 -148
  161. package/watchlist/client.src.cjs +148 -662
  162. package/watchlist/holding-rules.js +11 -38
  163. package/watchlist/index.js +127 -397
  164. package/watchlist/package.json +2 -2
@@ -2,7 +2,8 @@ import { MAX_QUERY_AGGREGATES, MAX_QUERY_FILTERS, MAX_QUERY_GROUP_BY, MAX_QUERY_
2
2
  import { coerceQueryShape, isRecord, parseJsonContainer, readDatasetId, readOptionalTaskId, } from './args.js';
3
3
  import { MAX_DESCRIBE_QUERIES, SAFE_RESULT_CHARS, describeDataset, normalizeDescribeArgs, } from './describe.js';
4
4
  import { normalizeQueryDates, withIsoTimeColumns } from './query-time.js';
5
- import { delegatedSession } from '../tool-exec.js';
5
+ import { authorizedSession } from '../tool-exec.js';
6
+ import { DATASET_READING_PRESET_IDS } from '../preset-ids.js';
6
7
  const jsonObject = (properties = {}, required = []) => ({
7
8
  type: 'object',
8
9
  properties,
@@ -104,27 +105,15 @@ function tool(name, description, parameters, schema, execute, options = {}) {
104
105
  parameters,
105
106
  output: { schema, render },
106
107
  execute,
107
- // `isConcurrencySafe` 是框架的**现成机制**:为 true 时,同一个 assistant 步里的多个调用
108
- // 会被放进有界并发池(默认上限 10),结果仍按模型顺序在**同一个下一步**一起返回。
109
- // 这正是 describe_dataset 省往返的手段——多个 dataset 由模型一次发完,而不是把 N 份结果
110
- // 合并成一个超长载荷(见 docs/design/describe-dataset-design.md §2.1)。
111
- // 宿主侧是 **fail-closed**,两版源码同为「没有这个函数就 exclusive」;2026-10-16 用一次性
112
- // headless 探针在真机核过(账本 L30):两颗除声明与否外逐字相同的工具,三颗**已声明**在
113
- // 1 毫秒内一起派发、整批 706 毫秒;三颗**未声明**严格 call→result 交替、整批 1426 毫秒。
114
- // ⚠ 混发会把已声明的那颗拖回串行(池在遇到第一颗未声明调用处断开),所以"要不要标"是
115
- // 按**工具**定的、不是按**这一次批次**定的:见下面 query_dataset 那条刻意不标的理由。
108
+ // Framework concurrency pool: multiple calls in one assistant step run together and return
109
+ // in the same next step. Mixed batches fall back to serial, so this is per-tool, not per-call.
116
110
  ...(options.concurrencySafe === true ? { isConcurrencySafe: () => true } : {}),
117
111
  };
118
112
  }
119
113
  /**
120
- * 模型经常把数组/对象参数序列化成 JSON 字符串再传(实测:`"select": "[\"date_ms\"]"`、
121
- * `"group_by": "[]"`、`"aggregates": "[{...}]"`、`"limit": "1"`),于是参数其实是对的,
122
- * 却报 `query_spec_invalid: select must contain 1-32 column names`,模型只能反复试错。
123
- *
124
- * 宽容解析原语已外迁到 src/data-collector/args.ts:`query_dataset` 与 `describe_dataset`
125
- * 必须共用同一套解析(两份实现漂移会让模型在换工具后重新踩同一个坑)。
114
+ * Lenient boundary parsing for stringified JSON arrays/objects, shared with describe.ts; the query
115
+ * engine itself stays strict so migration between tools cannot change behavior.
126
116
  */
127
- /** envelope 自身的元数据字段,不属于 QuerySpec;模型按协议原样传 envelope 时忽略它们。 */
128
117
  const QUERY_ENVELOPE_FIELDS = ['type', 'task_id', 'time_column'];
129
118
  function normalizeQueryArgs(args) {
130
119
  const outerDatasetId = readDatasetId(args.dataset_id);
@@ -244,27 +233,14 @@ const querySpecProperties = {
244
233
  error_policy: { type: 'string', enum: ['strict', 'skip_with_warning'] },
245
234
  };
246
235
  const querySpecSchema = jsonObject(querySpecProperties);
247
- /** envelope 形态独有的三个字段;flat 形态把它们视为 QuerySpec 之外的元数据。 */
248
236
  const queryEnvelopeProperties = {
249
237
  type: { type: 'string', enum: ['query_request'], description: '可选:声明使用 query_request envelope 形态' },
250
238
  task_id: { type: 'string' },
251
239
  query: querySpecSchema,
252
240
  };
253
241
  /**
254
- * 工具参数 schema 的根**只能**是 `{ type: 'object', properties, required }`。
255
- *
256
- * 事故(2026-09-14):这里原本是根级 `{ oneOf: [flat, envelope] }`,没有 `type`。
257
- * 模型供应商在请求进入时就整体校验 `tools[].function.parameters`,直接返回
258
- * `Invalid schema for function 'query_dataset': schema must be a JSON Schema of
259
- * 'type: "object"', got 'type: null'`(HTTP 400)。这一步发生在模型生成第一个
260
- * token 之前,而这套工具表在会话组作用域里**主 Agent 也带着**,于是每个 Capital
261
- * 会话都在第 1 轮第 1 步整体失败,与用户问什么无关。
262
- *
263
- * flat 与 envelope 的判定本来就在工具边界由 normalizeQueryArgs 完成(query 存在即
264
- * envelope),所以 schema 用「一个对象 + 两边字段都列出(都非必需,dataset_id 除外)」
265
- * 描述即可——这比根级 oneOf 更准确:oneOf 的两个分支都带 additionalProperties:false,
266
- * 混合传参(如 query + limit)在任何分支下都不合法,而宿主其实接受。
267
- * 回归测试:test/apply-integration.test.mjs(所有注册工具的 parameters 根必须是 object)。
242
+ * Root parameters must be a single object: a root-level oneOf makes providers return HTTP 400
243
+ * before the first token. Both forms are distinguished at the boundary, so list both field sets.
268
244
  */
269
245
  const queryParametersSchema = jsonObject({
270
246
  ...querySpecProperties,
@@ -282,12 +258,8 @@ const queryResultSchema = jsonObject({
282
258
  warnings: { type: 'array', items: { type: 'string' } },
283
259
  }, ['dataset_id', 'columns', 'rows', 'matched_row_count', 'group_count', 'returned_count', 'limit', 'warnings']);
284
260
  /**
285
- * `describe_dataset` 的参数:**一次只处理一个 dataset_id**。
286
- *
287
- * 多个 dataset 由模型在**同一条 assistant 消息**里发多个调用完成——本工具声明了
288
- * `isConcurrencySafe`,框架会把它们放进并发池,N 份结果在同一个下一步一起返回。
289
- * 因此这里刻意**不**接受 `dataset_ids` 数组:把 N 份结果合并成一个工具结果,才是
290
- * 体积预算/digest 那套复杂机制的来源(见 docs/design/describe-dataset-design.md §2.1)。
261
+ * One dataset_id per call; multiple datasets go as multiple calls in one assistant message.
262
+ * Merging them into one result is what the size budget exists to avoid.
291
263
  */
292
264
  const describeParametersSchema = jsonObject({
293
265
  dataset_id: datasetIdSchema,
@@ -302,11 +274,7 @@ const describeQueryOutcomeSchema = jsonObject({
302
274
  result: queryResultSchema,
303
275
  error: jsonObject({ code: { type: 'string' }, detail: { type: 'string' } }, ['code', 'detail']),
304
276
  }, ['index']);
305
- /**
306
- * 输出覆盖两种形态:正常结果(`status: 'ok'`)与体积闸门回执(`status: 'too_large'`)。
307
- * 根必须是单一 object(根级 oneOf 会被供应商 400 拒绝,见上面 query_dataset 的事故注释),
308
- * 所以两边的字段都列出来、都非必需,只有 `status` 与 `dataset_id` 恒有。
309
- */
277
+ /** Single object root, listing both ok and too_large fields (root oneOf is rejected by providers). */
310
278
  const describeResultSchema = jsonObject({
311
279
  status: { type: 'string', enum: ['ok', 'too_large'] },
312
280
  dataset_id: { type: 'string' },
@@ -342,39 +310,41 @@ const describeResultSchema = jsonObject({
342
310
  validation: profileValidationSchema,
343
311
  warnings: { type: 'array', items: { type: 'string' } },
344
312
  queries: { type: 'array', items: describeQueryOutcomeSchema },
345
- /** 仅 too_large:被拦下的形状、字节数、体积归因与重试指引。 */
313
+ /** Only for too_large: blocked shape, byte counts, attribution and retry hint. */
346
314
  shape: { type: 'string', enum: ['array', 'envelope_item', 'document', 'none'] },
347
315
  bytes: { type: 'integer' },
348
316
  facts_bytes: { type: 'integer' },
349
317
  queries_bytes: { type: 'integer' },
350
318
  hint: { type: 'string' },
351
319
  }, ['status', 'dataset_id']);
352
- export function registerDatasetTools(ctx, store) {
320
+ /**
321
+ * Dataset read family. writeProfile is off by default: profiles are host-computed, and the tool is
322
+ * absent from every family row; the monolithic entry opts in explicitly.
323
+ */
324
+ export function registerDatasetTools(ctx, store, options = {}) {
353
325
  const tools = ctx.get('tools');
354
326
  if (!tools)
355
327
  return;
356
328
  const registrations = [
357
- tool('inspect_dataset', '读取当前 session 已授权 Dataset 的元数据和可读性。只返回 DatasetRef 与可读性信息,不返回原始 rows,也不接受任何文件路径。query_access.shape 取值:array / envelope_item = 行集合,可 profile 也可 query;document = 顶层是文档对象(财务指标、回测结果这类),可 profile(宿主给结构摘要与有界内容)但不能 query;none = 不可读,reason 说明原因。批量描述多份 Dataset 请用 describe_dataset(一次一份、可并发);本工具用于单点复核。', jsonObject({ dataset_id: datasetIdSchema }, ['dataset_id']), datasetRefWithAccessSchema, async (args, exec) => store.inspectDataset(readDatasetId(args.dataset_id), delegatedSession(exec, 'inspect_dataset'), exec.signal)),
329
+ tool('inspect_dataset', '读取当前 session 已授权 Dataset 的元数据和可读性。只返回 DatasetRef 与可读性信息,不返回原始 rows,也不接受任何文件路径。query_access.shape 取值:array / envelope_item = 行集合,可 profile 也可 query;document = 顶层是文档对象(财务指标、回测结果这类),可 profile(宿主给结构摘要与有界内容)但不能 query;none = 不可读,reason 说明原因。批量描述多份 Dataset 请用 describe_dataset(一次一份、可并发);本工具用于单点复核。', jsonObject({ dataset_id: datasetIdSchema }, ['dataset_id']), datasetRefWithAccessSchema, async (args, exec) => store.inspectDataset(readDatasetId(args.dataset_id), authorizedSession(ctx, exec, 'inspect_dataset', DATASET_READING_PRESET_IDS), exec.signal)),
358
330
  tool('profile_dataset', `由宿主读取 Dataset 并返回基础质量检查与四类事实;原始 rows 不进入模型上下文。行集合返回:① schema/quality——字段 observed/contract 类型、missing/null/invalid 计数、重复行、时间顺序;② statistics——数值字段的 count/sum/min/max/mean/分位数;③ categories——字符串字段的 distinct_count 与最多 5 个高频取值(truncated=true 表示未列全);④ time_facts——时间列覆盖范围与首行/末行的数值取值(回答"最新值、区间涨跌"必须用它,不要用 min/max 代替首末值);⑤ structure——行内数组/对象的路径摘要(对象字段用 .name、数组元素用 [];未抽样时用 total_elements,超过 50 项时用 sampled_elements,后者明确表示只是抽样,不能据此回答完整数量)。文档型 Dataset(顶层是文档对象,没有行数组)返回 structure 结构摘要 + document 有界内容,不返回行列统计。time_column 可显式指定,省略时按列名候选并只在取值全为数字或日期样式时采用。不接受任何文件路径、脚本内容或额外 schema。批量描述请用 describe_dataset(一次一份、可并发);本工具用于单点复核或补取一份完整 profile。`, jsonObject({
359
331
  dataset_id: datasetIdSchema,
360
332
  task_id: { type: 'string' },
361
333
  time_column: { type: 'string' },
362
334
  primary_key: { type: 'string' },
363
335
  }, ['dataset_id']), profileDatasetResultSchema, async (args, exec) => store.profileDataset({
364
- session: delegatedSession(exec, 'profile_dataset'),
336
+ session: authorizedSession(ctx, exec, 'profile_dataset', DATASET_READING_PRESET_IDS),
365
337
  dataset_id: readDatasetId(args.dataset_id),
366
338
  task_id: readOptionalTaskId(args.task_id),
367
339
  time_column: typeof args.time_column === 'string' ? args.time_column : undefined,
368
340
  primary_key: typeof args.primary_key === 'string' ? args.primary_key : undefined,
369
341
  signal: exec.signal,
370
342
  })),
371
- // 刻意**不**标并发安全(与 describe_dataset 相反,且这是量出来的):真会话里 query_dataset
372
- // 单颗中位 17 毫秒(25 次样本),一次消息发 6 颗串行也只多付约 85 毫秒;而它的成本主体是
373
- // 读 raw.json 后的 `JSON.parse` + 聚合,Node 单线程下这部分**并发也不会重叠**,标了买不到什么。
374
- // 多份数据 / 多条查询的诉求已经由 describe_dataset 的 `queries` 承接(那颗标了)。
343
+ // Deliberately not concurrency-safe: parsing raw.json and aggregating is CPU-bound on Node's
344
+ // single thread, so parallel calls cannot overlap; describe_dataset covers multi-query needs.
375
345
  tool('query_dataset', `对当前 session 已授权的 json_rows Dataset 执行固定 QuerySpec:参数是一个 JSON 对象,QuerySpec 字段既可平铺在根上(flat),也可整体放进 query(query_request envelope,宿主自动展开);两种形态同时给出时以 query 为准。支持受限 filter、group_by、count/min/max/avg/sum、asc/desc 和 limit。必须返回聚合或分组结果,禁止原始行投影;select 可省略,默认返回分组列和聚合别名。默认 error_policy=skip_with_warning,脏值会被排除并在 warnings 披露;需要全量类型一致性时显式使用 strict。**时间筛选直接写日期**:时间列(如 date_ms)的 filter 值可写 "2026-08-23" / "2026-08" / "2026" 或 {period:"last_3_months"}(period 省略锚点时以该 Dataset 最后一天为准),宿主按该列自身的时区偏移换算成边界,不要自己算毫秒;日期写法配 >、<、!= 无意义会报 query_type_conflict(用 >= / <= / =)。结果里时间列会附 *_iso 可读日期。最多 ${MAX_QUERY_FILTERS} 个条件、${MAX_QUERY_GROUP_BY} 个分组列、${MAX_QUERY_AGGREGATES} 个聚合、${MAX_QUERY_LIMIT} 行,结果最多 ${MAX_QUERY_OUTPUT_BYTES} 字节。数值字符串只在 Dataset schema 明确为 number/integer 时兼容。query_dataset 是受控分组/聚合工具,不是通用 raw.json 读取或自定义分析工具:select 只能引用 group_by 列或聚合别名,取不到原始行(首末/最新值请用 profile 的 time_facts)。select/group_by/aggregates/order_by 请传真正的 JSON 数组(宿主也兼容 JSON 字符串,但不要依赖)。若还要 profile/元数据,可用 describe_dataset 的 queries 参数一次拿到;本工具用于单点补查。`, queryParametersSchema, queryResultSchema, async (args, exec) => {
376
346
  const normalized = normalizeQueryArgs(args);
377
- const session = delegatedSession(exec, 'query_dataset');
347
+ const session = authorizedSession(ctx, exec, 'query_dataset', DATASET_READING_PRESET_IDS);
378
348
  const timeColumn = typeof args.time_column === 'string' && args.time_column.length > 0 ? args.time_column : undefined;
379
349
  const prepared = await normalizeQueryDates({
380
350
  store,
@@ -396,11 +366,11 @@ export function registerDatasetTools(ctx, store) {
396
366
  }),
397
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({
398
368
  store,
399
- session: delegatedSession(exec, 'describe_dataset'),
369
+ session: authorizedSession(ctx, exec, 'describe_dataset', DATASET_READING_PRESET_IDS),
400
370
  request: normalizeDescribeArgs(args),
401
371
  signal: exec.signal,
402
372
  }),
403
- // 让"同一条消息里的多个 describe_dataset"进框架并发池:这是省往返的机制本身。
373
+ // This is the mechanism that saves round trips: multiple describes run in one pool.
404
374
  { concurrencySafe: true }),
405
375
  tool('write_profile', '将当前 session 对已授权 Dataset 生成的基础质量 profile 持久化到 workspace。宿主生成不可变 profile_ref;profile 不得包含原始 rows、文件路径、凭据或未定义字段。', jsonObject({
406
376
  dataset_id: datasetIdSchema,
@@ -419,14 +389,15 @@ export function registerDatasetTools(ctx, store) {
419
389
  warnings: { type: 'array', items: { type: 'string' } },
420
390
  }, ['row_count', 'columns', 'quality', 'warnings']),
421
391
  }, ['dataset_id', 'profile']), profileRefSchema, async (args, exec) => store.writeProfile({
422
- session: delegatedSession(exec, 'write_profile'),
392
+ session: authorizedSession(ctx, exec, 'write_profile', DATASET_READING_PRESET_IDS),
423
393
  dataset_id: readDatasetId(args.dataset_id),
424
394
  task_id: readOptionalTaskId(args.task_id),
425
395
  profile: args.profile,
426
396
  signal: exec.signal,
427
397
  })),
428
398
  ];
429
- for (const definition of registrations) {
399
+ const withWriteProfile = options.writeProfile === true;
400
+ for (const definition of registrations.filter((item) => withWriteProfile || item.name !== 'write_profile')) {
430
401
  ctx.effect(() => tools.register(definition), `capital-generation.dataset-tool(${definition.name})`);
431
402
  }
432
403
  }
@@ -1,37 +1,20 @@
1
1
  import { type DatasetInspection, type ProfileDatasetResult, type SessionLike, type WorkspaceDatasetStore } from './store.js';
2
2
  import { type QueryResult, type QuerySpec } from './query.js';
3
3
  /**
4
- * `describe_dataset`:把「inspect_dataset → profile_dataset →(可选)query_dataset」三次调用
5
- * 合成**一次**(每份 Dataset 一次),供 data_junior 使用。
6
- *
7
- * 设计要点(完整理由见 docs/design/describe-dataset-design.md):
8
- *
9
- * 1. **一次调用只处理一个 dataset**。多个 dataset 由模型在**同一条 assistant 消息**里发多个
10
- * 调用完成——工具声明了 `isConcurrencySafe`,框架会把它们放进并发池,N 份结果在同一个
11
- * 下一步一起返回。因此**不需要**把 N 份结果合并成一个超长载荷,也就不需要任何
12
- * digest / 公平份额 / 裁剪阶梯:单份结果的体积与今天"inspect + profile 两次结果之和"同量级。
13
- * 2. **形状分支由宿主做**:document 型 Dataset 直接跳过 queries,不再依赖模型先 inspect 再判断。
14
- * 3. **唯一的体积闸门** {@link SAFE_RESULT_CHARS}:序列化后超过它就不返回超长载荷,改返回
15
- * 一个小回执(含全部列名与 profile_ref),让模型用 `columns_of_interest` 收窄后重发。
16
- * 把"剪枝器静默截断"变成"响亮且可操作的重试",而不是在宿主里做内容改写。
17
- * 4. **不碰读权限**:一切仍走 store 的受控方法,原始 rows 不进任何 Agent 上下文。
18
- */
19
- /**
20
- * 单份结果的码点上限。preset 的工具结果剪枝阈值是 8192(head 4096 + tail 1024,按码点),
21
- * 这里取 7000 留出余量:超过 8192 会被剪掉中间段,而"被剪"与"被本闸门拦下"的区别是
22
- * **后者会让模型知道发生了什么并给出重试办法**。
4
+ * `describe_dataset`: inspect + profile + optional query in one call per Dataset. A single size gate
5
+ * (SAFE_RESULT_CHARS) returns a small retryable receipt instead of letting the pruner truncate.
23
6
  */
7
+ /** Cap below the preset's 8192 prune threshold: overflow is loud and retryable instead of silently clipped. */
24
8
  export declare const SAFE_RESULT_CHARS = 7000;
25
- /** 单次调用的 queries 条数上限:防的是请求体积,不是结果体积(结果另有闸门)。 */
9
+ /** Request-size cap, not a result-size cap. */
26
10
  export declare const MAX_DESCRIBE_QUERIES = 8;
27
11
  export interface DescribeRequest {
28
12
  dataset_id: string;
29
13
  task_id?: string;
30
14
  time_column?: string;
31
15
  primary_key?: string;
32
- /** 只对这些列返回 statistics / categories / schema;省略即全量。 */
16
+ /** Restrict statistics/categories/schema to these columns; omitted means all. */
33
17
  columns_of_interest?: string[];
34
- /** 受控 QuerySpec 列表;`dataset_id` 由管线补成当前 dataset。 */
35
18
  queries: QuerySpec[];
36
19
  }
37
20
  export interface DescribeQueryOutcome {
@@ -77,18 +60,13 @@ export interface DescribeTooLarge {
77
60
  shape: DatasetInspection['query_access']['shape'];
78
61
  columns: string[];
79
62
  bytes: number;
80
- /** 体积归因:facts(元数据 + profile 事实块)部分的码点数。 */
63
+ /** Size attribution: codepoints of facts vs queries, so the hint can name the bigger side. */
81
64
  facts_bytes: number;
82
- /** 体积归因:queries(含各条 result)部分的码点数。 */
83
65
  queries_bytes: number;
84
66
  hint: string;
85
67
  }
86
68
  export type DescribeOutput = DescribeResult | DescribeTooLarge;
87
- /**
88
- * 工具边界归一化:宽容解析(字符串化的 JSON 数组/对象、单个对象、envelope 形态),
89
- * 引擎侧保持严格。与 `query_dataset` 的 `normalizeQueryArgs` 共用同一套解析原语
90
- * (src/data-collector/args.ts),避免两个工具的宽容度漂移。
91
- */
69
+ /** Lenient boundary parsing, sharing primitives with query_dataset so both tools accept the same shapes. */
92
70
  export declare function normalizeDescribeArgs(args: Record<string, unknown>): DescribeRequest;
93
71
  export interface DescribeDatasetInput {
94
72
  store: WorkspaceDatasetStore;
@@ -96,8 +74,5 @@ export interface DescribeDatasetInput {
96
74
  request: DescribeRequest;
97
75
  signal?: AbortSignal;
98
76
  }
99
- /**
100
- * 单份 Dataset 的完整管线。失败一律抛错(`isError`):单 dataset 调用里没有"部分成功",
101
- * 把失败做成成功信封会让会话日志无法区分(见 docs/design/tool-result-error-signal.md)。
102
- */
77
+ /** Fail loudly on any error: a single-dataset call has no partial success. */
103
78
  export declare function describeDataset(input: DescribeDatasetInput): Promise<DescribeOutput>;
@@ -3,28 +3,12 @@ import { DatasetQueryError } from './query.js';
3
3
  import { normalizeQueryDates, withIsoTimeColumns } from './query-time.js';
4
4
  import { coerceQueryShape, isRecord, parseJsonContainer, readDatasetId, readOptionalColumnList, readOptionalString, readOptionalTaskId, } from './args.js';
5
5
  /**
6
- * `describe_dataset`:把「inspect_dataset → profile_dataset →(可选)query_dataset」三次调用
7
- * 合成**一次**(每份 Dataset 一次),供 data_junior 使用。
8
- *
9
- * 设计要点(完整理由见 docs/design/describe-dataset-design.md):
10
- *
11
- * 1. **一次调用只处理一个 dataset**。多个 dataset 由模型在**同一条 assistant 消息**里发多个
12
- * 调用完成——工具声明了 `isConcurrencySafe`,框架会把它们放进并发池,N 份结果在同一个
13
- * 下一步一起返回。因此**不需要**把 N 份结果合并成一个超长载荷,也就不需要任何
14
- * digest / 公平份额 / 裁剪阶梯:单份结果的体积与今天"inspect + profile 两次结果之和"同量级。
15
- * 2. **形状分支由宿主做**:document 型 Dataset 直接跳过 queries,不再依赖模型先 inspect 再判断。
16
- * 3. **唯一的体积闸门** {@link SAFE_RESULT_CHARS}:序列化后超过它就不返回超长载荷,改返回
17
- * 一个小回执(含全部列名与 profile_ref),让模型用 `columns_of_interest` 收窄后重发。
18
- * 把"剪枝器静默截断"变成"响亮且可操作的重试",而不是在宿主里做内容改写。
19
- * 4. **不碰读权限**:一切仍走 store 的受控方法,原始 rows 不进任何 Agent 上下文。
20
- */
21
- /**
22
- * 单份结果的码点上限。preset 的工具结果剪枝阈值是 8192(head 4096 + tail 1024,按码点),
23
- * 这里取 7000 留出余量:超过 8192 会被剪掉中间段,而"被剪"与"被本闸门拦下"的区别是
24
- * **后者会让模型知道发生了什么并给出重试办法**。
6
+ * `describe_dataset`: inspect + profile + optional query in one call per Dataset. A single size gate
7
+ * (SAFE_RESULT_CHARS) returns a small retryable receipt instead of letting the pruner truncate.
25
8
  */
9
+ /** Cap below the preset's 8192 prune threshold: overflow is loud and retryable instead of silently clipped. */
26
10
  export const SAFE_RESULT_CHARS = 7_000;
27
- /** 单次调用的 queries 条数上限:防的是请求体积,不是结果体积(结果另有闸门)。 */
11
+ /** Request-size cap, not a result-size cap. */
28
12
  export const MAX_DESCRIBE_QUERIES = 8;
29
13
  function errorEnvelope(error) {
30
14
  if (error instanceof DatasetQueryError || error instanceof DatasetStoreError) {
@@ -33,29 +17,13 @@ function errorEnvelope(error) {
33
17
  }
34
18
  return { code: 'query_failed', detail: error instanceof Error ? error.message : String(error) };
35
19
  }
36
- /** 与剪枝器同口径:按 Unicode 码点量体积,不按 UTF-16 码元。 */
20
+ /** Match the pruner: measure Unicode code points, not UTF-16 units. */
37
21
  function codePointLength(text) {
38
22
  return [...text].length;
39
23
  }
40
24
  /**
41
- * 丢掉值为 `undefined` 的自有属性。
42
- *
43
- * ⛔ 这是**必须**的收尾步骤,不是风格问题(2026-09-23 真机事故):
44
- * DSH 用 `snapshotJsonValue`(`@deepseek-ai/dsh-util-values`)校验工具的**返回值**,
45
- * 规则是"无损 JSON"——任何自有属性值为 `undefined`(以及 NaN/Infinity/-0、数组空洞、
46
- * 非 plain 对象)都会让 `walkJsonValue` 返回 undefined,宿主随即抛
47
- * `ToolOutputError: tool "describe_dataset" returned invalid output: value is not lossless JSON`,
48
- * **整次调用作废**。
49
- *
50
- * 而 `JSON.stringify` 会**静默丢掉** undefined 属性,所以本地怎么自测都看不出来。
51
- * 实测(真机会话 42f42c9e):同一批 5 次调用里 3 次失败——全是数值列的日线缺
52
- * `categories`、无数值列的 ticker_search 缺 `statistics`;而 quote 与 corporate_actions
53
- * 恰好两类列都有,所以成功。模型只看到一句"invalid output",误判成列名不匹配,
54
- * 重试一次后回退到 inspect/profile 老路。
55
- *
56
- * `store.ts` 的 `buildProfile` 一直用"条件展开"(`...(有内容 ? { statistics } : {})`)避开这个坑;
57
- * 这里改成**单一收尾闸门**:内部怎么组装都行,出工具前一律过 `compact()`。
58
- * 回归断言见 test/describe-dataset.test.mjs 的 `assertLosslessJson()`。
25
+ * Strip own properties whose value is `undefined`: DSH validates tool output as lossless JSON and
26
+ * invalidates the whole call. JSON.stringify hides these, so local self-tests cannot catch them.
59
27
  */
60
28
  function compact(value) {
61
29
  return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
@@ -74,16 +42,12 @@ function normalizeQueryItem(value, index, datasetId) {
74
42
  throw new DatasetQueryError('query_spec_invalid', `queries[${index}].dataset_id must match the describe_dataset dataset_id`);
75
43
  }
76
44
  const shape = coerceQueryShape(source);
77
- // envelope 与消息元数据不属于 QuerySpec;dataset_id 由管线按当前 dataset 补。
45
+ // Envelope and message metadata are not QuerySpec; dataset_id is filled by the pipeline.
78
46
  for (const field of ['type', 'task_id', 'dataset_id'])
79
47
  delete shape[field];
80
48
  return shape;
81
49
  }
82
- /**
83
- * 工具边界归一化:宽容解析(字符串化的 JSON 数组/对象、单个对象、envelope 形态),
84
- * 引擎侧保持严格。与 `query_dataset` 的 `normalizeQueryArgs` 共用同一套解析原语
85
- * (src/data-collector/args.ts),避免两个工具的宽容度漂移。
86
- */
50
+ /** Lenient boundary parsing, sharing primitives with query_dataset so both tools accept the same shapes. */
87
51
  export function normalizeDescribeArgs(args) {
88
52
  const dataset_id = readDatasetId(args.dataset_id);
89
53
  const rawQueries = args.queries;
@@ -131,23 +95,14 @@ function restrictFacts(profile, columns, shape, warnings) {
131
95
  : Object.fromEntries(Object.entries(map).filter(([column]) => keep.has(column)));
132
96
  return { statistics: pick(profile.statistics), categories: pick(profile.categories), schema: pick(profile.schema), keep };
133
97
  }
134
- /**
135
- * `time_facts.first` / `last` 也按 `columns_of_interest` 收窄。
136
- *
137
- * 为什么不收窄是错的:该参数承诺"只对这些列返回事实",而首末行投影**每个数值列都带一份**
138
- * (7 列日线 = 每个投影 7 个键),是 time_facts 里最占体积的部分。时间列本身永远保留:
139
- * 它是首末值的时间坐标,删了这两个投影就没有意义(实测整份 describe 里 time_facts 占 1320 字符)。
140
- */
98
+ /** Narrow first/last projections too; the time column always stays or the projection loses its coordinate. */
141
99
  function restrictTimeFacts(facts, keep) {
142
100
  if (facts === undefined || keep === undefined)
143
101
  return facts;
144
102
  const pick = (row) => Object.fromEntries(Object.entries(row).filter(([column]) => column === facts.time_column || keep.has(column)));
145
103
  return { ...facts, first: pick(facts.first), last: pick(facts.last) };
146
104
  }
147
- /**
148
- * 单份 Dataset 的完整管线。失败一律抛错(`isError`):单 dataset 调用里没有"部分成功",
149
- * 把失败做成成功信封会让会话日志无法区分(见 docs/design/tool-result-error-signal.md)。
150
- */
105
+ /** Fail loudly on any error: a single-dataset call has no partial success. */
151
106
  export async function describeDataset(input) {
152
107
  const { store, session, request, signal } = input;
153
108
  const inspection = await store.inspectDataset(request.dataset_id, session, signal);
@@ -172,10 +127,8 @@ export async function describeDataset(input) {
172
127
  warnings.push('document Dataset has no row array; queries were not executed');
173
128
  }
174
129
  else {
175
- // 内嵌 queries 与 `query_dataset` 必须**完全同构**(同一份 query-time 归一):
176
- // 曾经这里直连引擎,于是同一个回合里 `query_dataset` 的日期串能用、内嵌的却把
177
- // `">=": "2026"` 当数字 2026 比(全中 243 行)、`">=": "2026-07"` 被整条跳过(0 行)。
178
- // 时间列优先用请求里显式指定的,其次用 profile 已经认出来的那个。
130
+ // Embedded queries must be isomorphic to query_dataset (same query-time normalization);
131
+ // otherwise the same date string behaves differently in the same turn.
179
132
  const axisColumn = request.time_column ?? profile.time_facts?.time_column;
180
133
  for (const [index, query] of request.queries.entries()) {
181
134
  try {
@@ -198,18 +151,15 @@ export async function describeDataset(input) {
198
151
  queries.push({ index, result });
199
152
  }
200
153
  catch (error) {
201
- // 单条 query 失败不炸整份:把错误记在该条上,模型可以在下一次调用里修正。
202
154
  queries.push({ index, error: errorEnvelope(error) });
203
155
  }
204
156
  }
205
157
  }
206
158
  const facts = restrictFacts(profile, request.columns_of_interest, shape, warnings);
207
- // 内部按"该有就有、没有就是 undefined"组装,**出工具前统一过 compact()**:
208
- // undefined 属性会被 DSH 的输出校验判成 `value is not lossless JSON`(见 compact 注释)。
159
+ // Assemble with undefined allowed, then compact once before returning (see compact).
209
160
  const value = compact({
210
161
  status: 'ok',
211
- // 来自 inspect_dataset 的元数据。**刻意不带 DatasetRef.schema**:它与 profile 的逐列 schema
212
- // 同名不同义,带上既会撞名、又会让宽表体积翻倍;逐列事实已由 profile.schema 覆盖。
162
+ // Deliberately omit DatasetRef.schema: it collides with profile.schema and doubles wide-table size.
213
163
  dataset_id: inspection.dataset_id,
214
164
  task_id: inspection.task_id,
215
165
  session_id: inspection.session_id,
@@ -222,7 +172,6 @@ export async function describeDataset(input) {
222
172
  retention_until: inspection.retention_until,
223
173
  params_digest: inspection.params_digest,
224
174
  query_access: inspection.query_access,
225
- // 来自 profile_dataset
226
175
  profile_id: profile.profile_id,
227
176
  profile_ref: profile.artifact_ref,
228
177
  columns: profile.columns,
@@ -240,10 +189,8 @@ export async function describeDataset(input) {
240
189
  const encoded = JSON.stringify(value);
241
190
  const bytes = codePointLength(encoded);
242
191
  if (bytes > SAFE_RESULT_CHARS) {
243
- // 体积归因:把 queries 部分单量出来,hint 指名先砍哪边。2026-10-10 真机(task_fh_300803_20261010):
244
- // 两份宽表各带 5-6 聚合的 queries 打穿闸门,而回执只说"用 columns_of_interest 收窄"——列本来
245
- // 已经很窄,超预算的其实是 queries,模型只好 describe(去 queries)+ query_dataset 各来一轮,
246
- // 4 次调用里 2 次本可省。归因字段让 hint 直指大头,不引入第二道闸门(⛔ 体积闸门仍只有一道)。
192
+ // Attribution lets the hint name the big side without adding a second gate
193
+ // (there is still exactly one size gate).
247
194
  const queriesBytes = codePointLength(JSON.stringify(value.queries));
248
195
  const factsBytes = bytes - queriesBytes;
249
196
  return compact({