@v587d/capital-generation 2.9.2 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +5 -3
- package/chart-ui/package.json +1 -1
- package/config/package.json +1 -1
- package/lib/agents/settled-wake.d.ts +43 -0
- package/lib/agents/settled-wake.js +81 -0
- package/lib/agents/wait-for-agents.d.ts +79 -0
- package/lib/agents/wait-for-agents.js +256 -0
- package/lib/chart/artifact-ref.d.ts +4 -1
- package/lib/chart/artifact-ref.js +2 -1
- package/lib/chart/delivery.d.ts +15 -1
- package/lib/chart/delivery.js +17 -8
- package/lib/chart/events.d.ts +1 -1
- package/lib/chart/html.js +16 -0
- package/lib/chart/present-tool.js +52 -3
- package/lib/chart/tool.js +63 -9
- package/lib/data-collector/store.d.ts +1 -0
- package/lib/data-collector/store.js +2 -2
- package/lib/orchestration-entry.d.ts +11 -0
- package/lib/orchestration-entry.js +6 -0
- package/lib/present-entry.js +26 -5
- 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 +6 -1
- package/preset/capital-generation/README.md +3 -3
- package/preset/capital-generation/agent.patch.yml +9 -4
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +3 -2
- package/preset/capital-generation/skills/capital-data-query/SKILL.md +13 -0
- package/preset/capital-generation/skills/capital-equity-research/SKILL.md +1 -0
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +7 -1
- package/watchlist/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,49 @@
|
|
|
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.10.0] - 2026-10-23
|
|
11
|
+
|
|
12
|
+
自动模式收口为**单轮等待 + 一次交付**:主 Agent 委派后等整棵子树收敛,再把结论和图表一次交到最后
|
|
13
|
+
一轮;同时补上图表交付去重/标题校验、可读文件名,以及数据窗口、成交量单位与财务口径的硬规则。
|
|
14
|
+
本插件自己的工具与数据契约不变,不需要改配置。
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **自动模式单轮等待**:主 Agent 新增 `wait_for_agents`(仅根会话)。委派或发消息后先等整棵子 Agent
|
|
19
|
+
树收敛,子角色回传在同一轮内处理,不再每条回执都单独落成一个可见轮次。
|
|
20
|
+
- **四态回执**:`all_settled` / `message` / `user_input` / `timeout`;`timeout` 会列出仍在运行的
|
|
21
|
+
子 Agent,既不是完成也不是失败。
|
|
22
|
+
- **图表交付质量闸门**:`present_charts` 登记前按 `dataset + kind + 标题` 去重,并校验标题里的
|
|
23
|
+
6 位股票代码与 dataset 一致;重复图/错标题图进 `failed`,不再进入交付卡片。
|
|
24
|
+
- **可读图表产物与读图指引**:新图文件名带股票代码/ASCII 标题词与图形类型(如
|
|
25
|
+
`300803-kline-ab12cd34.html`),HTML 顶部补图形类型、来源、时间字段和横轴/纵轴/序列/K 线说明。
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **等待不再以可见等待行结束回合**:主 Agent 先调用 `wait_for_agents`;`message` / `user_input`
|
|
30
|
+
先处理后继续等,`all_settled` 才进入逐行对账,`timeout` 才写等待行。等待期间仍禁止外部检索、
|
|
31
|
+
反复 `list_agents` 与轮询。
|
|
32
|
+
- **单轮等待只归根会话**:`tools/orchestration` 只挂在根 preset;子 Agent 继续用 `send_message`
|
|
33
|
+
回传,不能等待兄弟或父 Agent。
|
|
34
|
+
- **派生统计算子收紧**:`N 日` 一律按最近 N 个交易日计算;MA20/10 日高不再用自然日窗口,成交量
|
|
35
|
+
明确按股展示,量比注明分母是否含当日。
|
|
36
|
+
- **财务与估值口径对齐**:`operating_income` 与媒体“营业总收入”、H1 累计与 Q2 单季、归母与
|
|
37
|
+
净利润、结构化扣非与媒体扣非分开标注;PE/PB/PS/PCF 并列,倍数分化时写“估值分化”。
|
|
38
|
+
- **Specialist 调度收紧**:单次最多 8 个并发,使用 `Promise.allSettled`,按
|
|
39
|
+
`dataset_id + chart_source_ref` 记录成功并只重试失败项,禁止重发已成功图表。
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **MA20/10 日高窗口错误**:修复把 12 个交易日当 MA20、8 个交易日当 10 日高的问题;同类问题
|
|
44
|
+
在 data_junior 的派生统计与主 Agent 的结论复核中都会被协议拦下。
|
|
45
|
+
- **成交量单位误写**:A 股 `history.volume` 原值为股,不再写成“万手”。
|
|
46
|
+
- **H1/单季与扣非混用**:不再把媒体 H1 营业总收入与结构化 Q2 `operating_income` 放在同一增速
|
|
47
|
+
对比,也不在“扣非缺失”的同时引用媒体扣非。
|
|
48
|
+
- **重复 K 线与错标题图**:`present_charts` 不再接受同 dataset/kind/标题的重复图,也不会把
|
|
49
|
+
“同花顺 300803”这类标题登记进交付卡片。
|
|
50
|
+
- **Specialist 并发失败吞任务**:`Promise.all` 遇到并发上限错误时不再吞掉整批,失败任务可被
|
|
51
|
+
单独重试;已成功的 K 线不再被重复创建。
|
|
52
|
+
|
|
10
53
|
## [2.9.2] - 2026-10-23
|
|
11
54
|
|
|
12
55
|
`data_junior` 接入官方 PTC(`run_code`):内层工具调用压进一段程序,多 Dataset 并发、`too_large`
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
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.
|
|
21
|
+
<a href="https://github.com/v587d/capital-generation/releases"><img src="https://img.shields.io/badge/version-2.10.0-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
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>
|
|
24
24
|
</p>
|
|
@@ -115,11 +115,13 @@ https://github.com/v587d/capital-generation
|
|
|
115
115
|
|
|
116
116
|
**每份结构化数据都会先做质检。** 无论你有没有提图表,自动模式收到行情或财务数据后都会先交给分析师检查结构、时间轴、缺失与基础统计;要不要图是检查之后的一个判断。明确说“不要图”时,质检照做,只是停在出图之前。
|
|
117
117
|
|
|
118
|
+
自动模式会把子角色工作期间的中间回执留在同一轮里处理,等材料收齐后再给出完整结论,而不是每条中间进展都单独回一段。
|
|
119
|
+
|
|
118
120
|
## 试试这么用
|
|
119
121
|
|
|
120
122
|
在新的自动模式会话中使用以下指令。将方括号内容换成自己的标的、公告或持仓;证券代码写完整市场后缀,例如 `600519.SH`。每条指令单独发送,便于观察结果。
|
|
121
123
|
|
|
122
|
-
[
|
|
124
|
+
[样例:指南针、同花顺复盘分析报告(20261023)](docs/sample/指南针-同花顺-复盘分析报告.md)
|
|
123
125
|
|
|
124
126
|
**完整投资研究报告**
|
|
125
127
|
|
|
@@ -191,7 +193,7 @@ https://github.com/v587d/capital-generation
|
|
|
191
193
|
- **安装后找不到模式?** 确认插件出现在「插件」→ 已安装列表;Web / CLI profile 安装后要重启该 profile,并在新会话选择模式。选择器里应看到自动模式、行情专家、资讯专员;分析师和绘图专员是自动模式的内部角色,不会出现在这里。
|
|
192
194
|
- **配置在哪?** 打开「插件」→ 已安装的 **Capital Generation**,页面顶部就是「数据源 / 精选 Skills / 包含的组件」三格,五把密钥都在第一格;不在 DSH 的普通「设置」页面,也不用点进任何一行组件。保存密钥后新建会话再试。
|
|
193
195
|
- **升级到 2.9.0 后,分析师和绘图专员去哪了?** 这一版把它们收进自动模式内部,不再出现在选择器与预设管理里,避免误开一个没有取数入口的空会话。旧会话仍能按原身份恢复;新任务直接用自动模式提出即可。
|
|
194
|
-
- **桌面端升级后仍是旧版?** 桌面端安装时会固定所解析的版本,不会自动升级。卸载后重新安装,再到插件详情查看版本号;如需指定版本,可在地址末尾加 `#v2.
|
|
196
|
+
- **桌面端升级后仍是旧版?** 桌面端安装时会固定所解析的版本,不会自动升级。卸载后重新安装,再到插件详情查看版本号;如需指定版本,可在地址末尾加 `#v2.10.0` 或 `@2.10.0`。
|
|
195
197
|
- **刚发布的新版本装不到?** 桌面端取包时会跳过**刚发布不久**的版本(宿主侧包管理器的发布冷却策略,实测会静默装成上一个版本),国内镜像的同步也可能滞后一段时间。急着用最新代码,改用仓库地址安装最直接。
|
|
196
198
|
- **网页抓取失败?** 配置卡片中的「允许启动本地提取网页内容」默认开启:AnySearch 提取失败时可尝试本机直连。关闭后,本机直连的具名来源查询也不可用;请检查该开关和网络连接。
|
|
197
199
|
- **Wind 那部分会消耗积分?** 会。公告与新闻检索、宏观 / 行业 / 汇率指标、按日期区间取的历史 K 线都按次消耗你在 Wind 侧的额度;额度与条款以 Wind 为准,插件不做用量封顶。没配 `WIND_API_KEY` 时这几类不可用,其余功能照常。
|
package/chart-ui/package.json
CHANGED
package/config/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes redundant `subagent-settled` notices at the closing frame; otherwise the host loop opens
|
|
3
|
+
* another step after a complete answer and folds it. Only children with an admitted relay are
|
|
4
|
+
* dropped — a silent child may carry its only outcome in the notice.
|
|
5
|
+
* Rationale: docs/dev/chart-delivery-events.md §6.3.
|
|
6
|
+
*/
|
|
7
|
+
import type { SessionLike } from '../data-collector/store.js';
|
|
8
|
+
export interface WakeMessageLike {
|
|
9
|
+
readonly id?: string;
|
|
10
|
+
readonly source?: {
|
|
11
|
+
readonly kind?: string;
|
|
12
|
+
readonly senderSessionId?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
interface InboxLike {
|
|
16
|
+
readonly nextTurn?: readonly unknown[];
|
|
17
|
+
readonly nextStep?: readonly unknown[];
|
|
18
|
+
remove?(messageId: string): boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface WakeAgentLike {
|
|
21
|
+
readonly session?: SessionLike;
|
|
22
|
+
readonly inbox?: InboxLike;
|
|
23
|
+
}
|
|
24
|
+
export interface SettledWakeGuard {
|
|
25
|
+
/** `user/message` events: human resets the exchange, an admitted child relay marks that child. */
|
|
26
|
+
acceptUserMessage(session: SessionLike, source: {
|
|
27
|
+
kind?: string;
|
|
28
|
+
senderSessionId?: string;
|
|
29
|
+
}): void;
|
|
30
|
+
/** Terminal assistant-stream frame: the closing answer is being written. */
|
|
31
|
+
acceptClosing(agent: WakeAgentLike | undefined): void;
|
|
32
|
+
/** A notice inserted after the closing frame must not wake a new step. */
|
|
33
|
+
acceptInboxInserted(agent: WakeAgentLike | undefined, message: WakeMessageLike | undefined): void;
|
|
34
|
+
/** A fresh activation invalidates the previous exchange's relay marker. */
|
|
35
|
+
acceptSubagentStart(info: {
|
|
36
|
+
id?: string;
|
|
37
|
+
}): void;
|
|
38
|
+
/** Diagnostics/tests. */
|
|
39
|
+
closing(): boolean;
|
|
40
|
+
admitted(): readonly string[];
|
|
41
|
+
}
|
|
42
|
+
export declare function createSettledWakeGuard(): SettledWakeGuard;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
function messageOf(value) {
|
|
2
|
+
return value !== null && typeof value === 'object' ? value : undefined;
|
|
3
|
+
}
|
|
4
|
+
export function createSettledWakeGuard() {
|
|
5
|
+
let rootSessionId;
|
|
6
|
+
let closing = false;
|
|
7
|
+
const admitted = new Set();
|
|
8
|
+
const owns = (session) => {
|
|
9
|
+
if (session?.id === undefined)
|
|
10
|
+
return false;
|
|
11
|
+
// A child's own user/message events must not clear the root's state.
|
|
12
|
+
if (session.header?.origin === 'subagent')
|
|
13
|
+
return false;
|
|
14
|
+
rootSessionId ??= session.id;
|
|
15
|
+
return session.id === rootSessionId;
|
|
16
|
+
};
|
|
17
|
+
const dropNotice = (agent, message) => {
|
|
18
|
+
const childId = message.source?.senderSessionId;
|
|
19
|
+
if (typeof childId !== 'string' || !admitted.has(childId))
|
|
20
|
+
return false;
|
|
21
|
+
if (agent?.inbox?.remove === undefined || typeof message.id !== 'string')
|
|
22
|
+
return false;
|
|
23
|
+
let removed = false;
|
|
24
|
+
try {
|
|
25
|
+
removed = agent.inbox.remove(message.id);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
removed = false;
|
|
29
|
+
}
|
|
30
|
+
if (removed)
|
|
31
|
+
admitted.delete(childId);
|
|
32
|
+
return removed;
|
|
33
|
+
};
|
|
34
|
+
const dropPending = (agent) => {
|
|
35
|
+
if (agent?.inbox === undefined)
|
|
36
|
+
return;
|
|
37
|
+
for (const value of [...(agent.inbox.nextTurn ?? []), ...(agent.inbox.nextStep ?? [])]) {
|
|
38
|
+
const message = messageOf(value);
|
|
39
|
+
if (message?.source?.kind !== 'subagent-settled')
|
|
40
|
+
continue;
|
|
41
|
+
dropNotice(agent, message);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
acceptUserMessage(session, source) {
|
|
46
|
+
if (!owns(session))
|
|
47
|
+
return;
|
|
48
|
+
if (source.kind === 'user') {
|
|
49
|
+
admitted.clear();
|
|
50
|
+
closing = false;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (source.kind !== 'agent-message')
|
|
54
|
+
return;
|
|
55
|
+
if (typeof source.senderSessionId !== 'string' || source.senderSessionId.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
admitted.add(source.senderSessionId);
|
|
58
|
+
// The answer that follows this relay is not the previous closing answer.
|
|
59
|
+
closing = false;
|
|
60
|
+
},
|
|
61
|
+
acceptClosing(agent) {
|
|
62
|
+
if (agent?.session !== undefined && !owns(agent.session))
|
|
63
|
+
return;
|
|
64
|
+
closing = true;
|
|
65
|
+
dropPending(agent);
|
|
66
|
+
},
|
|
67
|
+
acceptInboxInserted(agent, message) {
|
|
68
|
+
if (agent?.session !== undefined && !owns(agent.session))
|
|
69
|
+
return;
|
|
70
|
+
if (message?.source?.kind !== 'subagent-settled' || !closing)
|
|
71
|
+
return;
|
|
72
|
+
dropNotice(agent, message);
|
|
73
|
+
},
|
|
74
|
+
acceptSubagentStart(info) {
|
|
75
|
+
if (typeof info?.id === 'string' && info.id.length > 0)
|
|
76
|
+
admitted.delete(info.id);
|
|
77
|
+
},
|
|
78
|
+
closing: () => closing,
|
|
79
|
+
admitted: () => [...admitted],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root-only `wait_for_agents`: hold one turn across child relays; returns on the first relay, on
|
|
3
|
+
* the tree settling, or after 30s. `waitForChildren` has no abort parameter; the slice races it.
|
|
4
|
+
* Host contract: docs/reference/dsh-surface-ledger.md L36.
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
7
|
+
export interface WaitInboxLike {
|
|
8
|
+
readonly nextTurn?: readonly unknown[];
|
|
9
|
+
readonly nextStep?: readonly unknown[];
|
|
10
|
+
}
|
|
11
|
+
export interface WaitAgentLike {
|
|
12
|
+
readonly session?: {
|
|
13
|
+
readonly id?: string;
|
|
14
|
+
readonly header?: {
|
|
15
|
+
readonly parentSession?: string;
|
|
16
|
+
readonly origin?: 'subagent';
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
readonly inbox?: WaitInboxLike;
|
|
20
|
+
readonly status?: 'idle' | 'running';
|
|
21
|
+
}
|
|
22
|
+
export interface WaitDescendantLike {
|
|
23
|
+
readonly kind?: string;
|
|
24
|
+
readonly id?: string;
|
|
25
|
+
readonly label?: string;
|
|
26
|
+
readonly activity?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface WaitSubagentsLike {
|
|
29
|
+
waitForChildren(parent: WaitAgentLike): Promise<boolean>;
|
|
30
|
+
listDescendants?(rootSessionId: string, signal?: AbortSignal): Promise<readonly WaitDescendantLike[]>;
|
|
31
|
+
}
|
|
32
|
+
interface AgentsLike {
|
|
33
|
+
get?(id: string): {
|
|
34
|
+
readonly status?: 'idle' | 'running';
|
|
35
|
+
} | undefined;
|
|
36
|
+
}
|
|
37
|
+
interface EventContextLike {
|
|
38
|
+
on?(event: string, listener: (...args: unknown[]) => void): unknown;
|
|
39
|
+
}
|
|
40
|
+
export interface WaitForAgentsOptions {
|
|
41
|
+
/** Bounded slice; the model cannot extend it. */
|
|
42
|
+
readonly waitSliceMs?: number;
|
|
43
|
+
readonly now?: () => number;
|
|
44
|
+
}
|
|
45
|
+
export interface WaitForAgentsDeps extends WaitForAgentsOptions {
|
|
46
|
+
subagents: () => WaitSubagentsLike | undefined;
|
|
47
|
+
events?: EventContextLike;
|
|
48
|
+
agents?: () => AgentsLike | undefined;
|
|
49
|
+
}
|
|
50
|
+
export type WaitForAgentsStatus = 'all_settled' | 'message' | 'user_input' | 'timeout';
|
|
51
|
+
export interface WaitForAgentsReceipt {
|
|
52
|
+
readonly status: WaitForAgentsStatus;
|
|
53
|
+
readonly waited_ms: number;
|
|
54
|
+
readonly pending_messages: number;
|
|
55
|
+
readonly running: Array<{
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly label?: string;
|
|
58
|
+
}>;
|
|
59
|
+
readonly hint: string;
|
|
60
|
+
}
|
|
61
|
+
declare function render(_args: unknown, value: unknown): Array<{
|
|
62
|
+
type: 'text';
|
|
63
|
+
text: string;
|
|
64
|
+
}>;
|
|
65
|
+
export declare function waitForAgentsToolDefinition(deps: WaitForAgentsDeps): {
|
|
66
|
+
name: string;
|
|
67
|
+
description: string;
|
|
68
|
+
parameters: object;
|
|
69
|
+
output: {
|
|
70
|
+
schema: object;
|
|
71
|
+
render: typeof render;
|
|
72
|
+
};
|
|
73
|
+
execute(_args: Record<string, unknown>, exec: {
|
|
74
|
+
signal: AbortSignal;
|
|
75
|
+
agent?: WaitAgentLike;
|
|
76
|
+
}): Promise<WaitForAgentsReceipt>;
|
|
77
|
+
};
|
|
78
|
+
export declare function registerWaitForAgentsTool(ctx: Context, options?: WaitForAgentsOptions): void;
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
const DEFAULT_WAIT_SLICE_MS = 30_000;
|
|
2
|
+
const MAX_WAIT_SLICE_MS = 60_000;
|
|
3
|
+
const jsonObject = (properties = {}, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
|
|
4
|
+
function render(_args, value) {
|
|
5
|
+
return [{ type: 'text', text: JSON.stringify(value) }];
|
|
6
|
+
}
|
|
7
|
+
function sourceKindOf(message) {
|
|
8
|
+
if (message === null || typeof message !== 'object')
|
|
9
|
+
return undefined;
|
|
10
|
+
const source = message.source;
|
|
11
|
+
if (source === null || typeof source !== 'object')
|
|
12
|
+
return undefined;
|
|
13
|
+
const kind = source.kind;
|
|
14
|
+
return typeof kind === 'string' ? kind : undefined;
|
|
15
|
+
}
|
|
16
|
+
/** Settlement notices are lifecycle noise here; only a human or a relayed result is actionable. */
|
|
17
|
+
function pendingCounts(agent) {
|
|
18
|
+
const messages = [...(agent.inbox?.nextTurn ?? []), ...(agent.inbox?.nextStep ?? [])];
|
|
19
|
+
let users = 0;
|
|
20
|
+
let children = 0;
|
|
21
|
+
for (const message of messages) {
|
|
22
|
+
const kind = sourceKindOf(message);
|
|
23
|
+
if (kind === 'user')
|
|
24
|
+
users += 1;
|
|
25
|
+
else if (kind === 'agent-message')
|
|
26
|
+
children += 1;
|
|
27
|
+
}
|
|
28
|
+
return { total: users + children, users, children };
|
|
29
|
+
}
|
|
30
|
+
function resolveSlice(value) {
|
|
31
|
+
const numeric = typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : DEFAULT_WAIT_SLICE_MS;
|
|
32
|
+
return Math.max(1, Math.min(MAX_WAIT_SLICE_MS, numeric));
|
|
33
|
+
}
|
|
34
|
+
function abortReason(signal) {
|
|
35
|
+
return signal.reason instanceof Error ? signal.reason : new Error('wait_for_agents aborted');
|
|
36
|
+
}
|
|
37
|
+
function waitTimer(ms, signal) {
|
|
38
|
+
let handle;
|
|
39
|
+
let onAbort;
|
|
40
|
+
const promise = new Promise((resolve, reject) => {
|
|
41
|
+
if (signal.aborted) {
|
|
42
|
+
reject(abortReason(signal));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
handle = setTimeout(() => resolve('timeout'), ms);
|
|
46
|
+
onAbort = () => {
|
|
47
|
+
if (handle !== undefined)
|
|
48
|
+
clearTimeout(handle);
|
|
49
|
+
reject(abortReason(signal));
|
|
50
|
+
};
|
|
51
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
promise,
|
|
55
|
+
dispose: () => {
|
|
56
|
+
if (handle !== undefined)
|
|
57
|
+
clearTimeout(handle);
|
|
58
|
+
if (onAbort !== undefined)
|
|
59
|
+
signal.removeEventListener('abort', onAbort);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function runningRows(service, agentsOf, sessionId, signal) {
|
|
64
|
+
if (typeof service.listDescendants !== 'function')
|
|
65
|
+
return [];
|
|
66
|
+
let rows;
|
|
67
|
+
try {
|
|
68
|
+
rows = await service.listDescendants(sessionId, signal);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
let agents;
|
|
74
|
+
try {
|
|
75
|
+
agents = agentsOf?.();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
agents = undefined;
|
|
79
|
+
}
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
const running = [];
|
|
82
|
+
for (const row of rows ?? []) {
|
|
83
|
+
if (row?.kind !== 'child' || typeof row.id !== 'string' || seen.has(row.id))
|
|
84
|
+
continue;
|
|
85
|
+
let live;
|
|
86
|
+
try {
|
|
87
|
+
live = agents?.get?.(row.id);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
live = undefined;
|
|
91
|
+
}
|
|
92
|
+
// A live idle child is not progressing; a catalog row alone is only a historical child.
|
|
93
|
+
if (live?.status === 'idle')
|
|
94
|
+
continue;
|
|
95
|
+
if (row.activity !== 'running' && live?.status !== 'running')
|
|
96
|
+
continue;
|
|
97
|
+
seen.add(row.id);
|
|
98
|
+
const label = typeof row.label === 'string' && row.label.length > 0 ? row.label : undefined;
|
|
99
|
+
running.push({ id: row.id, ...(label === undefined ? {} : { label }) });
|
|
100
|
+
}
|
|
101
|
+
return running;
|
|
102
|
+
}
|
|
103
|
+
function hintFor(status, pending, running) {
|
|
104
|
+
switch (status) {
|
|
105
|
+
case 'all_settled':
|
|
106
|
+
return '所有子 Agent 已结束。现在按对账表逐行核对;收齐后先 present_charts(有图)再写完整最终答复。';
|
|
107
|
+
case 'message':
|
|
108
|
+
return `有 ${pending} 条子 Agent 消息待处理,下一步会带上它们;处理完仍有未结算角色就再调用 wait_for_agents。`;
|
|
109
|
+
case 'user_input':
|
|
110
|
+
return `用户在等待期间发来消息;当前有 ${pending} 条可执行消息待处理,先处理用户输入,再决定是否继续等待。`;
|
|
111
|
+
case 'timeout':
|
|
112
|
+
return running > 0
|
|
113
|
+
? `本次等待窗口内未收敛,仍有 ${running} 个子 Agent 在运行;继续调用 wait_for_agents。running 已列出存活子树,不要再调用 list_agents,也不要 send_message 催运行中的子 Agent;只有用户要求进度或无法继续时才说明。`
|
|
114
|
+
: '本次等待窗口内未收敛,也没有可报告的运行中子 Agent;再调用一次确认,不要调用 list_agents,不要当作完成。';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function waitForAgentsToolDefinition(deps) {
|
|
118
|
+
const waitSliceMs = resolveSlice(deps.waitSliceMs);
|
|
119
|
+
const now = deps.now ?? Date.now;
|
|
120
|
+
const parameters = jsonObject({});
|
|
121
|
+
const outputSchema = jsonObject({
|
|
122
|
+
status: { type: 'string', enum: ['all_settled', 'message', 'user_input', 'timeout'] },
|
|
123
|
+
waited_ms: { type: 'integer' },
|
|
124
|
+
pending_messages: { type: 'integer' },
|
|
125
|
+
running: {
|
|
126
|
+
type: 'array',
|
|
127
|
+
items: jsonObject({
|
|
128
|
+
id: { type: 'string' },
|
|
129
|
+
label: { type: 'string' },
|
|
130
|
+
}, ['id']),
|
|
131
|
+
},
|
|
132
|
+
hint: { type: 'string' },
|
|
133
|
+
}, ['status', 'waited_ms', 'pending_messages', 'running', 'hint']);
|
|
134
|
+
return {
|
|
135
|
+
name: 'wait_for_agents',
|
|
136
|
+
description: '等待你的子 Agent 树完成当前工作,单次最多 30 秒;把“每个子 Agent 回传/结算各开一轮”收敛成同一轮。'
|
|
137
|
+
+ '创建、复用或 send_message 之后调用它,等待期间不写可见正文,也不要用“请稍候”式正文结束回合。'
|
|
138
|
+
+ '返回 message 或 user_input 时,下一步会带上这些消息;处理完继续调用。'
|
|
139
|
+
+ '返回 all_settled 才进入对账与最终答复;timeout 表示仍未收敛,仍有 running 时继续调用,不要仅因一次超时写等待行。'
|
|
140
|
+
+ '等待期间不要用 list_agents 轮询。只对主 Agent 开放;没有子 Agent 时会立即返回 all_settled。',
|
|
141
|
+
parameters,
|
|
142
|
+
output: { schema: outputSchema, render },
|
|
143
|
+
// Deliberately no isConcurrencySafe: waiting is the ordering barrier between dispatch and synthesis.
|
|
144
|
+
async execute(_args, exec) {
|
|
145
|
+
const agent = exec.agent;
|
|
146
|
+
const sessionId = agent?.session?.id;
|
|
147
|
+
if (agent === undefined || sessionId === undefined) {
|
|
148
|
+
throw new Error('wait_for_agents requires a calling agent (exec.agent was undefined)');
|
|
149
|
+
}
|
|
150
|
+
// `parentSession` alone is a user fork; only the subagent origin marks a delegated child.
|
|
151
|
+
if (agent.session?.header?.origin === 'subagent') {
|
|
152
|
+
throw new Error('wait_for_agents 只对主 Agent 开放:子 Agent 用 send_message 回传结果,不要等待其他 Agent。');
|
|
153
|
+
}
|
|
154
|
+
if (exec.signal.aborted)
|
|
155
|
+
throw abortReason(exec.signal);
|
|
156
|
+
const service = deps.subagents();
|
|
157
|
+
if (service?.waitForChildren === undefined) {
|
|
158
|
+
throw new Error('wait_for_agents 不可用:宿主 subagents 服务未挂载;不要用 list_agents 轮询代替。');
|
|
159
|
+
}
|
|
160
|
+
const started = now();
|
|
161
|
+
const initial = pendingCounts(agent);
|
|
162
|
+
if (initial.users > 0 || initial.children > 0) {
|
|
163
|
+
const status = initial.users > 0 ? 'user_input' : 'message';
|
|
164
|
+
const running = await runningRows(service, deps.agents, sessionId, exec.signal);
|
|
165
|
+
return {
|
|
166
|
+
status,
|
|
167
|
+
waited_ms: Math.max(0, Math.round(now() - started)),
|
|
168
|
+
pending_messages: initial.total,
|
|
169
|
+
running,
|
|
170
|
+
hint: hintFor(status, initial.total, running.length),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
let resolveInbox;
|
|
174
|
+
const inboxEvent = new Promise((resolve) => { resolveInbox = resolve; });
|
|
175
|
+
const onInserted = (...args) => {
|
|
176
|
+
const payload = args[0];
|
|
177
|
+
if (payload?.agent?.session?.id !== sessionId)
|
|
178
|
+
return;
|
|
179
|
+
const kind = sourceKindOf(payload.message);
|
|
180
|
+
if (kind === 'agent-message')
|
|
181
|
+
resolveInbox('message');
|
|
182
|
+
else if (kind === 'user')
|
|
183
|
+
resolveInbox('user_input');
|
|
184
|
+
};
|
|
185
|
+
let dispose;
|
|
186
|
+
if (typeof deps.events?.on === 'function') {
|
|
187
|
+
try {
|
|
188
|
+
dispose = deps.events.on('agent/inbox/inserted', onInserted);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
dispose = undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
let outcome;
|
|
195
|
+
const slice = waitTimer(waitSliceMs, exec.signal);
|
|
196
|
+
try {
|
|
197
|
+
const wait = Promise.resolve(service.waitForChildren(agent));
|
|
198
|
+
// The loser of the race must not become an unhandled rejection when the slice or a relay wins.
|
|
199
|
+
wait.catch(() => undefined);
|
|
200
|
+
outcome = await Promise.race([
|
|
201
|
+
wait.then(() => 'settled'),
|
|
202
|
+
inboxEvent,
|
|
203
|
+
slice.promise,
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
slice.dispose();
|
|
208
|
+
if (typeof dispose === 'function')
|
|
209
|
+
dispose();
|
|
210
|
+
}
|
|
211
|
+
const pending = pendingCounts(agent);
|
|
212
|
+
const status = pending.users > 0
|
|
213
|
+
? 'user_input'
|
|
214
|
+
: pending.children > 0
|
|
215
|
+
? 'message'
|
|
216
|
+
: outcome === 'timeout' ? 'timeout' : 'all_settled';
|
|
217
|
+
const running = status === 'all_settled'
|
|
218
|
+
? []
|
|
219
|
+
: await runningRows(service, deps.agents, sessionId, exec.signal);
|
|
220
|
+
return {
|
|
221
|
+
status,
|
|
222
|
+
waited_ms: Math.max(0, Math.round(now() - started)),
|
|
223
|
+
pending_messages: pending.total,
|
|
224
|
+
running,
|
|
225
|
+
hint: hintFor(status, pending.total, running.length),
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
export function registerWaitForAgentsTool(ctx, options = {}) {
|
|
231
|
+
const tools = ctx.get('tools');
|
|
232
|
+
if (tools === undefined)
|
|
233
|
+
return;
|
|
234
|
+
const events = ctx;
|
|
235
|
+
const definition = waitForAgentsToolDefinition({
|
|
236
|
+
...options,
|
|
237
|
+
subagents: () => {
|
|
238
|
+
try {
|
|
239
|
+
return ctx.get('subagents');
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
events,
|
|
246
|
+
agents: () => {
|
|
247
|
+
try {
|
|
248
|
+
return ctx.get('agents');
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
ctx.effect(() => tools.register(definition), 'capital-generation.tool(wait_for_agents)');
|
|
256
|
+
}
|
|
@@ -11,6 +11,8 @@ export interface ChartArtifactRef {
|
|
|
11
11
|
dataset_id: string | null;
|
|
12
12
|
source_label: string | null;
|
|
13
13
|
captured_at: number | null;
|
|
14
|
+
/** Six-digit security/index code parsed from the Dataset envelope or rows, when present. */
|
|
15
|
+
subject_code: string | null;
|
|
14
16
|
kind: string;
|
|
15
17
|
axis: 'time' | 'index';
|
|
16
18
|
title: string;
|
|
@@ -28,7 +30,7 @@ export interface ChartArtifactRegistryOptions {
|
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* In-process chart_ref capabilities: session/task/workspace binding and the deliverable owner
|
|
31
|
-
* scope. Process restart invalidates them;
|
|
33
|
+
* scope. Process restart invalidates them; the title-named self-contained HTML remains the offline fallback.
|
|
32
34
|
*/
|
|
33
35
|
export declare class ChartArtifactRegistry {
|
|
34
36
|
private readonly records;
|
|
@@ -44,6 +46,7 @@ export declare class ChartArtifactRegistry {
|
|
|
44
46
|
dataset_id?: string | null;
|
|
45
47
|
source_label?: string | null;
|
|
46
48
|
captured_at?: number | null;
|
|
49
|
+
subject_code?: string | null;
|
|
47
50
|
kind: string;
|
|
48
51
|
axis: 'time' | 'index';
|
|
49
52
|
title: string;
|
|
@@ -18,7 +18,7 @@ export function chartSessionScopeId(session) {
|
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* In-process chart_ref capabilities: session/task/workspace binding and the deliverable owner
|
|
21
|
-
* scope. Process restart invalidates them;
|
|
21
|
+
* scope. Process restart invalidates them; the title-named self-contained HTML remains the offline fallback.
|
|
22
22
|
*/
|
|
23
23
|
export class ChartArtifactRegistry {
|
|
24
24
|
records = new Map();
|
|
@@ -57,6 +57,7 @@ export class ChartArtifactRegistry {
|
|
|
57
57
|
dataset_id: input.dataset_id ?? null,
|
|
58
58
|
source_label: input.source_label ?? null,
|
|
59
59
|
captured_at: input.captured_at ?? null,
|
|
60
|
+
subject_code: input.subject_code ?? null,
|
|
60
61
|
kind: input.kind,
|
|
61
62
|
axis: input.axis,
|
|
62
63
|
title: input.title,
|
package/lib/chart/delivery.d.ts
CHANGED
|
@@ -31,6 +31,15 @@ export interface AssistantStreamFrameLike {
|
|
|
31
31
|
};
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
/** Live root Agent passed to the closing hook so it can cancel inbox notices before the loop claims them. */
|
|
35
|
+
export interface DeliveryAgentLike {
|
|
36
|
+
readonly session?: SessionLike;
|
|
37
|
+
readonly inbox?: {
|
|
38
|
+
readonly nextTurn?: readonly unknown[];
|
|
39
|
+
readonly nextStep?: readonly unknown[];
|
|
40
|
+
remove?(messageId: string): boolean;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
34
43
|
interface InboxStateLike {
|
|
35
44
|
'next-turn'?: readonly unknown[];
|
|
36
45
|
'next-step'?: readonly unknown[];
|
|
@@ -41,13 +50,18 @@ export interface ChartDeliveryOptions {
|
|
|
41
50
|
publish: (sessionId: string, inputs: ChartTurnEventInput[]) => boolean;
|
|
42
51
|
/** Durable pending input; synchronous projection read. Absent means "cannot tell" → proceed. */
|
|
43
52
|
inboxOf?: (session: SessionLike) => InboxStateLike | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Runs on every closing finish, including exchanges with no chart. The wake guard removes
|
|
55
|
+
* redundant settlement notices here; it must never be allowed to block delivery.
|
|
56
|
+
*/
|
|
57
|
+
onClosing?: (session: SessionLike, agent: DeliveryAgentLike | undefined, turn: number | undefined) => void;
|
|
44
58
|
/** Bounded batch; the card collapses past four files and expands on demand. */
|
|
45
59
|
maxCharts?: number;
|
|
46
60
|
}
|
|
47
61
|
export interface ChartDelivery {
|
|
48
62
|
/** `present_charts` selection; merged across calls because models often retry in batches. */
|
|
49
63
|
request(session: SessionLike, inputs: ChartTurnEventInput[]): void;
|
|
50
|
-
acceptStream(session: SessionLike | undefined, frame: AssistantStreamFrameLike): void;
|
|
64
|
+
acceptStream(session: SessionLike | undefined, frame: AssistantStreamFrameLike, agent?: DeliveryAgentLike): void;
|
|
51
65
|
/** A new human message starts a new exchange: old charts must not ride into it. */
|
|
52
66
|
acceptUserMessage(sessionId: string, time: number): void;
|
|
53
67
|
acceptSubagentStart(info: {
|