@v587d/capital-generation 0.0.0-stage → 2.6.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 (117) hide show
  1. package/CHANGELOG.md +794 -0
  2. package/LICENSE +21 -0
  3. package/README.md +116 -2
  4. package/assets/cg-icon.png +0 -0
  5. package/capital-config/client.js +28 -0
  6. package/capital-config/client.src.cjs +388 -0
  7. package/capital-config/index.js +113 -0
  8. package/capital-config/package.json +36 -0
  9. package/capital-watchlist/client.js +182 -0
  10. package/capital-watchlist/client.src.cjs +1229 -0
  11. package/capital-watchlist/index.js +772 -0
  12. package/capital-watchlist/package.json +37 -0
  13. package/chart-ui/README.md +112 -0
  14. package/chart-ui/client.js +16 -0
  15. package/chart-ui/client.src.cjs +126 -0
  16. package/chart-ui/index.js +205 -0
  17. package/chart-ui/package.json +30 -0
  18. package/cordis.patch.yml +72 -0
  19. package/lib/agents/bash-guard.d.ts +55 -0
  20. package/lib/agents/bash-guard.js +162 -0
  21. package/lib/agents/root-tool-policy.d.ts +164 -0
  22. package/lib/agents/root-tool-policy.js +309 -0
  23. package/lib/chart/artifact-ref.d.ts +65 -0
  24. package/lib/chart/artifact-ref.js +107 -0
  25. package/lib/chart/downsample.d.ts +8 -0
  26. package/lib/chart/downsample.js +53 -0
  27. package/lib/chart/errors.d.ts +16 -0
  28. package/lib/chart/errors.js +15 -0
  29. package/lib/chart/events.d.ts +120 -0
  30. package/lib/chart/events.js +471 -0
  31. package/lib/chart/html.d.ts +21 -0
  32. package/lib/chart/html.js +154 -0
  33. package/lib/chart/runtime.d.ts +20 -0
  34. package/lib/chart/runtime.js +270 -0
  35. package/lib/chart/series.d.ts +102 -0
  36. package/lib/chart/series.js +424 -0
  37. package/lib/chart/source-token.d.ts +64 -0
  38. package/lib/chart/source-token.js +217 -0
  39. package/lib/chart/spec.d.ts +58 -0
  40. package/lib/chart/spec.js +265 -0
  41. package/lib/chart/tool.d.ts +86 -0
  42. package/lib/chart/tool.js +288 -0
  43. package/lib/chart/vendor/LICENSE +201 -0
  44. package/lib/chart/vendor/VERSION.json +8 -0
  45. package/lib/chart/vendor/lightweight-charts.standalone.production.js +7 -0
  46. package/lib/data-collector/args.d.ts +38 -0
  47. package/lib/data-collector/args.js +114 -0
  48. package/lib/data-collector/dataset-tools.d.ts +3 -0
  49. package/lib/data-collector/dataset-tools.js +421 -0
  50. package/lib/data-collector/describe.d.ts +99 -0
  51. package/lib/data-collector/describe.js +256 -0
  52. package/lib/data-collector/hub.d.ts +187 -0
  53. package/lib/data-collector/hub.js +469 -0
  54. package/lib/data-collector/query-time.d.ts +58 -0
  55. package/lib/data-collector/query-time.js +101 -0
  56. package/lib/data-collector/query.d.ts +57 -0
  57. package/lib/data-collector/query.js +473 -0
  58. package/lib/data-collector/store.d.ts +499 -0
  59. package/lib/data-collector/store.js +1486 -0
  60. package/lib/data-collector/time-axis.d.ts +169 -0
  61. package/lib/data-collector/time-axis.js +419 -0
  62. package/lib/data-collector/tools.d.ts +32 -0
  63. package/lib/data-collector/tools.js +216 -0
  64. package/lib/index.d.ts +275 -0
  65. package/lib/index.js +458 -0
  66. package/lib/net/eastmoney-client.d.ts +104 -0
  67. package/lib/net/eastmoney-client.js +118 -0
  68. package/lib/net/throttle.d.ts +16 -0
  69. package/lib/net/throttle.js +34 -0
  70. package/lib/ocr/client.d.ts +82 -0
  71. package/lib/ocr/client.js +281 -0
  72. package/lib/ocr/markdown.d.ts +75 -0
  73. package/lib/ocr/markdown.js +209 -0
  74. package/lib/ocr/store.d.ts +119 -0
  75. package/lib/ocr/store.js +226 -0
  76. package/lib/sources/eastmoney-http.d.ts +2 -0
  77. package/lib/sources/eastmoney-http.js +1253 -0
  78. package/lib/sources/fuyao-core.d.ts +52 -0
  79. package/lib/sources/fuyao-core.js +96 -0
  80. package/lib/sources/fuyao-rest.d.ts +14 -0
  81. package/lib/sources/fuyao-rest.js +2057 -0
  82. package/lib/sources/security-code.d.ts +27 -0
  83. package/lib/sources/security-code.js +104 -0
  84. package/lib/sources/security-identity.d.ts +24 -0
  85. package/lib/sources/security-identity.js +89 -0
  86. package/lib/sources/tencent-http.d.ts +2 -0
  87. package/lib/sources/tencent-http.js +1053 -0
  88. package/lib/sources/wind-mcp.d.ts +8 -0
  89. package/lib/sources/wind-mcp.js +593 -0
  90. package/lib/time/tools.d.ts +74 -0
  91. package/lib/time/tools.js +464 -0
  92. package/lib/tool-exec.d.ts +29 -0
  93. package/lib/tool-exec.js +17 -0
  94. package/lib/web-retriever/engines.d.ts +61 -0
  95. package/lib/web-retriever/engines.js +202 -0
  96. package/lib/web-retriever/html-markdown.d.ts +50 -0
  97. package/lib/web-retriever/html-markdown.js +321 -0
  98. package/lib/web-retriever/local-fetch.d.ts +121 -0
  99. package/lib/web-retriever/local-fetch.js +562 -0
  100. package/lib/web-retriever/retriever.d.ts +58 -0
  101. package/lib/web-retriever/retriever.js +94 -0
  102. package/lib/web-retriever/sources.d.ts +235 -0
  103. package/lib/web-retriever/sources.js +961 -0
  104. package/lib/web-retriever/tools.d.ts +50 -0
  105. package/lib/web-retriever/tools.js +1029 -0
  106. package/lib/web-retriever/wind-client.d.ts +69 -0
  107. package/lib/web-retriever/wind-client.js +311 -0
  108. package/locale/en.json +6 -0
  109. package/locale/zh.json +6 -0
  110. package/package.json +76 -4
  111. package/preset/capital-generation/README.md +161 -0
  112. package/preset/capital-generation/agent.patch.yml +509 -0
  113. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +137 -0
  114. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +416 -0
  115. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +174 -0
  116. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +296 -0
  117. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +240 -0
