@v587d/capital-generation 2.8.3 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -1
- package/README.md +40 -19
- package/chart-ui/README.md +5 -4
- package/chart-ui/client.src.cjs +7 -16
- package/chart-ui/index.js +12 -33
- package/chart-ui/package.json +2 -2
- package/config/client.js +18 -39
- package/config/client.src.cjs +79 -217
- package/config/index.js +22 -72
- package/config/package.json +2 -2
- package/cordis.patch.yml +46 -51
- package/lib/agents/bash-guard.d.ts +9 -55
- package/lib/agents/bash-guard.js +27 -67
- package/lib/agents/preset-roster.d.ts +22 -0
- package/lib/agents/preset-roster.js +87 -0
- package/lib/agents/role-tool-policy.d.ts +46 -0
- package/lib/agents/role-tool-policy.js +205 -0
- package/lib/agents/root-tool-policy.d.ts +28 -124
- package/lib/agents/root-tool-policy.js +164 -237
- package/lib/agents/selected-skills-provider.d.ts +2 -6
- package/lib/agents/selected-skills-provider.js +27 -84
- package/lib/capital-settings.d.ts +27 -0
- package/lib/capital-settings.js +23 -0
- package/lib/chart/artifact-ref.d.ts +13 -8
- package/lib/chart/artifact-ref.js +27 -10
- package/lib/chart/downsample.d.ts +2 -5
- package/lib/chart/downsample.js +3 -8
- package/lib/chart/errors.d.ts +3 -7
- package/lib/chart/errors.js +1 -2
- package/lib/chart/event-context.d.ts +8 -0
- package/lib/chart/event-context.js +39 -0
- package/lib/chart/events.d.ts +21 -78
- package/lib/chart/events.js +43 -189
- package/lib/chart/html.d.ts +2 -5
- package/lib/chart/html.js +8 -15
- package/lib/chart/present-tool.d.ts +28 -0
- package/lib/chart/present-tool.js +128 -0
- package/lib/chart/runtime.d.ts +5 -18
- package/lib/chart/runtime.js +14 -34
- package/lib/chart/series.d.ts +10 -17
- package/lib/chart/series.js +22 -47
- package/lib/chart/source-token.d.ts +5 -13
- package/lib/chart/source-token.js +18 -17
- package/lib/chart/spec.d.ts +6 -12
- package/lib/chart/spec.js +7 -31
- package/lib/chart/tool.d.ts +7 -29
- package/lib/chart/tool.js +15 -25
- package/lib/chart-render-entry.d.ts +10 -0
- package/lib/chart-render-entry.js +14 -0
- package/lib/chart-source-entry.d.ts +13 -0
- package/lib/chart-source-entry.js +20 -0
- package/lib/collect-entry.d.ts +9 -0
- package/lib/collect-entry.js +10 -0
- package/lib/collector-runtime.d.ts +22 -0
- package/lib/collector-runtime.js +97 -0
- package/lib/data-collector/args.d.ts +6 -24
- package/lib/data-collector/args.js +6 -24
- package/lib/data-collector/dataset-tools.d.ts +7 -1
- package/lib/data-collector/dataset-tools.js +27 -56
- package/lib/data-collector/describe.d.ts +8 -33
- package/lib/data-collector/describe.js +17 -70
- package/lib/data-collector/hub.d.ts +27 -85
- package/lib/data-collector/hub.js +19 -85
- package/lib/data-collector/query-time.d.ts +7 -26
- package/lib/data-collector/query-time.js +8 -27
- package/lib/data-collector/store.d.ts +41 -117
- package/lib/data-collector/store.js +32 -111
- package/lib/data-collector/time-axis.d.ts +16 -76
- package/lib/data-collector/time-axis.js +25 -111
- package/lib/data-collector/tools.d.ts +6 -6
- package/lib/data-collector/tools.js +34 -43
- package/lib/dataset-read-entry.d.ts +9 -0
- package/lib/dataset-read-entry.js +7 -0
- package/lib/index.d.ts +7 -117
- package/lib/index.js +43 -422
- package/lib/net/eastmoney-client.d.ts +13 -65
- package/lib/net/eastmoney-client.js +12 -53
- package/lib/net/throttle.d.ts +2 -13
- package/lib/net/throttle.js +3 -14
- package/lib/ocr/client.d.ts +12 -21
- package/lib/ocr/client.js +23 -40
- package/lib/ocr/markdown.d.ts +20 -26
- package/lib/ocr/markdown.js +23 -36
- package/lib/ocr/store.d.ts +14 -47
- package/lib/ocr/store.js +15 -45
- package/lib/ocr-entry.d.ts +9 -0
- package/lib/ocr-entry.js +10 -0
- package/lib/policy-entry.d.ts +17 -0
- package/lib/policy-entry.js +22 -0
- package/lib/present-entry.d.ts +9 -0
- package/lib/present-entry.js +13 -0
- package/lib/preset-claim.d.ts +22 -0
- package/lib/preset-claim.js +24 -0
- package/lib/preset-ids.d.ts +38 -0
- package/lib/preset-ids.js +46 -0
- package/lib/retriever-config.d.ts +37 -0
- package/lib/retriever-config.js +36 -0
- package/lib/retriever-runtime.d.ts +31 -0
- package/lib/retriever-runtime.js +82 -0
- package/lib/root-extras-entry.d.ts +17 -0
- package/lib/root-extras-entry.js +25 -0
- package/lib/runtime-entry.d.ts +15 -0
- package/lib/runtime-entry.js +5 -0
- package/lib/runtime.d.ts +21 -0
- package/lib/runtime.js +33 -0
- package/lib/search-entry.d.ts +9 -0
- package/lib/search-entry.js +9 -0
- package/lib/sources/eastmoney-http.js +20 -68
- package/lib/sources/ftshare-rest.d.ts +6 -34
- package/lib/sources/ftshare-rest.js +84 -292
- package/lib/sources/fuyao-core.d.ts +8 -18
- package/lib/sources/fuyao-core.js +10 -21
- package/lib/sources/fuyao-rest.d.ts +2 -6
- package/lib/sources/fuyao-rest.js +23 -102
- package/lib/sources/security-code.d.ts +6 -12
- package/lib/sources/security-code.js +6 -12
- package/lib/sources/security-identity.d.ts +1 -4
- package/lib/sources/security-identity.js +3 -8
- package/lib/sources/tencent-http.js +14 -28
- package/lib/sources/tencent-public-core.d.ts +8 -60
- package/lib/sources/tencent-public-core.js +12 -77
- package/lib/sources/wind-mcp.d.ts +0 -5
- package/lib/sources/wind-mcp.js +23 -78
- package/lib/sources-entry.d.ts +9 -0
- package/lib/sources-entry.js +9 -0
- package/lib/time/tools.d.ts +8 -31
- package/lib/time/tools.js +30 -66
- package/lib/time-entry.d.ts +9 -0
- package/lib/time-entry.js +7 -0
- package/lib/tool-exec.d.ts +45 -11
- package/lib/tool-exec.js +59 -6
- package/lib/user-customization.d.ts +14 -0
- package/lib/user-customization.js +47 -0
- package/lib/watchlist/tools.d.ts +3 -13
- package/lib/watchlist/tools.js +10 -30
- package/lib/watchlist-entry.d.ts +9 -0
- package/lib/watchlist-entry.js +6 -0
- package/lib/web-retriever/engines.d.ts +6 -11
- package/lib/web-retriever/engines.js +9 -14
- package/lib/web-retriever/html-markdown.d.ts +14 -34
- package/lib/web-retriever/html-markdown.js +39 -75
- package/lib/web-retriever/local-fetch.d.ts +22 -58
- package/lib/web-retriever/local-fetch.js +33 -77
- package/lib/web-retriever/retriever.d.ts +6 -18
- package/lib/web-retriever/retriever.js +7 -7
- package/lib/web-retriever/sources.d.ts +37 -124
- package/lib/web-retriever/sources.js +64 -196
- package/lib/web-retriever/tools.d.ts +43 -27
- package/lib/web-retriever/tools.js +99 -137
- package/lib/web-retriever/wind-client.d.ts +14 -31
- package/lib/web-retriever/wind-client.js +16 -32
- package/locale/zh.json +1 -1
- package/package.json +72 -7
- package/preset/capital-generation/README.md +87 -53
- package/preset/capital-generation/agent.patch.yml +532 -337
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -4
- package/preset/capital-generation/skills/capital-data-profile/SKILL.md +2 -2
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-equity-research/SKILL.md +4 -2
- package/preset/capital-generation/skills/capital-event-impact/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +30 -33
- package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +14 -16
- package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -1
- package/watchlist/client.js +15 -148
- package/watchlist/client.src.cjs +148 -662
- package/watchlist/holding-rules.js +11 -38
- package/watchlist/index.js +127 -397
- package/watchlist/package.json +2 -2
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import { DatasetStoreError } from './store.js';
|
|
2
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
|
-
* 在换工具后重新踩同一个坑。
|
|
3
|
+
* Lenient parameter parsing at the Dataset tool boundary: models often send stringified JSON.
|
|
4
|
+
* Shared by query_dataset and describe_dataset so both accept exactly the same shapes.
|
|
15
5
|
*/
|
|
16
6
|
export function isRecord(value) {
|
|
17
7
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -29,7 +19,7 @@ export function parseJsonContainer(value) {
|
|
|
29
19
|
return undefined;
|
|
30
20
|
}
|
|
31
21
|
}
|
|
32
|
-
/**
|
|
22
|
+
/** Accept a real array, a JSON array string, or a single column name. */
|
|
33
23
|
export function coerceNameList(value) {
|
|
34
24
|
if (Array.isArray(value))
|
|
35
25
|
return value;
|
|
@@ -43,7 +33,6 @@ export function coerceNameList(value) {
|
|
|
43
33
|
}
|
|
44
34
|
return value;
|
|
45
35
|
}
|
|
46
|
-
/** 对象数组:接受真数组、JSON 数组字符串,或单个对象(对象本身或其 JSON 字符串)。 */
|
|
47
36
|
export function coerceObjectList(value) {
|
|
48
37
|
if (Array.isArray(value))
|
|
49
38
|
return value;
|
|
@@ -64,7 +53,7 @@ export function coerceLimit(value) {
|
|
|
64
53
|
}
|
|
65
54
|
export const QUERY_NAME_LIST_FIELDS = ['select', 'group_by'];
|
|
66
55
|
export const QUERY_OBJECT_LIST_FIELDS = ['filters', 'aggregates', 'order_by'];
|
|
67
|
-
/**
|
|
56
|
+
/** Coerce QuerySpec array/object fields and limit to engine types; leave other fields untouched. */
|
|
68
57
|
export function coerceQueryShape(input) {
|
|
69
58
|
const shape = { ...input };
|
|
70
59
|
for (const field of QUERY_NAME_LIST_FIELDS)
|
|
@@ -77,14 +66,13 @@ export function coerceQueryShape(input) {
|
|
|
77
66
|
shape.limit = coerceLimit(shape.limit);
|
|
78
67
|
return shape;
|
|
79
68
|
}
|
|
80
|
-
/** dataset_id
|
|
69
|
+
/** Reject an invalid dataset_id at the tool boundary; never pass it to the store. */
|
|
81
70
|
export function readDatasetId(value) {
|
|
82
71
|
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(value)) {
|
|
83
72
|
throw new DatasetStoreError('dataset_id_invalid', 'dataset_id is invalid');
|
|
84
73
|
}
|
|
85
74
|
return value;
|
|
86
75
|
}
|
|
87
|
-
/** task_id 是可选元数据:空值等同于未提供,只做长度上限校验。 */
|
|
88
76
|
export function readOptionalTaskId(value) {
|
|
89
77
|
if (value === undefined || value === null || value === '')
|
|
90
78
|
return undefined;
|
|
@@ -93,16 +81,10 @@ export function readOptionalTaskId(value) {
|
|
|
93
81
|
}
|
|
94
82
|
return value;
|
|
95
83
|
}
|
|
96
|
-
/** 可选的字符串参数(time_column / primary_key):非字符串一律视为未提供。 */
|
|
97
84
|
export function readOptionalString(value) {
|
|
98
85
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
99
86
|
}
|
|
100
|
-
/**
|
|
101
|
-
* 列名过滤参数(describe_dataset 的 `columns_of_interest`)。
|
|
102
|
-
*
|
|
103
|
-
* 接受真数组 / JSON 数组字符串 / 单个列名字符串;解析结果不是数组就抛错——静默忽略一个
|
|
104
|
-
* 写错的过滤条件会让模型以为"结果已经收窄",而实际返回的是全量(或反过来),所以这里响亮失败。
|
|
105
|
-
*/
|
|
87
|
+
/** columns_of_interest: accept array/JSON/single name; a wrong shape fails loudly rather than silently widening results. */
|
|
106
88
|
export function readOptionalColumnList(value) {
|
|
107
89
|
if (value === undefined || value === null || value === '')
|
|
108
90
|
return undefined;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import { WorkspaceDatasetStore } from './store.js';
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Dataset read family. writeProfile is off by default: profiles are host-computed, and the tool is
|
|
5
|
+
* absent from every family row; the monolithic entry opts in explicitly.
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerDatasetTools(ctx: Context, store: WorkspaceDatasetStore, options?: {
|
|
8
|
+
writeProfile?: boolean;
|
|
9
|
+
}): void;
|
|
@@ -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 {
|
|
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
|
-
//
|
|
108
|
-
//
|
|
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
|
-
*
|
|
121
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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),
|
|
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:
|
|
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
|
-
//
|
|
372
|
-
//
|
|
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 =
|
|
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:
|
|
369
|
+
session: authorizedSession(ctx, exec, 'describe_dataset', DATASET_READING_PRESET_IDS),
|
|
400
370
|
request: normalizeDescribeArgs(args),
|
|
401
371
|
signal: exec.signal,
|
|
402
372
|
}),
|
|
403
|
-
//
|
|
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:
|
|
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
|
-
|
|
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
|
|
5
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
7
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
176
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
244
|
-
//
|
|
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({
|