@v587d/capital-generation 2.9.0 → 2.9.2
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 +57 -0
- package/README.md +4 -3
- package/chart-ui/README.md +4 -3
- package/chart-ui/package.json +1 -1
- package/config/package.json +1 -1
- package/lib/agents/bash-guard.d.ts +10 -6
- package/lib/agents/bash-guard.js +15 -12
- package/lib/agents/root-tool-policy.js +2 -0
- package/lib/chart/artifact-ref.d.ts +11 -1
- package/lib/chart/artifact-ref.js +25 -3
- package/lib/chart/delivery.d.ts +66 -0
- package/lib/chart/delivery.js +148 -0
- package/lib/chart/events.d.ts +2 -0
- package/lib/chart/events.js +55 -1
- package/lib/chart/present-tool.d.ts +36 -0
- package/lib/chart/present-tool.js +149 -0
- package/lib/chart/tool.d.ts +1 -0
- package/lib/chart/tool.js +9 -5
- package/lib/chart-render-entry.d.ts +2 -1
- package/lib/chart-render-entry.js +0 -3
- package/lib/data-collector/dataset-tools.js +1 -1
- package/lib/data-collector/store.d.ts +1 -0
- package/lib/present-entry.d.ts +19 -0
- package/lib/present-entry.js +114 -0
- package/lib/web-retriever/local-fetch.d.ts +1 -1
- package/lib/web-retriever/local-fetch.js +1 -1
- package/lib/web-retriever/wind-client.d.ts +1 -1
- package/lib/web-retriever/wind-client.js +1 -1
- package/package.json +8 -2
- package/preset/capital-generation/README.md +31 -13
- package/preset/capital-generation/agent.patch.yml +31 -34
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -4
- package/preset/capital-generation/skills/capital-data-profile/SKILL.md +9 -4
- package/preset/capital-generation/skills/capital-data-query/SKILL.md +34 -19
- package/preset/capital-generation/skills/capital-equity-research/SKILL.md +6 -1
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +15 -14
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +10 -5
- package/watchlist/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* present_charts: the root Agent's final-turn delivery step.
|
|
3
|
+
*
|
|
4
|
+
* render_chart only mints a chart_ref (the specialist row no longer auto-delivers). DSH's
|
|
5
|
+
* deliverable card replays only declarations recorded before the closing assistant message, so
|
|
6
|
+
* the card has to be written by the turn that carries the final answer: the root Agent presents
|
|
7
|
+
* its collected refs/deliverables here immediately before writing that answer. Never a custom
|
|
8
|
+
* event type — the session-event vocabulary is closed (see events.ts).
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
import type { ChartArtifactRegistry } from './artifact-ref.js';
|
|
12
|
+
import { type ChartEventPublisher } from './events.js';
|
|
13
|
+
import type { ChartDelivery } from './delivery.js';
|
|
14
|
+
import type { SessionLike } from '../data-collector/store.js';
|
|
15
|
+
/** One card can carry many files (the client collapses past four); the cap only bounds one event. */
|
|
16
|
+
export declare const MAX_PRESENT_CHARTS = 32;
|
|
17
|
+
export interface PresentChartsInput {
|
|
18
|
+
artifacts: ChartArtifactRegistry;
|
|
19
|
+
/** Lazy: the host projection may mount after this row; an absent publisher fails the call loudly. */
|
|
20
|
+
chartEvents: () => ChartEventPublisher | undefined;
|
|
21
|
+
/** Records the selection; the closing stream chunk writes it (normal path). */
|
|
22
|
+
delivery?: ChartDelivery;
|
|
23
|
+
/** Fallback-only guard when the stream listener is unavailable. */
|
|
24
|
+
readiness?: (session: SessionLike) => Promise<string | undefined>;
|
|
25
|
+
}
|
|
26
|
+
export interface PresentChartsReceipt {
|
|
27
|
+
presented: Array<{
|
|
28
|
+
chart_ref: string;
|
|
29
|
+
title: string;
|
|
30
|
+
}>;
|
|
31
|
+
failed: Array<{
|
|
32
|
+
chart_ref: string;
|
|
33
|
+
reason: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export declare function registerPresentChartsTool(ctx: Context, input: PresentChartsInput): void;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { MAX_CHARTS_PER_TURN } from './events.js';
|
|
2
|
+
import { callerSession } from '../tool-exec.js';
|
|
3
|
+
/** One card can carry many files (the client collapses past four); the cap only bounds one event. */
|
|
4
|
+
export const MAX_PRESENT_CHARTS = 32;
|
|
5
|
+
const jsonObject = (properties = {}, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
|
|
6
|
+
const parameters = jsonObject({
|
|
7
|
+
chart_refs: {
|
|
8
|
+
type: 'array',
|
|
9
|
+
items: { type: 'string' },
|
|
10
|
+
description: 'data_junior 回传的全部 chart_ref,一次传完(最多 8 个,不猜写)。与 task_id 至少给一个。',
|
|
11
|
+
},
|
|
12
|
+
task_id: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: '本任务 canonical task_id:省略 chart_refs 时按它登记本会话该任务的全部图表(上下文压缩后记不清 refs 时用)。',
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
const receiptSchema = jsonObject({
|
|
18
|
+
presented: {
|
|
19
|
+
type: 'array',
|
|
20
|
+
items: jsonObject({
|
|
21
|
+
chart_ref: { type: 'string' },
|
|
22
|
+
title: { type: 'string' },
|
|
23
|
+
}, ['chart_ref', 'title']),
|
|
24
|
+
},
|
|
25
|
+
failed: {
|
|
26
|
+
type: 'array',
|
|
27
|
+
items: jsonObject({
|
|
28
|
+
chart_ref: { type: 'string' },
|
|
29
|
+
reason: { type: 'string' },
|
|
30
|
+
}, ['chart_ref', 'reason']),
|
|
31
|
+
},
|
|
32
|
+
}, ['presented', 'failed']);
|
|
33
|
+
function normalizeRefs(value) {
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return [];
|
|
36
|
+
if (!Array.isArray(value))
|
|
37
|
+
throw new Error('present_charts 的 chart_refs 必须是字符串数组');
|
|
38
|
+
const refs = [];
|
|
39
|
+
for (const item of value) {
|
|
40
|
+
if (typeof item !== 'string' || item.trim().length === 0)
|
|
41
|
+
throw new Error('present_charts 的 chart_refs 里不能有空值');
|
|
42
|
+
const ref = item.trim();
|
|
43
|
+
if (!refs.includes(ref))
|
|
44
|
+
refs.push(ref);
|
|
45
|
+
}
|
|
46
|
+
if (refs.length > MAX_PRESENT_CHARTS) {
|
|
47
|
+
throw new Error(`present_charts 一次最多登记 ${MAX_PRESENT_CHARTS} 张图(收到 ${refs.length} 个 chart_ref)`);
|
|
48
|
+
}
|
|
49
|
+
return refs;
|
|
50
|
+
}
|
|
51
|
+
function normalizeTaskId(value) {
|
|
52
|
+
if (value === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
55
|
+
throw new Error('present_charts 的 task_id 必须是非空字符串');
|
|
56
|
+
return value.trim();
|
|
57
|
+
}
|
|
58
|
+
/** Present refs first; otherwise present the whole task so compaction cannot strand the charts. */
|
|
59
|
+
function selectArtifacts(input, session, refs, taskId) {
|
|
60
|
+
const failed = [];
|
|
61
|
+
if (refs.length > 0) {
|
|
62
|
+
const selected = [];
|
|
63
|
+
for (const chartRef of refs) {
|
|
64
|
+
try {
|
|
65
|
+
selected.push(input.artifacts.resolve({ session, chart_ref: chartRef, task_id: taskId }));
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
failed.push({ chart_ref: chartRef, reason: error instanceof Error ? error.message : String(error) });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// A stale ref set must not strand charts the task_id can still find (compaction can mix both).
|
|
72
|
+
if (selected.length > 0 || taskId === undefined)
|
|
73
|
+
return { selected, failed };
|
|
74
|
+
}
|
|
75
|
+
if (taskId === undefined)
|
|
76
|
+
throw new Error('present_charts 需要 chart_refs 或 task_id:两者都不给就无法确定要登记哪几张图');
|
|
77
|
+
const all = input.artifacts.list({ session, task_id: taskId });
|
|
78
|
+
if (all.length === 0)
|
|
79
|
+
failed.push({ chart_ref: '(task)', reason: `本会话该 task_id 没有可登记的图表:${taskId}` });
|
|
80
|
+
const selected = all.slice(0, MAX_PRESENT_CHARTS);
|
|
81
|
+
if (all.length > MAX_PRESENT_CHARTS) {
|
|
82
|
+
failed.push({ chart_ref: '(task)', reason: `该 task_id 有 ${all.length} 张图,单次最多登记 ${MAX_PRESENT_CHARTS} 张;其余未展示` });
|
|
83
|
+
}
|
|
84
|
+
return { selected, failed };
|
|
85
|
+
}
|
|
86
|
+
export function registerPresentChartsTool(ctx, input) {
|
|
87
|
+
const tools = ctx.get('tools');
|
|
88
|
+
if (!tools)
|
|
89
|
+
return;
|
|
90
|
+
const definition = {
|
|
91
|
+
name: 'present_charts',
|
|
92
|
+
description: '把本会话已生成的图表登记为**最终答复的交付物**(官方 deliverables/presented):用户会在答复末尾的「本轮文件改动 / 交付」行看到卡片,点开即渲染自包含 chart.html。'
|
|
93
|
+
+ '**由主 Agent 调用一次**:参数是 data_junior 回传的 chart_refs(一次传入,最多 32 个);refs 已丢失时改传 task_id 登记该任务的全部图表。'
|
|
94
|
+
+ '这里只记录本轮要交付的图,宿主会在**最终答复之前**统一写入交付行——提前调用不会落进中间轮,不必等“最后一刻”,也不要在过程轮反复调用。'
|
|
95
|
+
+ '不要传文件路径或 HTML(路径由宿主解析);不调用时宿主仍会在收尾时自动登记本 exchange 已生成的图表。',
|
|
96
|
+
parameters,
|
|
97
|
+
async execute(args, exec) {
|
|
98
|
+
const session = callerSession(exec);
|
|
99
|
+
const refs = normalizeRefs(args.chart_refs);
|
|
100
|
+
const taskId = normalizeTaskId(args.task_id);
|
|
101
|
+
if (refs.length === 0 && taskId === undefined) {
|
|
102
|
+
throw new Error('present_charts 需要 chart_refs 或 task_id:两者都不给就无法确定要登记哪几张图');
|
|
103
|
+
}
|
|
104
|
+
const { selected, failed } = selectArtifacts(input, session, refs, taskId);
|
|
105
|
+
const receipt = { presented: [], failed };
|
|
106
|
+
if (selected.length === 0)
|
|
107
|
+
return receipt;
|
|
108
|
+
const inputs = selected.map((artifact) => ({
|
|
109
|
+
ownerSessionId: session.id,
|
|
110
|
+
chart_id: artifact.chart_id,
|
|
111
|
+
title: artifact.title,
|
|
112
|
+
html_path: artifact.html_path,
|
|
113
|
+
}));
|
|
114
|
+
// Normal path: remember the selection; the closing stream chunk writes it before the answer.
|
|
115
|
+
if (input.delivery?.streaming() === true) {
|
|
116
|
+
input.delivery.request(session, inputs);
|
|
117
|
+
for (const artifact of selected)
|
|
118
|
+
receipt.presented.push({ chart_ref: artifact.chart_ref, title: artifact.title });
|
|
119
|
+
return receipt;
|
|
120
|
+
}
|
|
121
|
+
// Fallback path (stream listener unavailable): write now, but only when the turn is idle.
|
|
122
|
+
const blocked = await input.readiness?.(session);
|
|
123
|
+
if (blocked !== undefined)
|
|
124
|
+
throw new Error(blocked);
|
|
125
|
+
const publisher = input.chartEvents();
|
|
126
|
+
if (publisher === undefined) {
|
|
127
|
+
throw new Error('图表交付通道不可用(sessions / sessionProjections 解析不到):图表已落盘,可在 workspace 打开 chart.html');
|
|
128
|
+
}
|
|
129
|
+
const accepted = publisher.publishMany !== undefined
|
|
130
|
+
? publisher.publishMany(session.id, inputs)
|
|
131
|
+
: inputs.every((item) => publisher.publish(item));
|
|
132
|
+
if (accepted) {
|
|
133
|
+
for (const artifact of selected)
|
|
134
|
+
receipt.presented.push({ chart_ref: artifact.chart_ref, title: artifact.title });
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
for (const artifact of selected) {
|
|
138
|
+
receipt.failed.push({ chart_ref: artifact.chart_ref, reason: `本轮登记已达 ${MAX_CHARTS_PER_TURN} 次上限或回合已关闭,未写入交付行` });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return receipt;
|
|
142
|
+
},
|
|
143
|
+
output: {
|
|
144
|
+
schema: receiptSchema,
|
|
145
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
ctx.effect(() => tools.register(definition), 'capital-generation.tool(present_charts)');
|
|
149
|
+
}
|
package/lib/chart/tool.d.ts
CHANGED
package/lib/chart/tool.js
CHANGED
|
@@ -112,7 +112,7 @@ export async function renderChart(input, args, exec) {
|
|
|
112
112
|
if (sourceCount !== 1) {
|
|
113
113
|
throw new ChartError('chart_source_invalid', 'provide exactly one data source: dataset_id, path, or chart_source_ref');
|
|
114
114
|
}
|
|
115
|
-
if (session.header?.
|
|
115
|
+
if (session.header?.origin === 'subagent' && chartSourceRef === undefined) {
|
|
116
116
|
throw new ChartError('chart_source_scope_mismatch', 'delegated visualization callers must use chart_source_ref');
|
|
117
117
|
}
|
|
118
118
|
const tokenSource = chartSourceRef === undefined ? undefined : chartSourceTokenForRender(input.sourceTokens, {
|
|
@@ -177,6 +177,7 @@ export async function renderChart(input, args, exec) {
|
|
|
177
177
|
captured_at: meta.captured_at ?? null,
|
|
178
178
|
kind: payload.kind,
|
|
179
179
|
axis: payload.axis,
|
|
180
|
+
title: payload.title,
|
|
180
181
|
chart_url: chartUrl,
|
|
181
182
|
spec_path: pathOf('spec.json'),
|
|
182
183
|
series_path: pathOf('series.json'),
|
|
@@ -185,9 +186,10 @@ export async function renderChart(input, args, exec) {
|
|
|
185
186
|
});
|
|
186
187
|
// Unit callers without the registry still get a stable receipt.
|
|
187
188
|
const chartRef = artifact?.chart_ref ?? `chart_${chartId.slice(3)}`;
|
|
188
|
-
//
|
|
189
|
-
// event type
|
|
190
|
-
//
|
|
189
|
+
// Monolith carriers still register this turn's deliverable here (first-party
|
|
190
|
+
// deliverables/presented only — never a custom event type; closed vocabulary breaks cold load).
|
|
191
|
+
// The auto-mode specialist row passes no chartEvents: the root Agent presents chart_refs in its
|
|
192
|
+
// final turn instead, so the card cannot land in a folded process turn.
|
|
191
193
|
let deliverableTrace;
|
|
192
194
|
try {
|
|
193
195
|
const publisher = input.chartEvents?.();
|
|
@@ -257,7 +259,9 @@ export function registerChartTool(ctx, options) {
|
|
|
257
259
|
`最多保留 ${MAX_CHART_POINTS} 个点(超出由宿主下采样并如实标注)。` +
|
|
258
260
|
'字段语义、可用键与错误码见 skill capital-chart-protocol —— 首次画图前先加载它。' +
|
|
259
261
|
'是否出图由 data_junior 的可视化协议决定;本工具只负责安全生成图表产物和小型回执。' +
|
|
260
|
-
|
|
262
|
+
(typeof options.chartEvents === 'function'
|
|
263
|
+
? '出图成功后宿主会把这张图登记为当前对话的**本轮交付物**(官方 deliverables/presented),用户可从收尾的交付行点开自包含图表;调用方**不要**在回传或正文里罗列图表文件、路径或 HTML。'
|
|
264
|
+
: '交付登记由主 Agent 在最终答复前调用 present_charts 完成:你只把 chart_ref 回传给 data_junior 汇总,**不要**调用 present_charts、不要在回传或正文里罗列图表文件、路径或 HTML。') +
|
|
261
265
|
'图表是呈现不是分析:需要数值结论(首末值、涨跌幅、分位数等)仍走 data_junior 的 profile / query,不要用图去推断数字。',
|
|
262
266
|
parameters,
|
|
263
267
|
output: { schema: receiptSchema, render },
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `tools/chart-render` row: render_chart only. Row-level expression of "only the
|
|
3
3
|
* visualization specialist holds the chart pipeline"; shares token/artifact state with
|
|
4
|
-
* `tools/chart-source`.
|
|
4
|
+
* `tools/chart-source`. No chartEvents here: delivery is the root Agent's final-turn
|
|
5
|
+
* `present_charts` step, so the card never lands in a folded process turn.
|
|
5
6
|
*/
|
|
6
7
|
import type { Context } from '@deepseek-ai/cordis';
|
|
7
8
|
export declare const name = "capital-chart-render-tools";
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createChartEventPublisher } from './chart/events.js';
|
|
2
|
-
import { chartEventContextOf } from './chart/event-context.js';
|
|
3
1
|
import { registerChartTool } from './chart/tool.js';
|
|
4
2
|
import { capitalDatasetStore } from './runtime.js';
|
|
5
3
|
import { capitalSharedRuntimeOf } from './chart-source-entry.js';
|
|
@@ -12,6 +10,5 @@ export function apply(ctx) {
|
|
|
12
10
|
sourceTokens: shared.sourceTokens,
|
|
13
11
|
artifacts: shared.artifacts,
|
|
14
12
|
charts: () => ctx.get('capitalCharts'),
|
|
15
|
-
chartEvents: () => createChartEventPublisher(chartEventContextOf(ctx)),
|
|
16
13
|
});
|
|
17
14
|
}
|
|
@@ -364,7 +364,7 @@ export function registerDatasetTools(ctx, store, options = {}) {
|
|
|
364
364
|
withIsoTimeColumns(result, prepared.axis);
|
|
365
365
|
return result;
|
|
366
366
|
}),
|
|
367
|
-
tool('describe_dataset', `读取一份 Dataset 的**默认入口**:一次调用完成 inspect(元数据与可读性/形状)、profile(宿主算基础质量与四类事实,写入 profile_ref)与受控 query(可选);原始 rows 不进入模型上下文。一次只处理一个 dataset_id
|
|
367
|
+
tool('describe_dataset', `读取一份 Dataset 的**默认入口**:一次调用完成 inspect(元数据与可读性/形状)、profile(宿主算基础质量与四类事实,写入 profile_ref)与受控 query(可选);原始 rows 不进入模型上下文。一次只处理一个 dataset_id——本轮有多份数据时请并发调用本工具(PTC 在同一个 run_code 程序里用 Promise.all,native 在同一条消息里一次发出多个调用),不要一份一份等。行集合返回:① 元数据 artifact_ref/capability/source_label/captured_at/retention_until 与 query_access.shape(array / envelope_item = 行集合;document = 顶层文档对象,只能 profile,queries 由宿主跳过);② profile 四类事实——statistics(count/sum/min/max/mean/分位数)、categories(distinct + 最多 5 个高频取值)、time_facts(覆盖范围与首行/末行取值;回答"最新值、区间涨跌"必须用它,不要用 min/max 代替)、structure(嵌套路径摘要);③ queries——每条的 QueryResult,或该条自己的 error(单条失败不影响其余)。columns_of_interest 只对这些列返回 statistics/categories/schema(并同样收窄 time_facts 的首末值投影,时间列永远保留),是收窄结果体积的唯一手段。结果超过 ${SAFE_RESULT_CHARS} 码点时宿主不返回超长载荷,而返回 {status:'too_large', profile_ref, columns, bytes, facts_bytes, queries_bytes, hint}:facts_bytes/queries_bytes 是体积归因(哪边大砍哪边),按 hint 重发一次即可——queries 大就先去掉 queries(聚合改单独走 query_dataset),facts 大就用 columns_of_interest 收窄列(或减少 queries,最多 ${MAX_DESCRIBE_QUERIES} 条)。单点复核仍可用 inspect_dataset / profile_dataset / query_dataset;不接受任何文件路径、脚本内容或自定义表达式。`, describeParametersSchema, describeResultSchema, async (args, exec) => describeDataset({
|
|
368
368
|
store,
|
|
369
369
|
session: authorizedSession(ctx, exec, 'describe_dataset', DATASET_READING_PRESET_IDS),
|
|
370
370
|
request: normalizeDescribeArgs(args),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tools/present` row: present_charts + the closing-answer delivery listener.
|
|
3
|
+
*
|
|
4
|
+
* The model only records which charts belong to the answer. The actual first-party
|
|
5
|
+
* `deliverables/presented` append happens in the terminal assistant-stream chunk, before the closing
|
|
6
|
+
* message commits, gated on an idle root (no actionable inbox input, no running direct children). That is the only
|
|
7
|
+
* point where "the final answer" is knowable, so early/no `present_charts` calls cannot strand the
|
|
8
|
+
* card in a folded process turn (rationale: docs/dev/chart-delivery-events.md §6.3).
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
import type { SessionLike } from './data-collector/store.js';
|
|
12
|
+
export declare const name = "capital-present-tools";
|
|
13
|
+
export declare const inject: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Fallback only: when the stream listener never fires, immediate delivery still has to avoid a
|
|
16
|
+
* pending turn. The normal path defers to the closing stream chunk and does not consult this.
|
|
17
|
+
*/
|
|
18
|
+
export declare function chartDeliveryBlocker(ctx: Context, session: SessionLike): Promise<string | undefined>;
|
|
19
|
+
export declare function apply(ctx: Context): void;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createChartDelivery } from './chart/delivery.js';
|
|
2
|
+
import { createChartEventPublisher } from './chart/events.js';
|
|
3
|
+
import { chartEventContextOf } from './chart/event-context.js';
|
|
4
|
+
import { registerPresentChartsTool } from './chart/present-tool.js';
|
|
5
|
+
import { capitalSharedRuntimeOf } from './chart-source-entry.js';
|
|
6
|
+
export const name = 'capital-present-tools';
|
|
7
|
+
export const inject = ['tools'];
|
|
8
|
+
/**
|
|
9
|
+
* Fallback only: when the stream listener never fires, immediate delivery still has to avoid a
|
|
10
|
+
* pending turn. The normal path defers to the closing stream chunk and does not consult this.
|
|
11
|
+
*/
|
|
12
|
+
export async function chartDeliveryBlocker(ctx, session) {
|
|
13
|
+
const projections = ctx.get('sessionProjections');
|
|
14
|
+
try {
|
|
15
|
+
const inbox = projections?.stateOf(session, 'inbox');
|
|
16
|
+
// A settlement notice can race the closing answer; it must not block the card. Any relay or
|
|
17
|
+
// human input still means another model step is pending.
|
|
18
|
+
const pending = [...(inbox?.['next-turn'] ?? []), ...(inbox?.['next-step'] ?? [])]
|
|
19
|
+
.filter((message) => {
|
|
20
|
+
const source = message?.source;
|
|
21
|
+
return source?.kind !== 'subagent-settled';
|
|
22
|
+
}).length;
|
|
23
|
+
if (pending > 0) {
|
|
24
|
+
return `还有 ${pending} 条待处理消息(子 Agent 回传 / 结算):现在登记会让卡片留在被折叠的中间轮。`
|
|
25
|
+
+ '等这些消息处理完、写最终答复的那一步再调用 present_charts(refs 记不清就传 task_id)。';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Projection unavailable: delivery availability matters more than the guard.
|
|
30
|
+
}
|
|
31
|
+
const subagents = ctx.get('subagents');
|
|
32
|
+
if (typeof subagents?.listDescendants === 'function') {
|
|
33
|
+
try {
|
|
34
|
+
const rows = await subagents.listDescendants(session.id);
|
|
35
|
+
const running = rows.filter((row) => row?.kind === 'child' && row?.activity === 'running').length;
|
|
36
|
+
if (running > 0) {
|
|
37
|
+
return `还有 ${running} 个子 Agent 在运行:现在登记会让卡片留在被折叠的中间轮。`
|
|
38
|
+
+ '等它们全部结算、写最终答复的那一步再调用 present_charts(refs 记不清就传 task_id)。';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Subagent service unavailable: don't block delivery for a missing optional check.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
function inboxOf(ctx) {
|
|
48
|
+
const projections = ctx.get('sessionProjections');
|
|
49
|
+
return (session) => {
|
|
50
|
+
try {
|
|
51
|
+
return projections?.stateOf(session, 'inbox');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function apply(ctx) {
|
|
59
|
+
const shared = capitalSharedRuntimeOf(ctx);
|
|
60
|
+
const publisherOf = () => createChartEventPublisher(chartEventContextOf(ctx));
|
|
61
|
+
const eventCtx = ctx;
|
|
62
|
+
const delivery = createChartDelivery({
|
|
63
|
+
artifacts: shared.artifacts,
|
|
64
|
+
publish: (sessionId, inputs) => publisherOf()?.publishMany(sessionId, inputs) ?? false,
|
|
65
|
+
inboxOf: inboxOf(ctx),
|
|
66
|
+
});
|
|
67
|
+
// The preset row is applied in the root agent's scope: these events are the root's own.
|
|
68
|
+
const disposers = [];
|
|
69
|
+
if (typeof eventCtx.on === 'function') {
|
|
70
|
+
disposers.push(eventCtx.on('agent/assistant-stream', (...args) => {
|
|
71
|
+
const payload = args[0];
|
|
72
|
+
if (payload?.frame === undefined)
|
|
73
|
+
return;
|
|
74
|
+
delivery.acceptStream(payload.agent?.session, payload.frame);
|
|
75
|
+
}));
|
|
76
|
+
disposers.push(eventCtx.on('subagent/start', (...args) => delivery.acceptSubagentStart(args[0])));
|
|
77
|
+
disposers.push(eventCtx.on('subagent/end', (...args) => delivery.acceptSubagentEnd(args[0])));
|
|
78
|
+
}
|
|
79
|
+
// Session events are not agent-scoped: listen on the unscoped root and filter by payload.
|
|
80
|
+
const listen = (eventCtx.root ?? ctx);
|
|
81
|
+
if (typeof listen.on === 'function') {
|
|
82
|
+
disposers.push(listen.on('session/event', (...args) => {
|
|
83
|
+
const session = args[0];
|
|
84
|
+
const event = args[1];
|
|
85
|
+
if (event?.type !== 'user/message')
|
|
86
|
+
return;
|
|
87
|
+
const source = event.data?.source;
|
|
88
|
+
// The settlement notice is authoritative even when the lifecycle edge was missed; clearing
|
|
89
|
+
// here keeps the stream-time idle check from waiting on a child that is already done.
|
|
90
|
+
if (source?.kind === 'subagent-settled') {
|
|
91
|
+
if (typeof source.senderSessionId === 'string')
|
|
92
|
+
delivery.acceptSubagentEnd({ id: source.senderSessionId });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (source?.kind !== 'user')
|
|
96
|
+
return;
|
|
97
|
+
if (typeof session?.id !== 'string')
|
|
98
|
+
return;
|
|
99
|
+
delivery.acceptUserMessage(session.id, typeof event.time === 'number' ? event.time : Date.now());
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
ctx.effect(() => () => {
|
|
103
|
+
for (const dispose of disposers) {
|
|
104
|
+
if (typeof dispose === 'function')
|
|
105
|
+
dispose();
|
|
106
|
+
}
|
|
107
|
+
}, 'capital-generation.chart-delivery()');
|
|
108
|
+
registerPresentChartsTool(ctx, {
|
|
109
|
+
artifacts: shared.artifacts,
|
|
110
|
+
chartEvents: publisherOf,
|
|
111
|
+
readiness: (session) => chartDeliveryBlocker(ctx, session),
|
|
112
|
+
delivery,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Known limit: DNS resolves per request but the IP is not pinned to the connection,
|
|
4
4
|
* so a rebinding window remains (same trade-off as dsh-search-first).
|
|
5
5
|
*/
|
|
6
|
-
export declare const LOCAL_FETCH_CLIENT_VERSION = "2.9.
|
|
6
|
+
export declare const LOCAL_FETCH_CLIENT_VERSION = "2.9.2";
|
|
7
7
|
export declare const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
|
|
8
8
|
export type LocalFetchErrorCode = 'ABORTED' | 'TIMEOUT' | 'INVALID_URL' | 'BLOCKED_URL' | 'DNS' | 'REDIRECT' | 'HTTP' | 'TOO_LARGE' | 'UNSUPPORTED_CONTENT_TYPE';
|
|
9
9
|
export declare class LocalFetchError extends Error {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* so a rebinding window remains (same trade-off as dsh-search-first).
|
|
5
5
|
*/
|
|
6
6
|
import { htmlToMarkdown, stripInvisibleText } from './html-markdown.js';
|
|
7
|
-
export const LOCAL_FETCH_CLIENT_VERSION = '2.9.
|
|
7
|
+
export const LOCAL_FETCH_CLIENT_VERSION = '2.9.2';
|
|
8
8
|
export const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
|
|
9
9
|
export class LocalFetchError extends Error {
|
|
10
10
|
code;
|
|
@@ -35,7 +35,7 @@ export interface WindCallResult {
|
|
|
35
35
|
code?: WindErrorCode;
|
|
36
36
|
}
|
|
37
37
|
/** Must match `package.json` version; a test pins the two together. */
|
|
38
|
-
export declare const WIND_CLIENT_VERSION = "2.9.
|
|
38
|
+
export declare const WIND_CLIENT_VERSION = "2.9.2";
|
|
39
39
|
export interface WindClient {
|
|
40
40
|
callTool(toolName: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<WindCallResult>;
|
|
41
41
|
}
|
|
@@ -19,7 +19,7 @@ export const WIND_SERVER_ENDPOINTS = {
|
|
|
19
19
|
analytics_data: 'https://mcp.wind.com.cn/vserver_analytics_data/mcp/',
|
|
20
20
|
};
|
|
21
21
|
/** Must match `package.json` version; a test pins the two together. */
|
|
22
|
-
export const WIND_CLIENT_VERSION = '2.9.
|
|
22
|
+
export const WIND_CLIENT_VERSION = '2.9.2';
|
|
23
23
|
function errorText(error) {
|
|
24
24
|
return error instanceof Error ? error.message : String(error);
|
|
25
25
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@v587d/capital-generation",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.2",
|
|
4
4
|
"description": "Capital Generation:面向中国散户的 AI 投资助手插件(自动模式 Agent Preset)",
|
|
5
5
|
"icon": "assets/cg-icon.png",
|
|
6
6
|
"author": "v587d",
|
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
"import": "./lib/ocr-entry.js",
|
|
58
58
|
"default": "./lib/ocr-entry.js"
|
|
59
59
|
},
|
|
60
|
+
"./tools/present": {
|
|
61
|
+
"types": "./lib/present-entry.d.ts",
|
|
62
|
+
"import": "./lib/present-entry.js",
|
|
63
|
+
"default": "./lib/present-entry.js"
|
|
64
|
+
},
|
|
60
65
|
"./tools/retrieve-search": {
|
|
61
66
|
"types": "./lib/search-entry.d.ts",
|
|
62
67
|
"import": "./lib/search-entry.js",
|
|
@@ -135,7 +140,8 @@
|
|
|
135
140
|
"contract:probe": "node scripts/contract-probe.mjs",
|
|
136
141
|
"docs:capabilities": "npm run build && node scripts/gen-capability-table.mjs",
|
|
137
142
|
"verify:sessions": "node scripts/verify-sessions.mjs",
|
|
138
|
-
"audit:tool-errors": "node scripts/audit-tool-errors.mjs"
|
|
143
|
+
"audit:tool-errors": "node scripts/audit-tool-errors.mjs",
|
|
144
|
+
"ptc:report": "node scripts/ptc-session-report.mjs"
|
|
139
145
|
},
|
|
140
146
|
"peerDependencies": {
|
|
141
147
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
@@ -148,15 +148,32 @@
|
|
|
148
148
|
(新工具必须先有族行带它、行契约同步);更新 `AGENTS.md`、`docs/dev/data-roles.md` 与
|
|
149
149
|
`test/persona.test.mjs` 的预留期断言。
|
|
150
150
|
|
|
151
|
-
## 5.1 shell
|
|
151
|
+
## 5.1 shell 与文件口:主 Agent 保留 shell,data_junior 用 PTC(2026-10-15 / 2026-10-23)
|
|
152
152
|
|
|
153
153
|
- **动机**:`query_dataset` 只有 `count/min/max/avg/sum`,变化量 / 增幅 / CAGR / 比值这类派生
|
|
154
154
|
指标没有宿主工具;模型只能拿 `time_facts.first/last` 心算——那正是 §1.3 那次 41,961 字符
|
|
155
|
-
时间戳心算的同型浪费。所以给 data_junior
|
|
155
|
+
时间戳心算的同型浪费。所以给 data_junior 一个程序面。**不是**给它开取数通道。
|
|
156
156
|
- **2026-10-15 起主 Agent 也持有 shell 与 `read`**(用户决策「开 read、同时保留 bash」,推翻
|
|
157
157
|
2026-09-30 的"主 Agent 不给 shell"):主 Agent 需要 `read` 读 skill 分册与工作区文件
|
|
158
158
|
(此前只能 `bash cat`,能力等价但绕远),shell 保留纯计算兜底。`ROOT_AGENT_DENIED_TOOLS`
|
|
159
159
|
里的 `bash` / `pwsh` 随之移出,名单改为出图三件套 + 十三个出网工具 + `write` / `edit`。
|
|
160
|
+
|
|
161
|
+
### 5.1.1 PTC:只给 data_junior,主 Agent 仍是 native
|
|
162
|
+
|
|
163
|
+
- **一行怎么落**:junior preset 挂 `@deepseek-ai/dsh-agent-tool-presentation`(`mode: ptc`),
|
|
164
|
+
同时删掉 `tool-bash` / `tool-pwsh`。宿主 `ptc-runtime` 由 `dsh-base` 提供,preset 写不进去;
|
|
165
|
+
`smoke:boot` 同时断言 presentation 行 `fiberState=2` 与 `ctx.get('ptcRuntime')` 在场。
|
|
166
|
+
- **为什么删 shell 而不是并存**:PTC SDK 在可见 `bash` 时会主动塞一条 `tools.bash({command:'pwd'})`
|
|
167
|
+
示例,并存等于官方提示词把模型往 shell 引;junior 只需要一个执行面。主 Agent 仍是 native,
|
|
168
|
+
拿到的是原生 shell,根侧 `restrict()` 不能点名保留名 `run_code`(只在部署级 `tools.mode`
|
|
169
|
+
被改成 `ptc` 时才会出现,本仓只允许 preset 行)。
|
|
170
|
+
- **省的是回合与抄写,不是安全升级**:`run_code` 里的 Node 仍能 `import('node:fs/promises')` /
|
|
171
|
+
`fetch` / `child_process`;宿主沙箱只拦写。`# PROGRAM DISCIPLINE` 与
|
|
172
|
+
`capital-data-query` §3.3 是纪律,不是边界;要硬隔离只能换容器 / VM / 远端 runtime。
|
|
173
|
+
- **结果体积不变**:describe 仍是 7000 码点小回执 + hint;`run_code` 外层只过宿主 pruner(8192),
|
|
174
|
+
本仓不新设第二道闸门。per-call(每次新进程)是默认,不使用 `persistent-in-memory`。
|
|
175
|
+
- **回滚**:整颗改动(composition 行 + persona + 三册 skill + guard)是一个 commit;回滚后
|
|
176
|
+
junior 回到 bash + native,恢复两行 YAML 与 `# BASH DISCIPLINE` 即可。
|
|
160
177
|
- **文件写口不开,但"不开"靠的是调用级闸门,不是行配置**:根 preset 那行 `read: true /
|
|
161
178
|
write: false / edit: false` 只决定**本层变体**——2026-10-19 实测手开 Capital 里 `write` 仍在工具表
|
|
162
179
|
中且真的落盘,缺席不等于移除(`ScopedLayers.merge` 用 global 打底,账本 L33)。所以拦下写口的只有一颗:全局 `tools.guard()`
|
|
@@ -167,25 +184,26 @@
|
|
|
167
184
|
本来就有正路(`store.writeContext` 等工具实现)。角色一侧同一条判据:本插件的六颗 preset 一律
|
|
168
185
|
不许直接写文件,原始 rows 也永不出工具层。
|
|
169
186
|
- **平台成对挂载**:Windows 上 host 层挂的是 `pwsh-sandbox`,没有 `bash-sandbox`;只挂
|
|
170
|
-
`tool-bash` 会让名为 bash 的工具实际执行 PowerShell。所以两行互补 `disabled: !!js`。
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
187
|
+
`tool-bash` 会让名为 bash 的工具实际执行 PowerShell。所以两行互补 `disabled: !!js`。
|
|
188
|
+
0.2.1 起这份 YAML 里不再出现工具名,shell 名在 `src/agents/root-tool-policy.ts` 的
|
|
189
|
+
`SHELL_TOOL_NAMES` 定义一份,`bash-guard.ts` 再并进 `run_code`;
|
|
190
|
+
`test/bash-guard.test.mjs` 钉"preset 挂的每一行 shell,闸门都必须认",
|
|
174
191
|
成对挂载与后台执行那一格仍由 `test/persona.test.mjs`「shell 行:平台成对挂载……」看着。
|
|
192
|
+
2026-10-23 起 junior 不再挂这两行,成对挂载只剩主 Agent。
|
|
175
193
|
- **不给后台执行**:`enableRunInBackground: false`。本 preset 不挂 `tool-jobs`,开着后台只会让
|
|
176
194
|
模型拿到一个永远读不回来的 jobId(本 preset 压根没挂 `tool-jobs` 那一行)。
|
|
177
|
-
-
|
|
195
|
+
- **执行面闸门只剩一层,且不是安全边界**(`src/agents/bash-guard.ts`):拦掉
|
|
178
196
|
`sandbox_permissions` / `justification`——被委派会话的审批策略由框架固定为 `never`,升级必然
|
|
179
197
|
失败,而且模型会收到「the user rejected…」这种把系统拒绝说成用户拒绝的文案;被委派或身份
|
|
180
198
|
不可考的调用方 fail closed。主 Agent 的升级走正常 ask 审批(2026-10-15 前的"只对被委派子会话
|
|
181
|
-
开放"一层随决策移除,断言迁移见 `test/bash-guard.test.mjs`)。**刻意不解析 `command
|
|
182
|
-
|
|
183
|
-
(`curl` 直连 200),而正则挡不住 `node -e` / `python3 -c
|
|
184
|
-
|
|
199
|
+
开放"一层随决策移除,断言迁移见 `test/bash-guard.test.mjs`)。**刻意不解析 `command` / `code`**:
|
|
200
|
+
本仓实测沙箱只拦写、不拦读(`~/.dsh/.credentials.yaml` 与 `sessions/**` 整机可读)、不拦网络
|
|
201
|
+
(`curl` 直连 200),而正则挡不住 `node -e` / `python3 -c` / 动态 `import()`——那正是引入程序面的
|
|
202
|
+
目的。禁区与出网纪律是 data_junior persona 的 `# PROGRAM DISCIPLINE` 软约束。
|
|
185
203
|
- **写边界收不窄到会话**:`store.writeContext()` 用**调用方 session** 的 policy 且要求
|
|
186
204
|
`mode ∈ {workspace-write, danger-full-access}`,而 `describe_dataset` 会用 data_junior 的会话写
|
|
187
|
-
`profile.json`;把它的会话钉成 `read-only` 会直接打断 describe_dataset
|
|
188
|
-
|
|
205
|
+
`profile.json`;把它的会话钉成 `read-only` 会直接打断 describe_dataset。`run_code` / bash 与
|
|
206
|
+
Dataset 管线共用同一把尺子(session policy),所以模型侧写口只能靠上一条的调用期闸门收。
|
|
189
207
|
- **要真正收紧**只有两条路:换掉 `ctx.shell`(容器 / 微 VM / 远端执行器),或给 dsh 进程做出网
|
|
190
208
|
限制。两者都不是 `ctx.sandbox` 能提供的。
|
|
191
209
|
|