@@ -0,0 +1,114 @@
1
+ import { DatasetStoreError } from './store.js';
2
+ /**
3
+ * Dataset 工具边界的**宽容参数解析**。
4
+ *
5
+ * 模型经常把数组/对象参数序列化成 JSON 字符串再传(实测:`"select": "[\"date_ms\"]"`、
6
+ * `"group_by": "[]"`、`"aggregates": "[{...}]"`、`"limit": "1"`),于是参数其实是对的,
7
+ * 却报 `query_spec_invalid: select must contain 1-32 column names`,模型只能反复试错。
8
+ *
9
+ * 这里在**工具边界**做一次宽容解析,查询引擎本身保持严格:能解析成正确类型就放行,
10
+ * 解析不了就原样交给校验器报错(错误信息仍指向真实问题)。
11
+ *
12
+ * 本模块由 `query_dataset`(dataset-tools.ts)与 `describe_dataset`(describe.ts)共用,
13
+ * 保证两个工具对同一种"字符串化的 JSON"给出**完全一致**的宽容度——两份实现漂移会让模型
14
+ * 在换工具后重新踩同一个坑。
15
+ */
16
+ export function isRecord(value) {
17
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
18
+ }
19
+ export function parseJsonContainer(value) {
20
+ if (typeof value !== 'string')
21
+ return undefined;
22
+ const text = value.trim();
23
+ if (text.length === 0 || (text[0] !== '[' && text[0] !== '{'))
24
+ return undefined;
25
+ try {
26
+ return JSON.parse(text);
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }
32
+ /** 列名数组:接受真数组、JSON 数组字符串,或单个列名字符串。 */
33
+ export function coerceNameList(value) {
34
+ if (Array.isArray(value))
35
+ return value;
36
+ const parsed = parseJsonContainer(value);
37
+ if (Array.isArray(parsed))
38
+ return parsed;
39
+ if (typeof value === 'string' && parsed === undefined) {
40
+ const text = value.trim();
41
+ if (text.length > 0 && !text.startsWith('[') && !text.startsWith('{'))
42
+ return [value];
43
+ }
44
+ return value;
45
+ }
46
+ /** 对象数组:接受真数组、JSON 数组字符串,或单个对象(对象本身或其 JSON 字符串)。 */
47
+ export function coerceObjectList(value) {
48
+ if (Array.isArray(value))
49
+ return value;
50
+ const parsed = parseJsonContainer(value);
51
+ if (Array.isArray(parsed))
52
+ return parsed;
53
+ if (isRecord(parsed))
54
+ return [parsed];
55
+ if (isRecord(value))
56
+ return [value];
57
+ return value;
58
+ }
59
+ export function coerceLimit(value) {
60
+ if (typeof value !== 'string')
61
+ return value;
62
+ const text = value.trim();
63
+ return /^\d+$/.test(text) ? Number(text) : value;
64
+ }
65
+ export const QUERY_NAME_LIST_FIELDS = ['select', 'group_by'];
66
+ export const QUERY_OBJECT_LIST_FIELDS = ['filters', 'aggregates', 'order_by'];
67
+ /** 把 QuerySpec 的数组/对象字段与 limit 统一解析成引擎期望的类型(不改其余字段)。 */
68
+ export function coerceQueryShape(input) {
69
+ const shape = { ...input };
70
+ for (const field of QUERY_NAME_LIST_FIELDS)
71
+ if (field in shape)
72
+ shape[field] = coerceNameList(shape[field]);
73
+ for (const field of QUERY_OBJECT_LIST_FIELDS)
74
+ if (field in shape)
75
+ shape[field] = coerceObjectList(shape[field]);
76
+ if ('limit' in shape)
77
+ shape.limit = coerceLimit(shape.limit);
78
+ return shape;
79
+ }
80
+ /** dataset_id 的形态校验;不合法一律在工具边界抛错,绝不带进 store。 */
81
+ export function readDatasetId(value) {
82
+ if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(value)) {
83
+ throw new DatasetStoreError('dataset_id_invalid', 'dataset_id is invalid');
84
+ }
85
+ return value;
86
+ }
87
+ /** task_id 是可选元数据:空值等同于未提供,只做长度上限校验。 */
88
+ export function readOptionalTaskId(value) {
89
+ if (value === undefined || value === null || value === '')
90
+ return undefined;
91
+ if (typeof value !== 'string' || value.length > 256) {
92
+ throw new DatasetStoreError('profile_invalid', 'task_id is invalid');
93
+ }
94
+ return value;
95
+ }
96
+ /** 可选的字符串参数(time_column / primary_key):非字符串一律视为未提供。 */
97
+ export function readOptionalString(value) {
98
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
99
+ }
100
+ /**
101
+ * 列名过滤参数(describe_dataset 的 `columns_of_interest`)。
102
+ *
103
+ * 接受真数组 / JSON 数组字符串 / 单个列名字符串;解析结果不是数组就抛错——静默忽略一个
104
+ * 写错的过滤条件会让模型以为"结果已经收窄",而实际返回的是全量(或反过来),所以这里响亮失败。
105
+ */
106
+ export function readOptionalColumnList(value) {
107
+ if (value === undefined || value === null || value === '')
108
+ return undefined;
109
+ const coerced = coerceNameList(value);
110
+ if (!Array.isArray(coerced) || coerced.some((item) => typeof item !== 'string' || item.length === 0 || item.length > 128)) {
111
+ throw new DatasetStoreError('profile_invalid', 'columns_of_interest must be an array of column names');
112
+ }
113
+ return [...new Set(coerced)];
114
+ }
@@ -0,0 +1,3 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { WorkspaceDatasetStore } from './store.js';
3
+ export declare function registerDatasetTools(ctx: Context, store: WorkspaceDatasetStore): void;
@@ -0,0 +1,421 @@
1
+ import { MAX_QUERY_AGGREGATES, MAX_QUERY_FILTERS, MAX_QUERY_GROUP_BY, MAX_QUERY_LIMIT, MAX_QUERY_OUTPUT_BYTES, DatasetQueryError, } from './query.js';
2
+ import { coerceQueryShape, isRecord, parseJsonContainer, readDatasetId, readOptionalTaskId, } from './args.js';
3
+ import { MAX_DESCRIBE_QUERIES, SAFE_RESULT_CHARS, describeDataset, normalizeDescribeArgs, } from './describe.js';
4
+ import { normalizeQueryDates, withIsoTimeColumns } from './query-time.js';
5
+ import { delegatedSession } from '../tool-exec.js';
6
+ const jsonObject = (properties = {}, required = []) => ({
7
+ type: 'object',
8
+ properties,
9
+ required,
10
+ additionalProperties: false,
11
+ });
12
+ const datasetIdSchema = { type: 'string' };
13
+ const profileStatisticSchema = jsonObject({
14
+ count: { type: 'integer' },
15
+ sum: { oneOf: [{ type: 'number' }, { type: 'null' }] },
16
+ min: { oneOf: [{ type: 'number' }, { type: 'null' }] },
17
+ max: { oneOf: [{ type: 'number' }, { type: 'null' }] },
18
+ mean: { oneOf: [{ type: 'number' }, { type: 'null' }] },
19
+ p25: { oneOf: [{ type: 'number' }, { type: 'null' }] },
20
+ p50: { oneOf: [{ type: 'number' }, { type: 'null' }] },
21
+ p75: { oneOf: [{ type: 'number' }, { type: 'null' }] },
22
+ });
23
+ const profileCategorySchema = jsonObject({
24
+ distinct_count: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
25
+ top_values: { type: 'array', items: jsonObject({ value: { type: 'string' }, count: { type: 'integer' } }, ['value', 'count']) },
26
+ truncated: { type: 'boolean' },
27
+ }, ['distinct_count', 'top_values', 'truncated']);
28
+ const profileTimeFactsSchema = jsonObject({
29
+ time_column: { type: 'string' },
30
+ ordered_ascending: { oneOf: [{ type: 'boolean' }, { type: 'null' }] },
31
+ covered_from: {},
32
+ covered_to: {},
33
+ covered_from_iso: { type: 'string' },
34
+ covered_to_iso: { type: 'string' },
35
+ axis: jsonObject({
36
+ column: { type: 'string' },
37
+ value_format: { type: 'string', enum: ['epoch_ms', 'date_string', 'unknown'] },
38
+ time_zone: { type: 'string' },
39
+ utc_offset: { type: 'string' },
40
+ aligned_to: { type: 'string', enum: ['local_midnight', 'calendar_day', 'unknown'] },
41
+ }, ['column', 'value_format']),
42
+ windows: {
43
+ type: 'array',
44
+ items: jsonObject({
45
+ name: { type: 'string' },
46
+ start_date: { type: 'string' },
47
+ end_date: { type: 'string' },
48
+ value_ge: {},
49
+ value_le: {},
50
+ data_from: { oneOf: [{ type: 'string' }, { type: 'null' }] },
51
+ data_to: { oneOf: [{ type: 'string' }, { type: 'null' }] },
52
+ }, ['name', 'start_date', 'end_date', 'value_ge', 'value_le']),
53
+ },
54
+ first: { type: 'object', additionalProperties: true },
55
+ last: { type: 'object', additionalProperties: true },
56
+ }, ['time_column', 'ordered_ascending', 'covered_from', 'covered_to', 'axis', 'first', 'last']);
57
+ const profileDocumentSchema = jsonObject({
58
+ content: {},
59
+ truncated: { type: 'boolean' },
60
+ omitted: { type: 'array', items: jsonObject({ path: { type: 'string' }, kept: { type: 'integer' }, omitted: { type: 'integer' } }, ['path', 'kept', 'omitted']) },
61
+ }, ['content', 'truncated', 'omitted']);
62
+ const profileStructureSchema = jsonObject({
63
+ type: { type: 'string', enum: ['array', 'object'] },
64
+ occurrences: { type: 'integer' },
65
+ total_elements: { type: 'integer' },
66
+ sampled_elements: { type: 'integer' },
67
+ empty_count: { type: 'integer' },
68
+ min_length: { type: 'integer' },
69
+ max_length: { type: 'integer' },
70
+ fields: { type: 'array', items: jsonObject({ name: { type: 'string' }, type: { type: 'string' } }, ['name', 'type']) },
71
+ }, ['type', 'occurrences', 'empty_count', 'min_length', 'max_length', 'fields']);
72
+ const profileColumnSchema = jsonObject({
73
+ contract_type: { oneOf: [{ type: 'string', enum: ['string', 'number', 'integer', 'boolean', 'object', 'array', 'json'] }, { type: 'null' }] },
74
+ inferred_type: { type: 'string', enum: ['missing', 'null', 'boolean', 'integer', 'number', 'string', 'object', 'array', 'mixed'] },
75
+ observed_types: { type: 'object', additionalProperties: { type: 'integer' } },
76
+ missing_count: { type: 'integer' },
77
+ null_count: { type: 'integer' },
78
+ non_null_count: { type: 'integer' },
79
+ invalid_count: { type: 'integer' },
80
+ nullable: { type: 'boolean' },
81
+ contract_nullable: { type: 'boolean' },
82
+ required: { type: 'boolean' },
83
+ allow_numeric_string: { type: 'boolean' },
84
+ numeric_string_count: { type: 'integer' },
85
+ }, ['contract_type', 'inferred_type', 'observed_types', 'missing_count', 'null_count', 'non_null_count', 'invalid_count', 'nullable']);
86
+ const profileValidationSchema = jsonObject({
87
+ status: { type: 'string', enum: ['pass', 'fail'] },
88
+ violations: { type: 'array', items: jsonObject({
89
+ column: { type: 'string' },
90
+ rule: { type: 'string', enum: ['required', 'nullable', 'type'] },
91
+ expected: { oneOf: [{ type: 'string' }, { type: 'boolean' }] },
92
+ observed: { type: 'string' },
93
+ invalid_count: { type: 'integer' },
94
+ severity: { type: 'string', enum: ['error', 'warning'] },
95
+ }, ['column', 'rule', 'expected', 'observed', 'invalid_count', 'severity']) },
96
+ }, ['status', 'violations']);
97
+ function render(_args, value) {
98
+ return [{ type: 'text', text: JSON.stringify(value) }];
99
+ }
100
+ function tool(name, description, parameters, schema, execute, options = {}) {
101
+ return {
102
+ name,
103
+ description,
104
+ parameters,
105
+ output: { schema, render },
106
+ execute,
107
+ // `isConcurrencySafe` 是框架的**现成机制**:为 true 时,同一个 assistant 步里的多个调用
108
+ // 会被放进有界并发池(默认上限 10),结果仍按模型顺序在**同一个下一步**一起返回。
109
+ // 这正是 describe_dataset 省往返的手段——多个 dataset 由模型一次发完,而不是把 N 份结果
110
+ // 合并成一个超长载荷(见 docs/design/describe-dataset-design.md §2.1)。
111
+ ...(options.concurrencySafe === true ? { isConcurrencySafe: () => true } : {}),
112
+ };
113
+ }
114
+ /**
115
+ * 模型经常把数组/对象参数序列化成 JSON 字符串再传(实测:`"select": "[\"date_ms\"]"`、
116
+ * `"group_by": "[]"`、`"aggregates": "[{...}]"`、`"limit": "1"`),于是参数其实是对的,
117
+ * 却报 `query_spec_invalid: select must contain 1-32 column names`,模型只能反复试错。
118
+ *
119
+ * 宽容解析原语已外迁到 src/data-collector/args.ts:`query_dataset` 与 `describe_dataset`
120
+ * 必须共用同一套解析(两份实现漂移会让模型在换工具后重新踩同一个坑)。
121
+ */
122
+ /** envelope 自身的元数据字段,不属于 QuerySpec;模型按协议原样传 envelope 时忽略它们。 */
123
+ const QUERY_ENVELOPE_FIELDS = ['type', 'task_id', 'time_column'];
124
+ function normalizeQueryArgs(args) {
125
+ const outerDatasetId = readDatasetId(args.dataset_id);
126
+ const envelope = isRecord(args.query) ? args.query : parseJsonContainer(args.query);
127
+ if (isRecord(envelope)) {
128
+ const query = coerceQueryShape(envelope);
129
+ if (query.dataset_id === undefined)
130
+ query.dataset_id = outerDatasetId;
131
+ return { dataset_id: outerDatasetId, query: query };
132
+ }
133
+ if (args.query !== undefined) {
134
+ throw new DatasetQueryError('query_spec_invalid', 'query must be an object (a JSON object string is also accepted)');
135
+ }
136
+ const flat = coerceQueryShape(args);
137
+ for (const field of QUERY_ENVELOPE_FIELDS)
138
+ delete flat[field];
139
+ return { dataset_id: outerDatasetId, query: flat };
140
+ }
141
+ const datasetRefProperties = { dataset_id: { type: 'string' },
142
+ task_id: { oneOf: [{ type: 'string' }, { type: 'null' }] },
143
+ session_id: { type: 'string' },
144
+ artifact_ref: { type: 'string' },
145
+ format: { type: 'string' },
146
+ capability: { type: 'string' },
147
+ source_label: { type: 'string' },
148
+ schema: { oneOf: [{ type: 'object', additionalProperties: true }, { type: 'null' }] },
149
+ row_count: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
150
+ captured_at: { type: 'integer' },
151
+ retention_until: { type: 'integer' },
152
+ params_digest: { type: 'string' },
153
+ };
154
+ const datasetRefWithAccessSchema = jsonObject({
155
+ ...datasetRefProperties,
156
+ query_access: jsonObject({
157
+ readable: { type: 'boolean' },
158
+ shape: { type: 'string', enum: ['array', 'envelope_item', 'document', 'none'] },
159
+ reason: { type: 'string' },
160
+ }, ['readable', 'shape']),
161
+ }, [
162
+ 'dataset_id', 'task_id', 'session_id', 'artifact_ref', 'format', 'capability',
163
+ 'source_label', 'schema', 'row_count', 'captured_at', 'retention_until',
164
+ 'params_digest', 'query_access',
165
+ ]);
166
+ const profileRefSchema = jsonObject({
167
+ profile_id: { type: 'string' },
168
+ dataset_id: { type: 'string' },
169
+ task_id: { oneOf: [{ type: 'string' }, { type: 'null' }] },
170
+ session_id: { type: 'string' },
171
+ artifact_ref: { type: 'string' },
172
+ created_at: { type: 'integer' },
173
+ retention_until: { type: 'integer' },
174
+ }, ['profile_id', 'dataset_id', 'task_id', 'session_id', 'artifact_ref', 'created_at', 'retention_until']);
175
+ const profileDatasetResultSchema = jsonObject({
176
+ profile_id: { type: 'string' },
177
+ dataset_id: { type: 'string' },
178
+ task_id: { oneOf: [{ type: 'string' }, { type: 'null' }] },
179
+ session_id: { type: 'string' },
180
+ artifact_ref: { type: 'string' },
181
+ created_at: { type: 'integer' },
182
+ retention_until: { type: 'integer' },
183
+ row_count: { type: 'integer' },
184
+ columns: { type: 'array', items: { type: 'string' } },
185
+ quality: jsonObject({
186
+ missing_values: { type: 'integer' },
187
+ duplicate_rows: { type: 'integer' },
188
+ time_ordered: { oneOf: [{ type: 'boolean' }, { type: 'null' }] },
189
+ }, ['missing_values', 'duplicate_rows', 'time_ordered']),
190
+ statistics: { type: 'object', additionalProperties: true },
191
+ categories: { type: 'object', additionalProperties: true },
192
+ time_facts: profileTimeFactsSchema,
193
+ structure: { type: 'object', additionalProperties: true },
194
+ document: profileDocumentSchema,
195
+ schema: { type: 'object', additionalProperties: true },
196
+ validation: profileValidationSchema,
197
+ warnings: { type: 'array', items: { type: 'string' } },
198
+ }, ['profile_id', 'dataset_id', 'task_id', 'session_id', 'artifact_ref', 'created_at', 'retention_until', 'row_count', 'columns', 'quality', 'warnings']);
199
+ const queryNameSchema = { type: 'string' };
200
+ const queryFilterSchema = {
201
+ oneOf: [
202
+ jsonObject({
203
+ column: queryNameSchema,
204
+ operator: { type: 'string', enum: ['is_null', 'not_null'] },
205
+ }, ['column', 'operator']),
206
+ jsonObject({
207
+ column: queryNameSchema,
208
+ operator: { type: 'string', enum: ['=', '!=', '>', '>=', '<', '<=', 'in'] },
209
+ value: {},
210
+ }, ['column', 'operator', 'value']),
211
+ ],
212
+ };
213
+ const queryAggregateSchema = {
214
+ oneOf: [
215
+ jsonObject({
216
+ function: { type: 'string', enum: ['count'] },
217
+ column: queryNameSchema,
218
+ as: queryNameSchema,
219
+ }, ['function', 'as']),
220
+ jsonObject({
221
+ function: { type: 'string', enum: ['min', 'max', 'avg', 'sum'] },
222
+ column: queryNameSchema,
223
+ as: queryNameSchema,
224
+ }, ['function', 'column', 'as']),
225
+ ],
226
+ };
227
+ const queryOrderSchema = jsonObject({
228
+ column: queryNameSchema,
229
+ direction: { type: 'string', enum: ['asc', 'desc'] },
230
+ }, ['column', 'direction']);
231
+ const querySpecProperties = {
232
+ dataset_id: datasetIdSchema,
233
+ select: { type: 'array', items: queryNameSchema },
234
+ filters: { type: 'array', items: queryFilterSchema },
235
+ group_by: { type: 'array', items: queryNameSchema },
236
+ aggregates: { type: 'array', items: queryAggregateSchema },
237
+ order_by: { type: 'array', items: queryOrderSchema },
238
+ limit: { type: 'integer' },
239
+ error_policy: { type: 'string', enum: ['strict', 'skip_with_warning'] },
240
+ };
241
+ const querySpecSchema = jsonObject(querySpecProperties);
242
+ /** envelope 形态独有的三个字段;flat 形态把它们视为 QuerySpec 之外的元数据。 */
243
+ const queryEnvelopeProperties = {
244
+ type: { type: 'string', enum: ['query_request'], description: '可选:声明使用 query_request envelope 形态' },
245
+ task_id: { type: 'string' },
246
+ query: querySpecSchema,
247
+ };
248
+ /**
249
+ * 工具参数 schema 的根**只能**是 `{ type: 'object', properties, required }`。
250
+ *
251
+ * 事故(2026-09-14):这里原本是根级 `{ oneOf: [flat, envelope] }`,没有 `type`。
252
+ * 模型供应商在请求进入时就整体校验 `tools[].function.parameters`,直接返回
253
+ * `Invalid schema for function 'query_dataset': schema must be a JSON Schema of
254
+ * 'type: "object"', got 'type: null'`(HTTP 400)。这一步发生在模型生成第一个
255
+ * token 之前,而这套工具表在会话组作用域里**主 Agent 也带着**,于是每个 Capital
256
+ * 会话都在第 1 轮第 1 步整体失败,与用户问什么无关。
257
+ *
258
+ * flat 与 envelope 的判定本来就在工具边界由 normalizeQueryArgs 完成(query 存在即
259
+ * envelope),所以 schema 用「一个对象 + 两边字段都列出(都非必需,dataset_id 除外)」
260
+ * 描述即可——这比根级 oneOf 更准确:oneOf 的两个分支都带 additionalProperties:false,
261
+ * 混合传参(如 query + limit)在任何分支下都不合法,而宿主其实接受。
262
+ * 回归测试:test/apply-integration.test.mjs(所有注册工具的 parameters 根必须是 object)。
263
+ */
264
+ const queryParametersSchema = jsonObject({
265
+ ...querySpecProperties,
266
+ ...queryEnvelopeProperties,
267
+ time_column: { type: 'string' },
268
+ }, ['dataset_id']);
269
+ const queryResultSchema = jsonObject({
270
+ dataset_id: { type: 'string' },
271
+ columns: { type: 'array', items: { type: 'string' } },
272
+ rows: { type: 'array', items: { type: 'object', additionalProperties: true } },
273
+ matched_row_count: { type: 'integer' },
274
+ group_count: { type: 'integer' },
275
+ returned_count: { type: 'integer' },
276
+ limit: { type: 'integer' },
277
+ warnings: { type: 'array', items: { type: 'string' } },
278
+ }, ['dataset_id', 'columns', 'rows', 'matched_row_count', 'group_count', 'returned_count', 'limit', 'warnings']);
279
+ /**
280
+ * `describe_dataset` 的参数:**一次只处理一个 dataset_id**。
281
+ *
282
+ * 多个 dataset 由模型在**同一条 assistant 消息**里发多个调用完成——本工具声明了
283
+ * `isConcurrencySafe`,框架会把它们放进并发池,N 份结果在同一个下一步一起返回。
284
+ * 因此这里刻意**不**接受 `dataset_ids` 数组:把 N 份结果合并成一个工具结果,才是
285
+ * 体积预算/digest 那套复杂机制的来源(见 docs/design/describe-dataset-design.md §2.1)。
286
+ */
287
+ const describeParametersSchema = jsonObject({
288
+ dataset_id: datasetIdSchema,
289
+ task_id: { type: 'string' },
290
+ time_column: { type: 'string' },
291
+ primary_key: { type: 'string' },
292
+ columns_of_interest: { type: 'array', items: queryNameSchema },
293
+ queries: { type: 'array', items: querySpecSchema },
294
+ }, ['dataset_id']);
295
+ const describeQueryOutcomeSchema = jsonObject({
296
+ index: { type: 'integer' },
297
+ result: queryResultSchema,
298
+ error: jsonObject({ code: { type: 'string' }, detail: { type: 'string' } }, ['code', 'detail']),
299
+ }, ['index']);
300
+ /**
301
+ * 输出覆盖两种形态:正常结果(`status: 'ok'`)与体积闸门回执(`status: 'too_large'`)。
302
+ * 根必须是单一 object(根级 oneOf 会被供应商 400 拒绝,见上面 query_dataset 的事故注释),
303
+ * 所以两边的字段都列出来、都非必需,只有 `status` 与 `dataset_id` 恒有。
304
+ */
305
+ const describeResultSchema = jsonObject({
306
+ status: { type: 'string', enum: ['ok', 'too_large'] },
307
+ dataset_id: { type: 'string' },
308
+ task_id: { oneOf: [{ type: 'string' }, { type: 'null' }] },
309
+ session_id: { type: 'string' },
310
+ artifact_ref: { type: 'string' },
311
+ format: { type: 'string' },
312
+ capability: { type: 'string' },
313
+ source_label: { type: 'string' },
314
+ row_count: { type: 'integer' },
315
+ captured_at: { type: 'integer' },
316
+ retention_until: { type: 'integer' },
317
+ params_digest: { type: 'string' },
318
+ query_access: jsonObject({
319
+ readable: { type: 'boolean' },
320
+ shape: { type: 'string', enum: ['array', 'envelope_item', 'document', 'none'] },
321
+ reason: { type: 'string' },
322
+ }, ['readable', 'shape']),
323
+ profile_id: { type: 'string' },
324
+ profile_ref: { type: 'string' },
325
+ columns: { type: 'array', items: { type: 'string' } },
326
+ quality: jsonObject({
327
+ missing_values: { type: 'integer' },
328
+ duplicate_rows: { type: 'integer' },
329
+ time_ordered: { oneOf: [{ type: 'boolean' }, { type: 'null' }] },
330
+ }, ['missing_values', 'duplicate_rows', 'time_ordered']),
331
+ statistics: { type: 'object', additionalProperties: true },
332
+ categories: { type: 'object', additionalProperties: true },
333
+ time_facts: profileTimeFactsSchema,
334
+ structure: { type: 'object', additionalProperties: true },
335
+ document: profileDocumentSchema,
336
+ schema: { type: 'object', additionalProperties: true },
337
+ validation: profileValidationSchema,
338
+ warnings: { type: 'array', items: { type: 'string' } },
339
+ queries: { type: 'array', items: describeQueryOutcomeSchema },
340
+ /** 仅 too_large:被拦下的形状、字节数与重试指引。 */
341
+ shape: { type: 'string', enum: ['array', 'envelope_item', 'document', 'none'] },
342
+ bytes: { type: 'integer' },
343
+ hint: { type: 'string' },
344
+ }, ['status', 'dataset_id']);
345
+ export function registerDatasetTools(ctx, store) {
346
+ const tools = ctx.get('tools');
347
+ if (!tools)
348
+ return;
349
+ const registrations = [
350
+ 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)),
351
+ 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({
352
+ dataset_id: datasetIdSchema,
353
+ task_id: { type: 'string' },
354
+ time_column: { type: 'string' },
355
+ primary_key: { type: 'string' },
356
+ }, ['dataset_id']), profileDatasetResultSchema, async (args, exec) => store.profileDataset({
357
+ session: delegatedSession(exec, 'profile_dataset'),
358
+ dataset_id: readDatasetId(args.dataset_id),
359
+ task_id: readOptionalTaskId(args.task_id),
360
+ time_column: typeof args.time_column === 'string' ? args.time_column : undefined,
361
+ primary_key: typeof args.primary_key === 'string' ? args.primary_key : undefined,
362
+ signal: exec.signal,
363
+ })),
364
+ 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) => {
365
+ const normalized = normalizeQueryArgs(args);
366
+ const session = delegatedSession(exec, 'query_dataset');
367
+ const timeColumn = typeof args.time_column === 'string' && args.time_column.length > 0 ? args.time_column : undefined;
368
+ const prepared = await normalizeQueryDates({
369
+ store,
370
+ session,
371
+ dataset_id: normalized.dataset_id,
372
+ query: normalized.query,
373
+ time_column: timeColumn,
374
+ signal: exec.signal,
375
+ });
376
+ const result = await store.queryDataset({
377
+ session,
378
+ dataset_id: normalized.dataset_id,
379
+ query: prepared.query,
380
+ signal: exec.signal,
381
+ });
382
+ if (prepared.axis !== undefined)
383
+ withIsoTimeColumns(result, prepared.axis);
384
+ return result;
385
+ }),
386
+ 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, hint}:按 hint 用 columns_of_interest(或减少 queries,最多 ${MAX_DESCRIBE_QUERIES} 条)重发一次即可。单点复核仍可用 inspect_dataset / profile_dataset / query_dataset;不接受任何文件路径、脚本内容或自定义表达式。`, describeParametersSchema, describeResultSchema, async (args, exec) => describeDataset({
387
+ store,
388
+ session: delegatedSession(exec, 'describe_dataset'),
389
+ request: normalizeDescribeArgs(args),
390
+ signal: exec.signal,
391
+ }),
392
+ // 让"同一条消息里的多个 describe_dataset"进框架并发池:这是省往返的机制本身。
393
+ { concurrencySafe: true }),
394
+ tool('write_profile', '将当前 session 对已授权 Dataset 生成的基础质量 profile 持久化到 workspace。宿主生成不可变 profile_ref;profile 不得包含原始 rows、文件路径、凭据或未定义字段。', jsonObject({
395
+ dataset_id: datasetIdSchema,
396
+ task_id: { type: 'string' },
397
+ profile: jsonObject({
398
+ row_count: { type: 'integer' },
399
+ columns: { type: 'array', items: { type: 'string' } },
400
+ quality: jsonObject({
401
+ missing_values: { type: 'integer' },
402
+ duplicate_rows: { type: 'integer' },
403
+ time_ordered: { oneOf: [{ type: 'boolean' }, { type: 'null' }] },
404
+ }, ['missing_values', 'duplicate_rows', 'time_ordered']),
405
+ statistics: { type: 'object', additionalProperties: true },
406
+ schema: { type: 'object', additionalProperties: true },
407
+ validation: profileValidationSchema,
408
+ warnings: { type: 'array', items: { type: 'string' } },
409
+ }, ['row_count', 'columns', 'quality', 'warnings']),
410
+ }, ['dataset_id', 'profile']), profileRefSchema, async (args, exec) => store.writeProfile({
411
+ session: delegatedSession(exec, 'write_profile'),
412
+ dataset_id: readDatasetId(args.dataset_id),
413
+ task_id: readOptionalTaskId(args.task_id),
414
+ profile: args.profile,
415
+ signal: exec.signal,
416
+ })),
417
+ ];
418
+ for (const definition of registrations) {
419
+ ctx.effect(() => tools.register(definition), `capital-generation.dataset-tool(${definition.name})`);
420
+ }
421
+ }
@@ -0,0 +1,99 @@
1
+ import { type DatasetInspection, type ProfileDatasetResult, type SessionLike, type WorkspaceDatasetStore } from './store.js';
2
+ import { type QueryResult, type QuerySpec } from './query.js';
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
+ * **后者会让模型知道发生了什么并给出重试办法**。
23
+ */
24
+ export declare const SAFE_RESULT_CHARS = 7000;
25
+ /** 单次调用的 queries 条数上限:防的是请求体积,不是结果体积(结果另有闸门)。 */
26
+ export declare const MAX_DESCRIBE_QUERIES = 8;
27
+ export interface DescribeRequest {
28
+ dataset_id: string;
29
+ task_id?: string;
30
+ time_column?: string;
31
+ primary_key?: string;
32
+ /** 只对这些列返回 statistics / categories / schema;省略即全量。 */
33
+ columns_of_interest?: string[];
34
+ /** 受控 QuerySpec 列表;`dataset_id` 由管线补成当前 dataset。 */
35
+ queries: QuerySpec[];
36
+ }
37
+ export interface DescribeQueryOutcome {
38
+ index: number;
39
+ result?: QueryResult;
40
+ error?: {
41
+ code: string;
42
+ detail: string;
43
+ };
44
+ }
45
+ export interface DescribeResult {
46
+ status: 'ok';
47
+ dataset_id: string;
48
+ task_id: string | null;
49
+ session_id: string;
50
+ artifact_ref: string;
51
+ format: string;
52
+ capability: string;
53
+ source_label: string;
54
+ row_count: number;
55
+ captured_at: number;
56
+ retention_until: number;
57
+ params_digest: string;
58
+ query_access: DatasetInspection['query_access'];
59
+ profile_id: string;
60
+ profile_ref: string;
61
+ columns: string[];
62
+ quality: ProfileDatasetResult['quality'];
63
+ statistics?: ProfileDatasetResult['statistics'];
64
+ categories?: ProfileDatasetResult['categories'];
65
+ time_facts?: ProfileDatasetResult['time_facts'];
66
+ structure?: ProfileDatasetResult['structure'];
67
+ document?: ProfileDatasetResult['document'];
68
+ schema?: ProfileDatasetResult['schema'];
69
+ validation?: ProfileDatasetResult['validation'];
70
+ warnings: string[];
71
+ queries: DescribeQueryOutcome[];
72
+ }
73
+ export interface DescribeTooLarge {
74
+ status: 'too_large';
75
+ dataset_id: string;
76
+ profile_ref: string;
77
+ shape: DatasetInspection['query_access']['shape'];
78
+ columns: string[];
79
+ bytes: number;
80
+ hint: string;
81
+ }
82
+ export type DescribeOutput = DescribeResult | DescribeTooLarge;
83
+ /**
84
+ * 工具边界归一化:宽容解析(字符串化的 JSON 数组/对象、单个对象、envelope 形态),
85
+ * 引擎侧保持严格。与 `query_dataset` 的 `normalizeQueryArgs` 共用同一套解析原语
86
+ * (src/data-collector/args.ts),避免两个工具的宽容度漂移。
87
+ */
88
+ export declare function normalizeDescribeArgs(args: Record<string, unknown>): DescribeRequest;
89
+ export interface DescribeDatasetInput {
90
+ store: WorkspaceDatasetStore;
91
+ session: SessionLike;
92
+ request: DescribeRequest;
93
+ signal?: AbortSignal;
94
+ }
95
+ /**
96
+ * 单份 Dataset 的完整管线。失败一律抛错(`isError`):单 dataset 调用里没有"部分成功",
97
+ * 把失败做成成功信封会让会话日志无法区分(见 docs/design/tool-result-error-signal.md)。
98
+ */
99
+ export declare function describeDataset(input: DescribeDatasetInput): Promise<DescribeOutput>;