@v587d/capital-generation 2.9.1 → 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 +30 -0
- package/README.md +3 -2
- package/chart-ui/README.md +3 -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/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 +52 -1
- package/lib/chart/present-tool.d.ts +8 -0
- package/lib/chart/present-tool.js +42 -21
- package/lib/data-collector/dataset-tools.js +1 -1
- package/lib/present-entry.d.ts +13 -3
- package/lib/present-entry.js +102 -1
- 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 +3 -2
- package/preset/capital-generation/README.md +31 -13
- package/preset/capital-generation/agent.patch.yml +21 -30
- 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 +4 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +5 -1
- package/watchlist/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@
|
|
|
7
7
|
- **第三位(patch)**:不破坏既有工具 / 配置 / 会话的新增与修复(如 2.1.1、2.1.2)。
|
|
8
8
|
- **第二位(minor)**:有需要用户知晓的行为变更,且段内附迁移说明(如 2.1.0 的图表呈现通道重做、2.2.0 的数据源与检索来源扩容、2.4.0 的**必须换宿主版本**、2.9.0 的选择器收敛与质检前置——本插件自己的工具与数据契约没变,所以只抬第二位)。
|
|
9
9
|
|
|
10
|
+
## [2.9.2] - 2026-10-23
|
|
11
|
+
|
|
12
|
+
`data_junior` 接入官方 PTC(`run_code`):内层工具调用压进一段程序,多 Dataset 并发、`too_large`
|
|
13
|
+
收窄与 skill 读取不再逐次往返模型。实机同类任务由 15 分钟级降到 2–3 分钟级;工具、数据契约、
|
|
14
|
+
用户配置与宿主基线都不变。图表交付的收尾闸门同时修复结算通知竞争与同轮重复写入。
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **分析师的 PTC 执行面**:`data_junior` 只保留 `run_code` 直调,Dataset / skill / 出图委派 /
|
|
19
|
+
回传都通过 SDK `tools.*` 在程序内完成;不再挂 bash / pwsh,同时保住了“不读原始行”的边界。
|
|
20
|
+
- **多 Dataset 并发与 `too_large` 自愈**:同一程序里可以 `Promise.all` 多份 `describe_dataset`;
|
|
21
|
+
结果过大时按 hint 收窄后继续,不再多开模型轮次。
|
|
22
|
+
- **PTC 会话报告**:`npm run ptc:report` 汇总 `run_code` 段数、`Promise.all`、skill 打印、
|
|
23
|
+
`too_large` 与并发窗口,供实机验收。
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **分析师提示词与三册协议同步 PTC 写法**:第一次加载协议就明确 skill 也要在 `run_code` 内用
|
|
28
|
+
`tools.skill`,不要直调原生工具;查询、profile 与可视化协议同步程序内调度。
|
|
29
|
+
- **图表交付继续由收尾流写入**:`present_charts` 只记录清单,宿主在最终答复前统一落
|
|
30
|
+
`deliverables/presented`;结算通知不再阻塞收尾,且同一 exchange 不会重复写卡。
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **图表卡在结算通知竞争时丢失**:子 Agent 的 `subagent-settled` 通知在收尾流中途到达时,
|
|
35
|
+
不再被当成“还有待处理消息”;通知同时清掉已结算的直属子 Agent。
|
|
36
|
+
- **同一轮重复交付**:后续 `finish` 帧缺少 `turn` 时不再写第二张卡。
|
|
37
|
+
- **直调 skill 误调用**:软约束已把 `tools.skill` 提到第一次加载协议处;模型仍可能试错一次,
|
|
38
|
+
宿主拒绝后会自愈,不影响结果。
|
|
39
|
+
|
|
10
40
|
## [2.9.1] - 2026-10-21
|
|
11
41
|
|
|
12
42
|
修复实机 `dsh web` 反馈的两处收尾体验:图表交付不再散落在等待轮,最终答复不再只剩一句
|
package/README.md
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
<p align="center">
|
|
19
19
|
<a href="https://awesome-dsh-plugin.com"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="awesome · DSH plugin"></a>
|
|
20
20
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-green" alt="License"></a>
|
|
21
|
-
<a href="https://github.com/v587d/capital-generation/releases"><img src="https://img.shields.io/badge/version-2.9.
|
|
21
|
+
<a href="https://github.com/v587d/capital-generation/releases"><img src="https://img.shields.io/badge/version-2.9.2-9cf" alt="Version"></a>
|
|
22
22
|
<a href="https://www.npmjs.com/package/@v587d/capital-generation"><img src="https://img.shields.io/npm/v/%40v587d%2Fcapital-generation" alt="npm version"></a>
|
|
23
|
+
<a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white" alt="powered-by-dsh"></a>
|
|
23
24
|
</p>
|
|
24
25
|
|
|
25
26
|
# Capital Generation
|
|
@@ -190,7 +191,7 @@ https://github.com/v587d/capital-generation
|
|
|
190
191
|
- **安装后找不到模式?** 确认插件出现在「插件」→ 已安装列表;Web / CLI profile 安装后要重启该 profile,并在新会话选择模式。选择器里应看到自动模式、行情专家、资讯专员;分析师和绘图专员是自动模式的内部角色,不会出现在这里。
|
|
191
192
|
- **配置在哪?** 打开「插件」→ 已安装的 **Capital Generation**,页面顶部就是「数据源 / 精选 Skills / 包含的组件」三格,五把密钥都在第一格;不在 DSH 的普通「设置」页面,也不用点进任何一行组件。保存密钥后新建会话再试。
|
|
192
193
|
- **升级到 2.9.0 后,分析师和绘图专员去哪了?** 这一版把它们收进自动模式内部,不再出现在选择器与预设管理里,避免误开一个没有取数入口的空会话。旧会话仍能按原身份恢复;新任务直接用自动模式提出即可。
|
|
193
|
-
- **桌面端升级后仍是旧版?** 桌面端安装时会固定所解析的版本,不会自动升级。卸载后重新安装,再到插件详情查看版本号;如需指定版本,可在地址末尾加 `#v2.9.
|
|
194
|
+
- **桌面端升级后仍是旧版?** 桌面端安装时会固定所解析的版本,不会自动升级。卸载后重新安装,再到插件详情查看版本号;如需指定版本,可在地址末尾加 `#v2.9.2` 或 `@2.9.2`。
|
|
194
195
|
- **刚发布的新版本装不到?** 桌面端取包时会跳过**刚发布不久**的版本(宿主侧包管理器的发布冷却策略,实测会静默装成上一个版本),国内镜像的同步也可能滞后一段时间。急着用最新代码,改用仓库地址安装最直接。
|
|
195
196
|
- **网页抓取失败?** 配置卡片中的「允许启动本地提取网页内容」默认开启:AnySearch 提取失败时可尝试本机直连。关闭后,本机直连的具名来源查询也不可用;请检查该开关和网络连接。
|
|
196
197
|
- **Wind 那部分会消耗积分?** 会。公告与新闻检索、宏观 / 行业 / 汇率指标、按日期区间取的历史 K 线都按次消耗你在 Wind 侧的额度;额度与条款以 Wind 为准,插件不做用量封顶。没配 `WIND_API_KEY` 时这几类不可用,其余功能照常。
|
package/chart-ui/README.md
CHANGED
|
@@ -90,9 +90,9 @@ route owner 自己负责,平台自己的 `/api` 与首页分别走 `connection
|
|
|
90
90
|
|
|
91
91
|
## 本轮呈现(官方交付通道)
|
|
92
92
|
|
|
93
|
-
auto 模式下 `render_chart`
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
auto 模式下 `render_chart` 不再自动登记:`present_charts` 只记录本轮图表,宿主在**收尾流结束、
|
|
94
|
+
收尾消息落盘之前**写一条**官方** `deliverables/presented` 事件到"用户正在看的根会话",把
|
|
95
|
+
`capital-analysis/charts/<chart_id>/chart.html` 登记为**收尾轮交付物**(单体载体仍在出图时
|
|
96
96
|
自动登记):
|
|
97
97
|
|
|
98
98
|
- 载荷只有官方契约字段:`turn` / `callId`(`capital-chart:<chart_id>` 句柄)/ `files[]`
|
package/chart-ui/package.json
CHANGED
package/config/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* inspected (the sandbox blocks writes, not reads/network). Delegated approvals
|
|
3
|
-
* `never`, so escalation auto-rejects with a misleading "user rejected"; block early
|
|
4
|
-
* Root escalation still uses ask; do not pin delegated sessions read-only
|
|
5
|
-
* needs workspace-write). `tools.guard` is monotonic; pre-execute is a
|
|
1
|
+
/** Execution guard for bash/pwsh/run_code: blocks only doomed sandbox escalation; command/program
|
|
2
|
+
* content is never inspected (the sandbox blocks writes, not reads/network). Delegated approvals
|
|
3
|
+
* are fixed to `never`, so escalation auto-rejects with a misleading "user rejected"; block early
|
|
4
|
+
* instead. Root escalation still uses ask; do not pin delegated sessions read-only
|
|
5
|
+
* (`store.writeContext` needs workspace-write). `tools.guard` is monotonic; pre-execute is a
|
|
6
|
+
* rewritable waterfall. */
|
|
6
7
|
import { type GuardedExecution, type PolicyContext } from './root-tool-policy.js';
|
|
8
|
+
/** Both shell names on every platform plus the PTC transport: the preset mounts one shell pair for
|
|
9
|
+
* the root and `run_code` for data_junior, so matching only the local platform kills the Windows gate. */
|
|
10
|
+
export declare const EXECUTION_ESCALATION_TOOL_NAMES: readonly ["bash", "pwsh", "run_code"];
|
|
7
11
|
export type BashGuardExecution = GuardedExecution;
|
|
8
12
|
/** States that the rejection is the system's, not the user's, and gives the recovery path. */
|
|
9
13
|
export declare const SHELL_ESCALATION_UNREACHABLE: string;
|
|
10
|
-
/** Reads identity plus `sandbox_permissions`/`justification`, never `command`; unknown identity fails closed. */
|
|
14
|
+
/** Reads identity plus `sandbox_permissions`/`justification`, never `command`/`code`; unknown identity fails closed. */
|
|
11
15
|
export declare function bashGuardReason(exec: BashGuardExecution | undefined): string | undefined;
|
|
12
16
|
/** Installs on every Capital preset agent. Must listen on `ctx.root`: agent/created routes by
|
|
13
17
|
* the agent object, so tagged standing-scope listeners never fire. Failures only warn. */
|
package/lib/agents/bash-guard.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* inspected (the sandbox blocks writes, not reads/network). Delegated approvals
|
|
3
|
-
* `never`, so escalation auto-rejects with a misleading "user rejected"; block early
|
|
4
|
-
* Root escalation still uses ask; do not pin delegated sessions read-only
|
|
5
|
-
* needs workspace-write). `tools.guard` is monotonic; pre-execute is a
|
|
1
|
+
/** Execution guard for bash/pwsh/run_code: blocks only doomed sandbox escalation; command/program
|
|
2
|
+
* content is never inspected (the sandbox blocks writes, not reads/network). Delegated approvals
|
|
3
|
+
* are fixed to `never`, so escalation auto-rejects with a misleading "user rejected"; block early
|
|
4
|
+
* instead. Root escalation still uses ask; do not pin delegated sessions read-only
|
|
5
|
+
* (`store.writeContext` needs workspace-write). `tools.guard` is monotonic; pre-execute is a
|
|
6
|
+
* rewritable waterfall. */
|
|
6
7
|
import { agentSessionId, isCapitalPresetAgent, SHELL_TOOL_NAMES } from './root-tool-policy.js';
|
|
7
|
-
/** Both names on every platform
|
|
8
|
-
|
|
8
|
+
/** Both shell names on every platform plus the PTC transport: the preset mounts one shell pair for
|
|
9
|
+
* the root and `run_code` for data_junior, so matching only the local platform kills the Windows gate. */
|
|
10
|
+
export const EXECUTION_ESCALATION_TOOL_NAMES = [...SHELL_TOOL_NAMES, 'run_code'];
|
|
11
|
+
const executionToolNames = new Set(EXECUTION_ESCALATION_TOOL_NAMES);
|
|
9
12
|
/** One listener set per root context: host policy and legacy call sites install once. */
|
|
10
13
|
const installedRoots = new WeakSet();
|
|
11
14
|
/** States that the rejection is the system's, not the user's, and gives the recovery path. */
|
|
12
|
-
export const SHELL_ESCALATION_UNREACHABLE = '
|
|
15
|
+
export const SHELL_ESCALATION_UNREACHABLE = '执行工具(bash / pwsh / run_code)的沙箱升级只有主 Agent 的会话能拿到用户审批:被委派的子会话拿不到'
|
|
13
16
|
+ '(框架把委派会话的审批策略固定为 never,不会去问用户),当前调用方也不是可确认的主 Agent 会话。'
|
|
14
17
|
+ '不要重试升级:缺数值用 describe_dataset / query_dataset,缺数据用 send_message 发 data_gap 回主 Agent。';
|
|
15
18
|
function isRecord(value) {
|
|
16
19
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
20
|
}
|
|
18
|
-
/** Reads identity plus `sandbox_permissions`/`justification`, never `command`; unknown identity fails closed. */
|
|
21
|
+
/** Reads identity plus `sandbox_permissions`/`justification`, never `command`/`code`; unknown identity fails closed. */
|
|
19
22
|
export function bashGuardReason(exec) {
|
|
20
|
-
if (exec === undefined || typeof exec.name !== 'string' || !
|
|
23
|
+
if (exec === undefined || typeof exec.name !== 'string' || !executionToolNames.has(exec.name))
|
|
21
24
|
return undefined;
|
|
22
25
|
const args = isRecord(exec.arguments) ? exec.arguments : undefined;
|
|
23
26
|
const escalated = args !== undefined && (args.sandbox_permissions !== undefined || args.justification !== undefined);
|
|
@@ -64,7 +67,7 @@ export function registerBashGuard(ctx) {
|
|
|
64
67
|
return;
|
|
65
68
|
const dispose = installAgentGuard(agent);
|
|
66
69
|
if (dispose === undefined) {
|
|
67
|
-
ctx.logger?.warn('capital-generation:
|
|
70
|
+
ctx.logger?.warn('capital-generation: 执行工具闸门未生效(agent scope 上没有 tools.guard):沙箱升级拦截只剩 persona 软约束');
|
|
68
71
|
return;
|
|
69
72
|
}
|
|
70
73
|
const id = agentSessionId(agent);
|
|
@@ -72,7 +75,7 @@ export function registerBashGuard(ctx) {
|
|
|
72
75
|
guards.set(id, dispose);
|
|
73
76
|
}
|
|
74
77
|
catch (error) {
|
|
75
|
-
ctx.logger?.warn(`capital-generation:
|
|
78
|
+
ctx.logger?.warn(`capital-generation: 执行工具闸门安装异常:${error instanceof Error ? error.message : String(error)}`);
|
|
76
79
|
}
|
|
77
80
|
});
|
|
78
81
|
const stopDisposed = listenCtx.on?.('agent/disposed', (payload) => {
|
|
@@ -47,6 +47,8 @@ export const ROOT_AGENT_DENIED_TOOLS = [
|
|
|
47
47
|
'write',
|
|
48
48
|
'edit',
|
|
49
49
|
];
|
|
50
|
+
// ⛔ `run_code` belongs nowhere in this list: it is a reserved PTC transport name, so `restrict()`
|
|
51
|
+
// throws on it. The root stays native; deployment-level `dsh-tools.mode` must not be set to ptc.
|
|
50
52
|
export const CAPITAL_PRESET_ID = ROOT_PRESET_ID;
|
|
51
53
|
export function isRootAgent(agent) {
|
|
52
54
|
if (agent?.session === undefined)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart delivery at the closing answer.
|
|
3
|
+
*
|
|
4
|
+
* DSH's deliverable card only replays `deliverables/presented` events whose seq is before the
|
|
5
|
+
* turn's closing assistant message, and a turn is final only after the fact. The one place that is
|
|
6
|
+
* provably before the closing message is the assistant stream's terminal `finish` chunk: the loop
|
|
7
|
+
* appends the message after the stream ends. So delivery is deferred to that chunk, gated on the
|
|
8
|
+
* root session being idle (no actionable inbox input, no running direct children), which is exactly
|
|
9
|
+
* the shape of a closing turn. Waiting/aborted/tool-call responses never flush.
|
|
10
|
+
*
|
|
11
|
+
* `subagent-settled` notices are not actionable input: they can arrive while the closing answer is
|
|
12
|
+
* already streaming. Counting them as pending would strand a card that the model has just selected,
|
|
13
|
+
* because the follow-up step often has nothing new to say.
|
|
14
|
+
*/
|
|
15
|
+
import type { ChartArtifactRegistry } from './artifact-ref.js';
|
|
16
|
+
import type { ChartTurnEventInput } from './events.js';
|
|
17
|
+
import type { SessionLike } from '../data-collector/store.js';
|
|
18
|
+
/** Structural subset of `@deepseek-ai/dsh-llm` AssistantStreamFrame. */
|
|
19
|
+
export interface AssistantStreamFrameLike {
|
|
20
|
+
readonly type?: string;
|
|
21
|
+
readonly attemptId?: string;
|
|
22
|
+
readonly turn?: number;
|
|
23
|
+
readonly chunk?: {
|
|
24
|
+
readonly type?: string;
|
|
25
|
+
readonly reason?: {
|
|
26
|
+
readonly kind?: string;
|
|
27
|
+
};
|
|
28
|
+
readonly blockType?: string;
|
|
29
|
+
readonly block?: {
|
|
30
|
+
readonly type?: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
interface InboxStateLike {
|
|
35
|
+
'next-turn'?: readonly unknown[];
|
|
36
|
+
'next-step'?: readonly unknown[];
|
|
37
|
+
}
|
|
38
|
+
export interface ChartDeliveryOptions {
|
|
39
|
+
artifacts: ChartArtifactRegistry;
|
|
40
|
+
/** Writes one first-party deliverables/presented batch; must be synchronous (pre-message seq). */
|
|
41
|
+
publish: (sessionId: string, inputs: ChartTurnEventInput[]) => boolean;
|
|
42
|
+
/** Durable pending input; synchronous projection read. Absent means "cannot tell" → proceed. */
|
|
43
|
+
inboxOf?: (session: SessionLike) => InboxStateLike | undefined;
|
|
44
|
+
/** Bounded batch; the card collapses past four files and expands on demand. */
|
|
45
|
+
maxCharts?: number;
|
|
46
|
+
}
|
|
47
|
+
export interface ChartDelivery {
|
|
48
|
+
/** `present_charts` selection; merged across calls because models often retry in batches. */
|
|
49
|
+
request(session: SessionLike, inputs: ChartTurnEventInput[]): void;
|
|
50
|
+
acceptStream(session: SessionLike | undefined, frame: AssistantStreamFrameLike): void;
|
|
51
|
+
/** A new human message starts a new exchange: old charts must not ride into it. */
|
|
52
|
+
acceptUserMessage(sessionId: string, time: number): void;
|
|
53
|
+
acceptSubagentStart(info: {
|
|
54
|
+
id?: string;
|
|
55
|
+
}): void;
|
|
56
|
+
acceptSubagentEnd(info: {
|
|
57
|
+
id?: string;
|
|
58
|
+
}): void;
|
|
59
|
+
/** False until any stream frame was observed: callers fall back to immediate delivery. */
|
|
60
|
+
streaming(): boolean;
|
|
61
|
+
/** Diagnostics/tests: explicitly selected inputs for the current exchange. */
|
|
62
|
+
pending(sessionId?: string): number;
|
|
63
|
+
}
|
|
64
|
+
export declare const DEFAULT_MAX_DELIVERED_CHARTS = 32;
|
|
65
|
+
export declare function createChartDelivery(options: ChartDeliveryOptions): ChartDelivery;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Settlement notices race the closing answer; they do not require another model step before the card. */
|
|
2
|
+
function isSettledNotice(value) {
|
|
3
|
+
return value !== null && typeof value === 'object'
|
|
4
|
+
&& value.source?.kind === 'subagent-settled';
|
|
5
|
+
}
|
|
6
|
+
function inboxMessages(inbox) {
|
|
7
|
+
return [...(inbox?.['next-turn'] ?? []), ...(inbox?.['next-step'] ?? [])];
|
|
8
|
+
}
|
|
9
|
+
export const DEFAULT_MAX_DELIVERED_CHARTS = 32;
|
|
10
|
+
export function createChartDelivery(options) {
|
|
11
|
+
const maxCharts = options.maxCharts ?? DEFAULT_MAX_DELIVERED_CHARTS;
|
|
12
|
+
/** Plugin instances are per root agent, so one session id guards against child streams leaking in. */
|
|
13
|
+
let sessionId;
|
|
14
|
+
let explicit = [];
|
|
15
|
+
const exchangeStarts = new Map();
|
|
16
|
+
const fallbackStart = Date.now();
|
|
17
|
+
let lastFlushedTurn;
|
|
18
|
+
let sawFrame = false;
|
|
19
|
+
const attempts = new Map();
|
|
20
|
+
const running = new Map();
|
|
21
|
+
const owns = (session) => {
|
|
22
|
+
if (session?.id === undefined)
|
|
23
|
+
return false;
|
|
24
|
+
// A leaked child frame must not bind the manager: the card belongs to the user-facing root.
|
|
25
|
+
if (session.header?.origin === 'subagent')
|
|
26
|
+
return false;
|
|
27
|
+
sessionId ??= session.id;
|
|
28
|
+
return session.id === sessionId;
|
|
29
|
+
};
|
|
30
|
+
const autoInputs = (session) => options.artifacts
|
|
31
|
+
.list({ session })
|
|
32
|
+
.filter((artifact) => artifact.created_at >= (exchangeStarts.get(session.id) ?? fallbackStart))
|
|
33
|
+
.map((artifact) => ({
|
|
34
|
+
ownerSessionId: session.id,
|
|
35
|
+
chart_id: artifact.chart_id,
|
|
36
|
+
title: artifact.title,
|
|
37
|
+
html_path: artifact.html_path,
|
|
38
|
+
}));
|
|
39
|
+
/** A settled notice proves the child is done even if the lifecycle edge was missed or the parent
|
|
40
|
+
* observed it outside its scope. Reconciliating here keeps the stream-time gate synchronous. */
|
|
41
|
+
const reconcileSettled = (inbox) => {
|
|
42
|
+
for (const message of inboxMessages(inbox)) {
|
|
43
|
+
if (!isSettledNotice(message))
|
|
44
|
+
continue;
|
|
45
|
+
const childId = message.source?.senderSessionId;
|
|
46
|
+
if (typeof childId === 'string')
|
|
47
|
+
running.delete(childId);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const idle = (inbox) => {
|
|
51
|
+
if (running.size > 0)
|
|
52
|
+
return false;
|
|
53
|
+
if (inbox === undefined)
|
|
54
|
+
return true;
|
|
55
|
+
return inboxMessages(inbox).every(isSettledNotice);
|
|
56
|
+
};
|
|
57
|
+
const flush = (session, turn) => {
|
|
58
|
+
// A later step in the same exchange may omit `turn`; it still must not write a second card.
|
|
59
|
+
if (lastFlushedTurn !== undefined && (turn === undefined || lastFlushedTurn === turn))
|
|
60
|
+
return;
|
|
61
|
+
const inbox = options.inboxOf?.(session);
|
|
62
|
+
reconcileSettled(inbox);
|
|
63
|
+
if (!idle(inbox))
|
|
64
|
+
return;
|
|
65
|
+
const inputs = (explicit.length > 0 ? explicit : autoInputs(session)).slice(0, maxCharts);
|
|
66
|
+
if (inputs.length === 0)
|
|
67
|
+
return;
|
|
68
|
+
let written = false;
|
|
69
|
+
try {
|
|
70
|
+
written = options.publish(session.id, inputs);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
written = false;
|
|
74
|
+
}
|
|
75
|
+
// Sentinel keeps a second turn-less finish in the same exchange from duplicating the card.
|
|
76
|
+
if (written)
|
|
77
|
+
lastFlushedTurn = turn ?? -1;
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
request(session, inputs) {
|
|
81
|
+
if (!owns(session) || inputs.length === 0)
|
|
82
|
+
return;
|
|
83
|
+
const seen = new Set(explicit.map((item) => item.chart_id));
|
|
84
|
+
for (const input of inputs) {
|
|
85
|
+
if (seen.has(input.chart_id))
|
|
86
|
+
continue;
|
|
87
|
+
seen.add(input.chart_id);
|
|
88
|
+
explicit.push(input);
|
|
89
|
+
}
|
|
90
|
+
if (explicit.length > maxCharts)
|
|
91
|
+
explicit = explicit.slice(0, maxCharts);
|
|
92
|
+
},
|
|
93
|
+
acceptStream(session, frame) {
|
|
94
|
+
if (!owns(session) || frame === undefined)
|
|
95
|
+
return;
|
|
96
|
+
sawFrame = true;
|
|
97
|
+
const attemptId = frame.attemptId ?? '__default__';
|
|
98
|
+
if (frame.type === 'start') {
|
|
99
|
+
attempts.set(attemptId, { toolCall: false });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (frame.type === 'end') {
|
|
103
|
+
attempts.delete(attemptId);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const chunk = frame.chunk;
|
|
107
|
+
if (frame.type !== 'chunk' || chunk === undefined)
|
|
108
|
+
return;
|
|
109
|
+
const attempt = attempts.get(attemptId) ?? { toolCall: false };
|
|
110
|
+
attempts.set(attemptId, attempt);
|
|
111
|
+
if (chunk.type === 'tool-call-delta'
|
|
112
|
+
|| (chunk.type === 'block-start' && chunk.blockType === 'tool-call')
|
|
113
|
+
|| (chunk.type === 'block-end' && chunk.block?.type === 'tool-call')) {
|
|
114
|
+
attempt.toolCall = true;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (chunk.type !== 'finish' || chunk.reason?.kind !== 'stop' || attempt.toolCall)
|
|
118
|
+
return;
|
|
119
|
+
flush(session, frame.turn);
|
|
120
|
+
},
|
|
121
|
+
acceptUserMessage(id, time) {
|
|
122
|
+
exchangeStarts.set(id, time);
|
|
123
|
+
if (id !== sessionId)
|
|
124
|
+
return;
|
|
125
|
+
explicit = [];
|
|
126
|
+
lastFlushedTurn = undefined;
|
|
127
|
+
},
|
|
128
|
+
acceptSubagentStart(info) {
|
|
129
|
+
if (typeof info?.id !== 'string' || info.id.length === 0)
|
|
130
|
+
return;
|
|
131
|
+
running.set(info.id, 1);
|
|
132
|
+
},
|
|
133
|
+
acceptSubagentEnd(info) {
|
|
134
|
+
if (typeof info?.id !== 'string')
|
|
135
|
+
return;
|
|
136
|
+
// One child session runs one activation at a time; deleting also clears duplicate starts.
|
|
137
|
+
running.delete(info.id);
|
|
138
|
+
},
|
|
139
|
+
streaming() {
|
|
140
|
+
return sawFrame;
|
|
141
|
+
},
|
|
142
|
+
pending(id) {
|
|
143
|
+
if (id !== undefined && id !== sessionId)
|
|
144
|
+
return 0;
|
|
145
|
+
return explicit.length;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
package/lib/chart/events.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface ChartTurnEventInput {
|
|
|
38
38
|
}
|
|
39
39
|
export interface ChartEventPublisher {
|
|
40
40
|
publish(input: ChartTurnEventInput): boolean;
|
|
41
|
+
/** One first-party event carrying every file; the turn cap counts events, not files. */
|
|
42
|
+
publishMany(ownerSessionId: string, inputs: ChartTurnEventInput[]): boolean;
|
|
41
43
|
/** Diagnostics: queued charts not yet written. */
|
|
42
44
|
pendingCount?(ownerSessionId?: string): number;
|
|
43
45
|
/** Diagnostics: last publish trace, surfaced in receipts / disk so failures are visible without a terminal. */
|
package/lib/chart/events.js
CHANGED
|
@@ -160,7 +160,7 @@ function buildChartEventPublisher(ctx) {
|
|
|
160
160
|
const key = `${owner.id}:${turn}`;
|
|
161
161
|
const used = perTurn.get(key) ?? 0;
|
|
162
162
|
if (used >= MAX_CHARTS_PER_TURN) {
|
|
163
|
-
warn(`图表对话流事件跳过:turn ${turn} 已达到 ${MAX_CHARTS_PER_TURN}
|
|
163
|
+
warn(`图表对话流事件跳过:turn ${turn} 已达到 ${MAX_CHARTS_PER_TURN} 次上限`);
|
|
164
164
|
return false;
|
|
165
165
|
}
|
|
166
166
|
try {
|
|
@@ -174,6 +174,31 @@ function buildChartEventPublisher(ctx) {
|
|
|
174
174
|
note(`图表已登记为本轮交付(owner=${owner.id},turn=${turn},${input.chart_id})`);
|
|
175
175
|
return true;
|
|
176
176
|
};
|
|
177
|
+
const appendMany = (owner, turn, inputs) => {
|
|
178
|
+
if (inputs.length === 0)
|
|
179
|
+
return false;
|
|
180
|
+
const key = `${owner.id}:${turn}`;
|
|
181
|
+
const used = perTurn.get(key) ?? 0;
|
|
182
|
+
if (used >= MAX_CHARTS_PER_TURN) {
|
|
183
|
+
warn(`图表对话流事件跳过:turn ${turn} 已达到 ${MAX_CHARTS_PER_TURN} 次上限`);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const first = inputs[0];
|
|
187
|
+
if (first === undefined)
|
|
188
|
+
return false;
|
|
189
|
+
const payload = buildChartEventPayload(first, turn);
|
|
190
|
+
payload.files = inputs.map((item) => ({ path: item.html_path, description: item.title }));
|
|
191
|
+
try {
|
|
192
|
+
owner.append(CHART_DELIVERABLE_EVENT, payload);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
warn(`图表对话流事件写入失败(图表已落盘):${error instanceof Error ? error.message : String(error)}`);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
perTurn.set(key, used + 1);
|
|
199
|
+
note(`图表已登记为本轮交付(owner=${owner.id},turn=${turn},${inputs.length} 张)`);
|
|
200
|
+
return true;
|
|
201
|
+
};
|
|
177
202
|
/** Flush queued charts into a turn; the preferred hook is turn-stopping. */
|
|
178
203
|
const flush = (ownerSessionId, turn) => {
|
|
179
204
|
const queued = pending.get(ownerSessionId);
|
|
@@ -308,6 +333,32 @@ function buildChartEventPublisher(ctx) {
|
|
|
308
333
|
lastPublishTrace = `${trace.join(' ')} 回合打开(turn=${lastTurn}) → ${written ? '已直接登记' : '写入失败'}`;
|
|
309
334
|
return written;
|
|
310
335
|
},
|
|
336
|
+
publishMany(ownerSessionId, inputs) {
|
|
337
|
+
const owner = resolveOwnerSession(sessions, ownerSessionId);
|
|
338
|
+
if (owner === undefined) {
|
|
339
|
+
lastPublishTrace = `owner 未解析:批量交付缺少可用 owner 会话`;
|
|
340
|
+
warn('图表对话流事件跳过:找不到批量交付的 owner 会话(图表已落盘,走 html_path 降级)');
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
const boundary = boundaryOf(owner);
|
|
344
|
+
const turn = boundary?.lastTurn;
|
|
345
|
+
if (typeof boundary?.openTurnStartSeq !== 'number' || typeof turn !== 'number' || !Number.isSafeInteger(turn) || turn < 1) {
|
|
346
|
+
// Between turns: queue each input so the existing flush path still delivers them later.
|
|
347
|
+
const queued = pending.get(owner.id) ?? [];
|
|
348
|
+
if (queued.length + inputs.length > MAX_PENDING_CHARTS) {
|
|
349
|
+
lastPublishTrace = `owner=${owner.id} 批量寄存超限(${queued.length}+${inputs.length} > ${MAX_PENDING_CHARTS})→ 未写入`;
|
|
350
|
+
warn('图表对话流事件跳过:进程内寄存已超出上限(冲刷链路异常)');
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
queued.push(...inputs);
|
|
354
|
+
pending.set(owner.id, queued);
|
|
355
|
+
lastPublishTrace = `owner=${owner.id} 回合未打开 → 已寄存 ${inputs.length} 张,等 turn-stopping/turn-start 冲刷`;
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
const written = appendMany(owner, turn, inputs);
|
|
359
|
+
lastPublishTrace = `owner=${owner.id} 回合打开(turn=${turn}) → ${written ? `已批量登记 ${inputs.length} 张` : '写入失败'}`;
|
|
360
|
+
return written;
|
|
361
|
+
},
|
|
311
362
|
/** Diagnostics/tests: queued charts not yet flushed. */
|
|
312
363
|
pendingCount(ownerSessionId) {
|
|
313
364
|
if (ownerSessionId !== undefined)
|
|
@@ -10,10 +10,18 @@
|
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
11
|
import type { ChartArtifactRegistry } from './artifact-ref.js';
|
|
12
12
|
import { type ChartEventPublisher } from './events.js';
|
|
13
|
+
import type { ChartDelivery } from './delivery.js';
|
|
14
|
+
import type { SessionLike } from '../data-collector/store.js';
|
|
15
|
+
/** One card can carry many files (the client collapses past four); the cap only bounds one event. */
|
|
16
|
+
export declare const MAX_PRESENT_CHARTS = 32;
|
|
13
17
|
export interface PresentChartsInput {
|
|
14
18
|
artifacts: ChartArtifactRegistry;
|
|
15
19
|
/** Lazy: the host projection may mount after this row; an absent publisher fails the call loudly. */
|
|
16
20
|
chartEvents: () => ChartEventPublisher | undefined;
|
|
21
|
+
/** Records the selection; the closing stream chunk writes it (normal path). */
|
|
22
|
+
delivery?: ChartDelivery;
|
|
23
|
+
/** Fallback-only guard when the stream listener is unavailable. */
|
|
24
|
+
readiness?: (session: SessionLike) => Promise<string | undefined>;
|
|
17
25
|
}
|
|
18
26
|
export interface PresentChartsReceipt {
|
|
19
27
|
presented: Array<{
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { MAX_CHARTS_PER_TURN } from './events.js';
|
|
2
2
|
import { callerSession } from '../tool-exec.js';
|
|
3
|
+
/** One card can carry many files (the client collapses past four); the cap only bounds one event. */
|
|
4
|
+
export const MAX_PRESENT_CHARTS = 32;
|
|
3
5
|
const jsonObject = (properties = {}, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
|
|
4
6
|
const parameters = jsonObject({
|
|
5
7
|
chart_refs: {
|
|
@@ -41,8 +43,8 @@ function normalizeRefs(value) {
|
|
|
41
43
|
if (!refs.includes(ref))
|
|
42
44
|
refs.push(ref);
|
|
43
45
|
}
|
|
44
|
-
if (refs.length >
|
|
45
|
-
throw new Error(`present_charts 一次最多登记 ${
|
|
46
|
+
if (refs.length > MAX_PRESENT_CHARTS) {
|
|
47
|
+
throw new Error(`present_charts 一次最多登记 ${MAX_PRESENT_CHARTS} 张图(收到 ${refs.length} 个 chart_ref)`);
|
|
46
48
|
}
|
|
47
49
|
return refs;
|
|
48
50
|
}
|
|
@@ -75,9 +77,9 @@ function selectArtifacts(input, session, refs, taskId) {
|
|
|
75
77
|
const all = input.artifacts.list({ session, task_id: taskId });
|
|
76
78
|
if (all.length === 0)
|
|
77
79
|
failed.push({ chart_ref: '(task)', reason: `本会话该 task_id 没有可登记的图表:${taskId}` });
|
|
78
|
-
const selected = all.slice(0,
|
|
79
|
-
if (all.length >
|
|
80
|
-
failed.push({ chart_ref: '(task)', reason: `该 task_id 有 ${all.length}
|
|
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} 张;其余未展示` });
|
|
81
83
|
}
|
|
82
84
|
return { selected, failed };
|
|
83
85
|
}
|
|
@@ -87,10 +89,10 @@ export function registerPresentChartsTool(ctx, input) {
|
|
|
87
89
|
return;
|
|
88
90
|
const definition = {
|
|
89
91
|
name: 'present_charts',
|
|
90
|
-
description: '把本会话已生成的图表登记为**最终答复的交付物**(官方 deliverables/presented
|
|
91
|
-
+ '
|
|
92
|
-
+ '
|
|
93
|
-
+ '
|
|
92
|
+
description: '把本会话已生成的图表登记为**最终答复的交付物**(官方 deliverables/presented):用户会在答复末尾的「本轮文件改动 / 交付」行看到卡片,点开即渲染自包含 chart.html。'
|
|
93
|
+
+ '**由主 Agent 调用一次**:参数是 data_junior 回传的 chart_refs(一次传入,最多 32 个);refs 已丢失时改传 task_id 登记该任务的全部图表。'
|
|
94
|
+
+ '这里只记录本轮要交付的图,宿主会在**最终答复之前**统一写入交付行——提前调用不会落进中间轮,不必等“最后一刻”,也不要在过程轮反复调用。'
|
|
95
|
+
+ '不要传文件路径或 HTML(路径由宿主解析);不调用时宿主仍会在收尾时自动登记本 exchange 已生成的图表。',
|
|
94
96
|
parameters,
|
|
95
97
|
async execute(args, exec) {
|
|
96
98
|
const session = callerSession(exec);
|
|
@@ -99,23 +101,42 @@ export function registerPresentChartsTool(ctx, input) {
|
|
|
99
101
|
if (refs.length === 0 && taskId === undefined) {
|
|
100
102
|
throw new Error('present_charts 需要 chart_refs 或 task_id:两者都不给就无法确定要登记哪几张图');
|
|
101
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);
|
|
102
125
|
const publisher = input.chartEvents();
|
|
103
126
|
if (publisher === undefined) {
|
|
104
127
|
throw new Error('图表交付通道不可用(sessions / sessionProjections 解析不到):图表已落盘,可在 workspace 打开 chart.html');
|
|
105
128
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
chart_id: artifact.chart_id,
|
|
112
|
-
title: artifact.title,
|
|
113
|
-
html_path: artifact.html_path,
|
|
114
|
-
});
|
|
115
|
-
if (accepted)
|
|
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)
|
|
116
134
|
receipt.presented.push({ chart_ref: artifact.chart_ref, title: artifact.title });
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
for (const artifact of selected) {
|
|
138
|
+
receipt.failed.push({ chart_ref: artifact.chart_ref, reason: `本轮登记已达 ${MAX_CHARTS_PER_TURN} 次上限或回合已关闭,未写入交付行` });
|
|
139
|
+
}
|
|
119
140
|
}
|
|
120
141
|
return receipt;
|
|
121
142
|
},
|
|
@@ -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),
|
package/lib/present-entry.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `tools/present` row: present_charts
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* `tools/present` row: present_charts + the closing-answer delivery listener.
|
|
3
|
+
*
|
|
4
|
+
* The model only records which charts belong to the answer. The actual first-party
|
|
5
|
+
* `deliverables/presented` append happens in the terminal assistant-stream chunk, before the closing
|
|
6
|
+
* message commits, gated on an idle root (no actionable inbox input, no running direct children). That is the only
|
|
7
|
+
* point where "the final answer" is knowable, so early/no `present_charts` calls cannot strand the
|
|
8
|
+
* card in a folded process turn (rationale: docs/dev/chart-delivery-events.md §6.3).
|
|
5
9
|
*/
|
|
6
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
import type { SessionLike } from './data-collector/store.js';
|
|
7
12
|
export declare const name = "capital-present-tools";
|
|
8
13
|
export declare const inject: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Fallback only: when the stream listener never fires, immediate delivery still has to avoid a
|
|
16
|
+
* pending turn. The normal path defers to the closing stream chunk and does not consult this.
|
|
17
|
+
*/
|
|
18
|
+
export declare function chartDeliveryBlocker(ctx: Context, session: SessionLike): Promise<string | undefined>;
|
|
9
19
|
export declare function apply(ctx: Context): void;
|
package/lib/present-entry.js
CHANGED
|
@@ -1,13 +1,114 @@
|
|
|
1
|
+
import { createChartDelivery } from './chart/delivery.js';
|
|
1
2
|
import { createChartEventPublisher } from './chart/events.js';
|
|
2
3
|
import { chartEventContextOf } from './chart/event-context.js';
|
|
3
4
|
import { registerPresentChartsTool } from './chart/present-tool.js';
|
|
4
5
|
import { capitalSharedRuntimeOf } from './chart-source-entry.js';
|
|
5
6
|
export const name = 'capital-present-tools';
|
|
6
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
|
+
}
|
|
7
58
|
export function apply(ctx) {
|
|
8
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()');
|
|
9
108
|
registerPresentChartsTool(ctx, {
|
|
10
109
|
artifacts: shared.artifacts,
|
|
11
|
-
chartEvents:
|
|
110
|
+
chartEvents: publisherOf,
|
|
111
|
+
readiness: (session) => chartDeliveryBlocker(ctx, session),
|
|
112
|
+
delivery,
|
|
12
113
|
});
|
|
13
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",
|
|
@@ -140,7 +140,8 @@
|
|
|
140
140
|
"contract:probe": "node scripts/contract-probe.mjs",
|
|
141
141
|
"docs:capabilities": "npm run build && node scripts/gen-capability-table.mjs",
|
|
142
142
|
"verify:sessions": "node scripts/verify-sessions.mjs",
|
|
143
|
-
"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"
|
|
144
145
|
},
|
|
145
146
|
"peerDependencies": {
|
|
146
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
|
|
|
@@ -133,24 +133,24 @@
|
|
|
133
133
|
(不做投资判断、不输出买卖建议),也不改写原始 Dataset。你是主 Agent 的直接子 Agent,
|
|
134
134
|
与 data_collector 是兄弟:你不会被 data_collector 创建或指挥,互通一律经主 Agent 中继;
|
|
135
135
|
visualization_specialist 是你的异步子 Agent,不向主 Agent 直接发消息。
|
|
136
|
-
|
|
136
|
+
工具面没有外部行情 API、网页检索与凭据入口;run_code 里的 Node 也不得用于取数、联网或读凭据;宿主若暴露 web_search / web_fetch 也不得用于取数或查资料。不索取或保存账户密码。
|
|
137
137
|
|
|
138
138
|
# DUTIES
|
|
139
|
-
0.
|
|
140
|
-
|
|
141
|
-
(describe_dataset / profile_dataset)后无条件加载 skill capital-visualization-protocol,
|
|
139
|
+
0. 第一步先加载协议:Dataset 工具前先 skill capital-data-profile(必须 tools.skill,别直调);写 query_request
|
|
140
|
+
或做算术再加 skill capital-data-query;描述后无条件加载 skill capital-visualization-protocol,
|
|
142
141
|
再选择 gate。载荷样例、事实读法、data_gap 与 QuerySpec 细则都在 skill 里。
|
|
143
|
-
1.
|
|
142
|
+
1. 启动自检:先看 SDK 有无 tools.describe_dataset,没有就 send_message 回告;正常时登记 data_junior_ready。
|
|
144
143
|
|
|
145
144
|
2. 只用受控工具:dataset_ids 按主 Agent 转发的原样使用;一切路径都是 opaque
|
|
146
|
-
artifact_ref(workspace
|
|
147
|
-
|
|
145
|
+
artifact_ref(workspace://...),由受控工具解析;Dataset 工具只收 dataset_id 与 QuerySpec,
|
|
146
|
+
run_code 只调度工具与算术,不得读文件。
|
|
147
|
+
3. 默认用 describe_dataset 读 Dataset(多份在同一 run_code 程序里 Promise.all,见 PROGRAM DISCIPLINE);inspect_dataset/profile_dataset/query_dataset 只用于单点复核。优先用 profile 的 time_facts 与 structure;query_dataset 只做受控聚合,你取不到原始行。
|
|
148
148
|
|
|
149
149
|
4. 数据缺口:先分清「数据里确实没有」与「那份数据还没取」。后者不要写成「无法确认」,
|
|
150
150
|
立即用 send_message 向父 Agent 发一次 data_gap(task_id / dataset_id / need / reason /
|
|
151
151
|
blocking),同一条缺口只发一次,这是请求不是结论。你不得自行取数、也不得直接找
|
|
152
152
|
data_collector(取数由主 Agent 转交);不要为了填空编造能力名。
|
|
153
|
-
5.
|
|
153
|
+
5. 回传:用 tools.send_message 逐份回传结构化载荷(type / task_id / dataset_id / profile_ref
|
|
154
154
|
或 query_result),字段按工具返回原样填入——字段名、数值、日期一律不改写;可附表或
|
|
155
155
|
摘要,禁止只回传 markdown 汇总。失败回传 profile_failed / query_failed 与 error/code,
|
|
156
156
|
绝不编造统计或查询结果;失败不影响原始 Dataset,可按主 Agent 指示重试。
|
|
@@ -163,12 +163,11 @@
|
|
|
163
163
|
任何时间窗/时间戳换算一律交给宿主:时间窗优先 resolve_data_time_range(传 dataset_id + period),
|
|
164
164
|
拿 bounds 填 filter;绝不自行把日期算成毫秒,看到 epoch 要加减乘除才能得出结论,就是走错了路。
|
|
165
165
|
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
输入只敲标量(≤20 个)、逐数核对再算,不整行复制。
|
|
166
|
+
# PROGRAM DISCIPLINE
|
|
167
|
+
调用一律写进 run_code;skill 也走 tools.skill,console.log(s.content),只 await 等于没读。
|
|
168
|
+
字段直接取返回值,不手抄;变化量/增幅/CAGR 在程序里算。
|
|
169
|
+
多份 describe 用 Promise.all,只返回结论;回传用 tools.send_message,不用原生名。
|
|
170
|
+
不读文件、不碰 capital-data、不 fetch、不申请升级(sandbox_permissions);缺数据发 data_gap。
|
|
172
171
|
|
|
173
172
|
- id: agent-instructions
|
|
174
173
|
name: '@deepseek-ai/dsh-agent-instructions'
|
|
@@ -185,6 +184,12 @@
|
|
|
185
184
|
- id: tool-skill
|
|
186
185
|
name: '@deepseek-ai/dsh-tool-skill'
|
|
187
186
|
|
|
187
|
+
# PTC: presentation is preset-scoped; the host runtime (dsh-base) stays per-call.
|
|
188
|
+
- id: tool-presentation
|
|
189
|
+
name: '@deepseek-ai/dsh-agent-tool-presentation'
|
|
190
|
+
config:
|
|
191
|
+
mode: ptc
|
|
192
|
+
|
|
188
193
|
# ── compaction: both rows must live in one realm; thresholdChars 8192 is the instance-wide single value ──
|
|
189
194
|
- id: compaction
|
|
190
195
|
name: cordis:group
|
|
@@ -206,19 +211,6 @@
|
|
|
206
211
|
headChars: 4096
|
|
207
212
|
tailChars: 1024
|
|
208
213
|
|
|
209
|
-
- id: tool-bash
|
|
210
|
-
name: '@deepseek-ai/dsh-tool-bash'
|
|
211
|
-
disabled: !!js process.platform === 'win32'
|
|
212
|
-
config:
|
|
213
|
-
# No tool-jobs row here: a background run would hand the model a jobId it can never read back.
|
|
214
|
-
enableRunInBackground: false
|
|
215
|
-
|
|
216
|
-
- id: tool-pwsh
|
|
217
|
-
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
218
|
-
disabled: !!js process.platform !== 'win32'
|
|
219
|
-
config:
|
|
220
|
-
enableRunInBackground: false
|
|
221
|
-
|
|
222
214
|
- id: tool-subagent-control
|
|
223
215
|
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
224
216
|
|
|
@@ -511,7 +503,7 @@
|
|
|
511
503
|
# OUTPUT CONVENTIONS
|
|
512
504
|
关键指标与截止用表格;子 Agent 回传按原样引用。
|
|
513
505
|
图表由 data_junior 的可视化 gate 统一出品:你不画图、不评价图;派的时机与收窄见下方「数据调度纪律」。
|
|
514
|
-
|
|
506
|
+
图表交付:收到 chart_refs 后调用一次 present_charts(一次传全部 ref;refs 丢了就传 task_id)——它只记录清单,宿主会在最终答复前写入答复末尾的「交付」行,提前调用不会落进中间轮。正文不要提图表、不在正文里罗列图表文件 / 路径 / HTML。图表是呈现不是分析:不用图推断数字,数值结论一律以 profile / query 回传为准。
|
|
515
507
|
最终答(硬):必须自足——结论、关键证据与来源标注、局限与风险、观察 / 验证 / 防守选项;归因准确。**禁止“分析已完成 / 结论见上 / 如上”这类指针式收尾**:界面只展示最后一轮答复、前面轮次会被折叠,指针句会让用户什么也看不到。被通知唤醒且无新增信息时:不输出可见正文(让上一条完整结论成为最后一轮),或重发一份自足总结;绝不写指针句。
|
|
516
508
|
|
|
517
509
|
# RULES & DISCIPLINE
|
|
@@ -608,7 +600,7 @@
|
|
|
608
600
|
不得冒充行情证据或补位缺失能力。不支持的频率如实披露,用已有粒度降级,不编造。
|
|
609
601
|
|
|
610
602
|
|
|
611
|
-
# Shell rows: bash/pwsh pair per platform,
|
|
603
|
+
# Shell rows: bash/pwsh pair per platform, main Agent only (junior uses PTC `run_code`); README §5.1.
|
|
612
604
|
- id: tool-bash
|
|
613
605
|
name: '@deepseek-ai/dsh-tool-bash'
|
|
614
606
|
disabled: !!js process.platform === 'win32'
|
|
@@ -642,7 +634,6 @@
|
|
|
642
634
|
provider: spawn
|
|
643
635
|
toolName: subagent_data_junior
|
|
644
636
|
agentPreset: capital-data-junior
|
|
645
|
-
# Platform-selected shell name; must match the shell row above (restrict() fails on unknown names).
|
|
646
637
|
- id: tool-subagent-web-retriever
|
|
647
638
|
name: '@deepseek-ai/dsh-tool-subagent'
|
|
648
639
|
config:
|
|
@@ -15,6 +15,7 @@ description: Use when reading a Dataset back — describe_dataset as the default
|
|
|
15
15
|
|
|
16
16
|
`data_junior` 读一份 Dataset 用 **`describe_dataset`**(一次调用完成 inspect + profile + 可选 queries),
|
|
17
17
|
**不要再走 `inspect_dataset → profile_dataset → query_dataset` 三步**——那三步只保留给单点复核。
|
|
18
|
+
`run_code` 与 `Promise.all` 的执行方式只对 data_junior 生效;主 Agent 只读事实块与回传格式。
|
|
18
19
|
|
|
19
20
|
```json
|
|
20
21
|
{
|
|
@@ -27,8 +28,8 @@ description: Use when reading a Dataset back — describe_dataset as the default
|
|
|
27
28
|
}
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
- **一次一份**:一次调用只传一个 `dataset_id
|
|
31
|
-
|
|
31
|
+
- **一次一份**:一次调用只传一个 `dataset_id`。本轮有多份数据时,在同一个 `run_code` 程序里用
|
|
32
|
+
`Promise.all` 并发 describe;结果留在程序里,只把结论 `return`。不要一份一份等。
|
|
32
33
|
- 返回 = inspect 元数据(`artifact_ref` / `capability` / `source_label` / `captured_at` / `retention_until` /
|
|
33
34
|
`query_access.shape`)+ profile 四类事实(`statistics` / `categories` / `time_facts` / `structure`)+
|
|
34
35
|
`profile_ref` + 每条 query 的 `result`,或该条自己的 `error`(单条失败不影响其余)。
|
|
@@ -36,8 +37,9 @@ description: Use when reading a Dataset back — describe_dataset as the default
|
|
|
36
37
|
- `columns_of_interest` 只对这些列返回 `statistics` / `categories` / `schema`(并同样收窄 `time_facts` 的首末值投影;时间列永远保留),是收窄结果体积的唯一手段。
|
|
37
38
|
- **体积闸门**:结果超过 7000 码点时返回
|
|
38
39
|
`{"status":"too_large","profile_ref":"...","columns":[...],"hint":"..."}`,而不是超长载荷。
|
|
39
|
-
|
|
40
|
-
`profile_dataset`。**看到 `too_large`
|
|
40
|
+
在同一个程序里按 hint 用 `columns_of_interest`(或减少 `queries`,一次最多 8 条)重发一次;文档型按
|
|
41
|
+
hint 直接调 `profile_dataset`。**看到 `too_large` 不是失败**——它带着完整列名,用来让你收窄;
|
|
42
|
+
不要把它当异常、也不要另起一轮。
|
|
41
43
|
- 单点复核(只看元数据 / 只要一份完整 profile / 补一条查询)仍可用 `inspect_dataset`、`profile_dataset`、
|
|
42
44
|
`query_dataset`。
|
|
43
45
|
|
|
@@ -153,6 +155,9 @@ data_junior 发现「问题需要的那份数据还没取」时(不是「数
|
|
|
153
155
|
}
|
|
154
156
|
```
|
|
155
157
|
|
|
158
|
+
PTC 下发送是 `await tools.send_message({ agent_id: PARENT, message: JSON.stringify(payload) })`;
|
|
159
|
+
payload 的 JSON 形状不变,父 Agent id 仍来自系统提示。
|
|
160
|
+
|
|
156
161
|
| 字段 | 规则 |
|
|
157
162
|
|------|------|
|
|
158
163
|
| `type` | 固定 `data_gap` |
|
|
@@ -1,46 +1,61 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: capital-data-query
|
|
3
|
-
description: Use when aggregating a Dataset or computing a derived number — the controlled QuerySpec (filters, group_by, aggregates, calendar time values, error codes) and data_junior's
|
|
3
|
+
description: Use when aggregating a Dataset or computing a derived number — the controlled QuerySpec (filters, group_by, aggregates, calendar time values, error codes) and data_junior's run_code programs for derived arithmetic. Volume 3 of the Capital data protocol; payloads in capital-data-protocol, profile facts and the time axis in capital-data-profile.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Capital 数据协议 · 第三册:受控查询与派生算术
|
|
7
7
|
|
|
8
|
-
本册是数据协议的第三册(§3.3
|
|
8
|
+
本册是数据协议的第三册(§3.3 run_code 只做派生算术 / §4 受控查询)。时间轴与 `time_facts` 在第二册
|
|
9
9
|
`capital-data-profile` §3.2,载荷契约与重试纪律在第一册 `capital-data-protocol`。
|
|
10
10
|
|
|
11
|
-
## 3.3
|
|
11
|
+
## 3.3 run_code:派生算术,不是第二条取数通道
|
|
12
12
|
|
|
13
|
-
只有 data_junior 有 `
|
|
14
|
-
`count / min / max / avg / sum`——**变化量、增幅、CAGR
|
|
15
|
-
|
|
13
|
+
只有 data_junior 有 `run_code`;主 Agent 读本节是为了理解回传格式,不要照着执行。它存在的唯一
|
|
14
|
+
理由是 `query_dataset` 的聚合只有 `count / min / max / avg / sum`——**变化量、增幅、CAGR、比值、
|
|
15
|
+
单位换算**这类派生指标没有宿主工具。数字必须从工具返回值直接取,不手抄、不整行复制、不中转到
|
|
16
|
+
别的语言。
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
**可以做的**(值来自工具返回对象,算完只返回结论)。第一段程序只读协议,`content` 必须打印出来,
|
|
19
|
+
只 `await` 等于没读;也不要和 describe 混在同一段:
|
|
18
20
|
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
```ts
|
|
22
|
+
const s = await tools.skill({ name: 'capital-data-profile' })
|
|
23
|
+
console.log(s.content)
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
```ts
|
|
27
|
+
const r = await tools.describe_dataset({ dataset_id, task_id })
|
|
28
|
+
const a = r.time_facts.first.close_price
|
|
29
|
+
const b = r.time_facts.last.close_price
|
|
30
|
+
return { pct: ((b - a) / a) * 100, from: a, to: b }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- `describe_dataset` 返回 `{status:'too_large', columns, hint}` 时,它在同一个程序里按 hint 用
|
|
34
|
+
`columns_of_interest` 重发一次;这是返回值不是异常,别用 `catch` 接、也别另起一轮。
|
|
35
|
+
- 多份 Dataset 用 `Promise.all` 并发 describe(只有 `describe_dataset` 声明了并发安全);
|
|
36
|
+
`query_dataset` 串行,不要混进同一批。某份可能失败时用 `Promise.allSettled`。
|
|
37
|
+
- 一次程序只做一段事:先 `console.log(s.content)` 读协议,再 describe/算数,最后回传;
|
|
38
|
+
不要把整份 profile 或多个工具结果 `return` 回去(外层结果会过 8192 剪枝)。
|
|
39
|
+
- 回传与 `data_gap` 用 `await tools.send_message({ agent_id, message })`;它返回 `{messageId}`,
|
|
40
|
+
不结束本轮。`agent_id` 取系统提示里的 parent agent id。
|
|
27
41
|
|
|
28
42
|
**不许做的**(这是纪律,不是沙箱限制——沙箱只拦写,不拦读与网络):
|
|
29
43
|
|
|
30
44
|
| 不许 | 为什么 | 正确做法 |
|
|
31
45
|
|---|---|---|
|
|
32
|
-
| 读 `capital-data/**`、`raw.json` |
|
|
46
|
+
| 读 `capital-data/**`、`raw.json` | 原始行**永不**进入你的上下文;读了也算错 | 数值走 `describe_dataset` / `query_dataset` 的统计与聚合 |
|
|
33
47
|
| 读 `~/.dsh/**`、`sessions/**` | 那是宿主凭据与全部会话日志 | 不碰;需要元数据用工具回执 |
|
|
34
48
|
| 读 / 改 `preset/`、`src/`、`test/` | 那是装配与门禁本身 | 不碰 |
|
|
35
|
-
| `
|
|
36
|
-
| 申请更高权限(`sandbox_permissions`) |
|
|
49
|
+
| `fetch` / 任何网络请求、`git clone`、`npm i`、`pip install` | 你的角色没有网络职责,外部材料只经主 Agent 转交 `web_retriever` | 缺外部事实就在回传里写明缺口 |
|
|
50
|
+
| 申请更高权限(`sandbox_permissions`) | 委派会话的审批固定为 `never`,必然失败 | 不要试;缺数据发 `data_gap` |
|
|
37
51
|
|
|
38
52
|
另外两条:
|
|
39
53
|
|
|
40
|
-
-
|
|
41
|
-
|
|
54
|
+
- **输出只留结论数字**:`run_code` 的外层结果同样会过剪枝器(>8192 字符中间段被剪掉),把中间
|
|
55
|
+
过程或整份 profile 打印出来只会挤掉真正要看的东西;回传时注明算法与口径(如「按首末收盘价、
|
|
56
|
+
未复权」)。只返回结论,不要 `return` 整份 profile。
|
|
42
57
|
- **时间窗仍归宿主**:窗口边界一律用 `time_facts.windows` / `resolve_data_time_range`
|
|
43
|
-
(`capital-data-profile` §3.2
|
|
58
|
+
(`capital-data-profile` §3.2),不要在程序里把日期算成毫秒——口径错了数字就全错。
|
|
44
59
|
|
|
45
60
|
`profile_dataset` 的统计项:数值字段的 count 与 sum(合计多少、覆盖多少条)、字符串字段的
|
|
46
61
|
`distinct_count` 与高频取值、字段类型(observed / contract)、缺失值与显式 null、类型冲突、
|
|
@@ -44,4 +44,7 @@ description: Use when the user requests a complete research report or comprehens
|
|
|
44
44
|
|
|
45
45
|
每个会影响判断的数值注明能力/来源、报告期或交易日及采集时间;材料结论给可追溯的原文链接,或文件标题、发布主体与页码(无法提供原文时明说)。不要用已实施分红或历史价格推断股价下限,不把同业倍数排序写成“便宜/贵”的充分证据。对缺失、冲突和数据延迟给出它们如何改变判断的解释;未评估清单仅列与本轮问题或结论有关的缺口。区分投资者可采取的验证步骤与交易指令;不承诺收益、不输出没有可复现模型支持的目标价、概率或胜率。
|
|
46
46
|
|
|
47
|
-
**这一段就是最后一轮的唯一可见内容**:写之前先调用一次 `present_charts`(有 chart_refs
|
|
47
|
+
**这一段就是最后一轮的唯一可见内容**:写之前先调用一次 `present_charts`(有 chart_refs 时;refs 记不清就传 task_id 一次登记全部),报告正文不罗列图表文件,也不从图上反推数字;写完不要再补一行“已完成/结论见上”把报告盖掉。
|
|
48
|
+
|
|
49
|
+
`present_charts` 只记录本轮图表清单,宿主会在**最终答复之前**统一写入交付行;提前调用也安全,
|
|
50
|
+
不必留到最后一刻。调用后继续把取证做完也可以——最终答复仍由宿主在收尾时附上图表。
|
|
@@ -122,7 +122,8 @@ ohlc
|
|
|
122
122
|
prepare_chart_source({ dataset_id, task_id })
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
data_junior 在 `run_code` 里调用 `tools.prepare_chart_source(...)`;specialist 是 native,
|
|
126
|
+
照旧直调 `render_chart`,不经过 `run_code`。只使用工具返回的 `chart_source_ref`。不要自己构造 token、路径或 `raw.json` 地址。
|
|
126
127
|
|
|
127
128
|
**token 有 15 分钟 TTL,同一 task_id + dataset_id 重复签发幂等返回同一个 token**:仍建议先定好
|
|
128
129
|
要出哪几张图、每张用哪份 Dataset,再一次性签发(省往返);万一改了视图方案再签一次也无妨——
|
|
@@ -196,6 +197,9 @@ prepare_chart_source({ dataset_id, task_id })
|
|
|
196
197
|
|
|
197
198
|
## 4. 调用 render_chart
|
|
198
199
|
|
|
200
|
+
data_junior 只在 `run_code` 程序里用 `tools.subagent_visualization_specialist(...)` 创建 specialist;
|
|
201
|
+
specialist 是 native,照旧直调 `render_chart`。以下步骤属于 specialist。
|
|
202
|
+
|
|
199
203
|
第一次调用前加载:
|
|
200
204
|
|
|
201
205
|
```text
|
package/watchlist/package.json
CHANGED