@v587d/capital-generation 2.9.1 → 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 +73 -0
- package/README.md +6 -3
- 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/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 +80 -0
- package/lib/chart/delivery.js +157 -0
- package/lib/chart/events.d.ts +3 -1
- package/lib/chart/events.js +52 -1
- package/lib/chart/html.js +16 -0
- package/lib/chart/present-tool.d.ts +8 -0
- package/lib/chart/present-tool.js +92 -22
- package/lib/chart/tool.js +63 -9
- package/lib/data-collector/dataset-tools.js +1 -1
- 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.d.ts +13 -3
- package/lib/present-entry.js +123 -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 +8 -2
- package/preset/capital-generation/README.md +34 -16
- package/preset/capital-generation/agent.patch.yml +30 -34
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +3 -2
- package/preset/capital-generation/skills/capital-data-profile/SKILL.md +9 -4
- package/preset/capital-generation/skills/capital-data-query/SKILL.md +47 -19
- package/preset/capital-generation/skills/capital-equity-research/SKILL.md +5 -1
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +12 -2
- package/watchlist/package.json +1 -1
|
@@ -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,
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
/** 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
|
+
}
|
|
43
|
+
interface InboxStateLike {
|
|
44
|
+
'next-turn'?: readonly unknown[];
|
|
45
|
+
'next-step'?: readonly unknown[];
|
|
46
|
+
}
|
|
47
|
+
export interface ChartDeliveryOptions {
|
|
48
|
+
artifacts: ChartArtifactRegistry;
|
|
49
|
+
/** Writes one first-party deliverables/presented batch; must be synchronous (pre-message seq). */
|
|
50
|
+
publish: (sessionId: string, inputs: ChartTurnEventInput[]) => boolean;
|
|
51
|
+
/** Durable pending input; synchronous projection read. Absent means "cannot tell" → proceed. */
|
|
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;
|
|
58
|
+
/** Bounded batch; the card collapses past four files and expands on demand. */
|
|
59
|
+
maxCharts?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface ChartDelivery {
|
|
62
|
+
/** `present_charts` selection; merged across calls because models often retry in batches. */
|
|
63
|
+
request(session: SessionLike, inputs: ChartTurnEventInput[]): void;
|
|
64
|
+
acceptStream(session: SessionLike | undefined, frame: AssistantStreamFrameLike, agent?: DeliveryAgentLike): void;
|
|
65
|
+
/** A new human message starts a new exchange: old charts must not ride into it. */
|
|
66
|
+
acceptUserMessage(sessionId: string, time: number): void;
|
|
67
|
+
acceptSubagentStart(info: {
|
|
68
|
+
id?: string;
|
|
69
|
+
}): void;
|
|
70
|
+
acceptSubagentEnd(info: {
|
|
71
|
+
id?: string;
|
|
72
|
+
}): void;
|
|
73
|
+
/** False until any stream frame was observed: callers fall back to immediate delivery. */
|
|
74
|
+
streaming(): boolean;
|
|
75
|
+
/** Diagnostics/tests: explicitly selected inputs for the current exchange. */
|
|
76
|
+
pending(sessionId?: string): number;
|
|
77
|
+
}
|
|
78
|
+
export declare const DEFAULT_MAX_DELIVERED_CHARTS = 32;
|
|
79
|
+
export declare function createChartDelivery(options: ChartDeliveryOptions): ChartDelivery;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
/** One card per exchange. `turn` is unreliable: a later finish in the same turn may omit it. */
|
|
18
|
+
let flushed = false;
|
|
19
|
+
let sawFrame = false;
|
|
20
|
+
const attempts = new Map();
|
|
21
|
+
const running = new Map();
|
|
22
|
+
const owns = (session) => {
|
|
23
|
+
if (session?.id === undefined)
|
|
24
|
+
return false;
|
|
25
|
+
// A leaked child frame must not bind the manager: the card belongs to the user-facing root.
|
|
26
|
+
if (session.header?.origin === 'subagent')
|
|
27
|
+
return false;
|
|
28
|
+
sessionId ??= session.id;
|
|
29
|
+
return session.id === sessionId;
|
|
30
|
+
};
|
|
31
|
+
const autoInputs = (session) => options.artifacts
|
|
32
|
+
.list({ session })
|
|
33
|
+
.filter((artifact) => artifact.created_at >= (exchangeStarts.get(session.id) ?? fallbackStart))
|
|
34
|
+
.map((artifact) => ({
|
|
35
|
+
ownerSessionId: session.id,
|
|
36
|
+
chart_id: artifact.chart_id,
|
|
37
|
+
title: artifact.title,
|
|
38
|
+
html_path: artifact.html_path,
|
|
39
|
+
}));
|
|
40
|
+
/** A settled notice proves the child is done even if the lifecycle edge was missed or the parent
|
|
41
|
+
* observed it outside its scope. Reconciliating here keeps the stream-time gate synchronous. */
|
|
42
|
+
const reconcileSettled = (inbox) => {
|
|
43
|
+
for (const message of inboxMessages(inbox)) {
|
|
44
|
+
if (!isSettledNotice(message))
|
|
45
|
+
continue;
|
|
46
|
+
const childId = message.source?.senderSessionId;
|
|
47
|
+
if (typeof childId === 'string')
|
|
48
|
+
running.delete(childId);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const idle = (inbox) => {
|
|
52
|
+
if (running.size > 0)
|
|
53
|
+
return false;
|
|
54
|
+
if (inbox === undefined)
|
|
55
|
+
return true;
|
|
56
|
+
return inboxMessages(inbox).every(isSettledNotice);
|
|
57
|
+
};
|
|
58
|
+
const flush = (session, turn, agent) => {
|
|
59
|
+
// A later step in the same exchange may omit `turn`; it still must not write a second card.
|
|
60
|
+
if (flushed)
|
|
61
|
+
return;
|
|
62
|
+
const inbox = options.inboxOf?.(session);
|
|
63
|
+
reconcileSettled(inbox);
|
|
64
|
+
if (!idle(inbox))
|
|
65
|
+
return;
|
|
66
|
+
// The wake guard drops notices that would otherwise make the loop open another step after
|
|
67
|
+
// this answer. Run it even when no chart is selected, and never let it block delivery.
|
|
68
|
+
try {
|
|
69
|
+
options.onClosing?.(session, agent, turn);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// A guard failure must not strand an already-selected card.
|
|
73
|
+
}
|
|
74
|
+
const inputs = (explicit.length > 0 ? explicit : autoInputs(session)).slice(0, maxCharts);
|
|
75
|
+
if (inputs.length === 0)
|
|
76
|
+
return;
|
|
77
|
+
let written = false;
|
|
78
|
+
try {
|
|
79
|
+
written = options.publish(session.id, inputs);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
written = false;
|
|
83
|
+
}
|
|
84
|
+
// A failed write keeps the exchange open so a later closing finish can retry.
|
|
85
|
+
if (written)
|
|
86
|
+
flushed = true;
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
request(session, inputs) {
|
|
90
|
+
if (!owns(session) || inputs.length === 0)
|
|
91
|
+
return;
|
|
92
|
+
const seen = new Set(explicit.map((item) => item.chart_id));
|
|
93
|
+
for (const input of inputs) {
|
|
94
|
+
if (seen.has(input.chart_id))
|
|
95
|
+
continue;
|
|
96
|
+
seen.add(input.chart_id);
|
|
97
|
+
explicit.push(input);
|
|
98
|
+
}
|
|
99
|
+
if (explicit.length > maxCharts)
|
|
100
|
+
explicit = explicit.slice(0, maxCharts);
|
|
101
|
+
},
|
|
102
|
+
acceptStream(session, frame, agent) {
|
|
103
|
+
if (!owns(session) || frame === undefined)
|
|
104
|
+
return;
|
|
105
|
+
sawFrame = true;
|
|
106
|
+
const attemptId = frame.attemptId ?? '__default__';
|
|
107
|
+
if (frame.type === 'start') {
|
|
108
|
+
attempts.set(attemptId, { toolCall: false });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (frame.type === 'end') {
|
|
112
|
+
attempts.delete(attemptId);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const chunk = frame.chunk;
|
|
116
|
+
if (frame.type !== 'chunk' || chunk === undefined)
|
|
117
|
+
return;
|
|
118
|
+
const attempt = attempts.get(attemptId) ?? { toolCall: false };
|
|
119
|
+
attempts.set(attemptId, attempt);
|
|
120
|
+
if (chunk.type === 'tool-call-delta'
|
|
121
|
+
|| (chunk.type === 'block-start' && chunk.blockType === 'tool-call')
|
|
122
|
+
|| (chunk.type === 'block-end' && chunk.block?.type === 'tool-call')) {
|
|
123
|
+
attempt.toolCall = true;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (chunk.type !== 'finish' || chunk.reason?.kind !== 'stop' || attempt.toolCall)
|
|
127
|
+
return;
|
|
128
|
+
flush(session, frame.turn, agent);
|
|
129
|
+
},
|
|
130
|
+
acceptUserMessage(id, time) {
|
|
131
|
+
exchangeStarts.set(id, time);
|
|
132
|
+
if (id !== sessionId)
|
|
133
|
+
return;
|
|
134
|
+
explicit = [];
|
|
135
|
+
flushed = false;
|
|
136
|
+
},
|
|
137
|
+
acceptSubagentStart(info) {
|
|
138
|
+
if (typeof info?.id !== 'string' || info.id.length === 0)
|
|
139
|
+
return;
|
|
140
|
+
running.set(info.id, 1);
|
|
141
|
+
},
|
|
142
|
+
acceptSubagentEnd(info) {
|
|
143
|
+
if (typeof info?.id !== 'string')
|
|
144
|
+
return;
|
|
145
|
+
// One child session runs one activation at a time; deleting also clears duplicate starts.
|
|
146
|
+
running.delete(info.id);
|
|
147
|
+
},
|
|
148
|
+
streaming() {
|
|
149
|
+
return sawFrame;
|
|
150
|
+
},
|
|
151
|
+
pending(id) {
|
|
152
|
+
if (id !== undefined && id !== sessionId)
|
|
153
|
+
return 0;
|
|
154
|
+
return explicit.length;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
package/lib/chart/events.d.ts
CHANGED
|
@@ -33,11 +33,13 @@ export interface ChartTurnEventInput {
|
|
|
33
33
|
ownerSessionId: string;
|
|
34
34
|
chart_id: string;
|
|
35
35
|
title: string;
|
|
36
|
-
/** Workspace-relative
|
|
36
|
+
/** Workspace-relative self-contained HTML path; the official preview resolves it against session.cwd. */
|
|
37
37
|
html_path: string;
|
|
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)
|
package/lib/chart/html.js
CHANGED
|
@@ -59,7 +59,9 @@ function formatCapturedAt(epochMs) {
|
|
|
59
59
|
export function buildStandaloneHtml(input) {
|
|
60
60
|
const { payload, library } = input;
|
|
61
61
|
const meta = payload.meta;
|
|
62
|
+
const kindLabel = payload.kind === 'candlestick' ? 'K 线图' : payload.kind === 'bar' ? 'OHLC 图' : payload.kind === 'column' ? '柱状图' : payload.kind === 'area' ? '面积图' : '折线图';
|
|
62
63
|
const facts = [];
|
|
64
|
+
facts.push(kindLabel);
|
|
63
65
|
facts.push(`${meta.points} 个数据点`);
|
|
64
66
|
if (meta.downsampled)
|
|
65
67
|
facts.push(`已从 ${meta.original_points} 点下采样(保留极值形状,非全量)`);
|
|
@@ -73,6 +75,18 @@ export function buildStandaloneHtml(input) {
|
|
|
73
75
|
facts.push(`时间字段 ${meta.time_field}`);
|
|
74
76
|
if (meta.axis === 'index')
|
|
75
77
|
facts.push('分类轴(按行序)');
|
|
78
|
+
const guide = [];
|
|
79
|
+
guide.push(meta.axis === 'index' ? '横轴为分类,按数据行序排列;纵轴为数值。' : '横轴为时间;纵轴为数值。');
|
|
80
|
+
if (payload.ohlc)
|
|
81
|
+
guide.push('K 线红涨绿跌,下方成交量柱按当日涨跌着色。');
|
|
82
|
+
else if (payload.volume)
|
|
83
|
+
guide.push('下方成交量柱按涨跌方向着色。');
|
|
84
|
+
if (payload.series.length > 0)
|
|
85
|
+
guide.push(`序列:${payload.series.map((item) => item.label).join('、')}。`);
|
|
86
|
+
if (meta.downsampled)
|
|
87
|
+
guide.push('曲线为下采样后的形状,数值结论请回到表格/查询结果。');
|
|
88
|
+
if (meta.warnings.length > 0)
|
|
89
|
+
guide.push('数据提示见下方 warnings。');
|
|
76
90
|
const warnings = meta.warnings.length > 0
|
|
77
91
|
? `<ul class="warnings">${meta.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('')}</ul>`
|
|
78
92
|
: '';
|
|
@@ -108,6 +122,7 @@ export function buildStandaloneHtml(input) {
|
|
|
108
122
|
body { font: 14px/1.6 -apple-system, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; padding: 16px 20px 24px; }
|
|
109
123
|
h1 { margin: 0 0 4px; font-size: 16px; font-weight: 600; }
|
|
110
124
|
.facts { margin: 0; color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
125
|
+
.guide { margin: 6px 0 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.7; }
|
|
111
126
|
main { margin-top: 12px; height: 460px; min-height: 320px; }
|
|
112
127
|
.warnings { margin: 12px 0 0; padding-left: 18px; color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
113
128
|
footer { margin-top: 16px; color: var(--dsw-alias-label-secondary); font-size: 11px; }
|
|
@@ -117,6 +132,7 @@ export function buildStandaloneHtml(input) {
|
|
|
117
132
|
<header>
|
|
118
133
|
<h1>${escapeHtml(payload.title)}</h1>
|
|
119
134
|
<p class="facts">${facts.map((fact) => escapeHtml(fact)).join(' · ')}</p>
|
|
135
|
+
<p class="guide">${guide.map((line) => escapeHtml(line)).join(' ')}</p>
|
|
120
136
|
</header>
|
|
121
137
|
<main id="chart"></main>
|
|
122
138
|
${warnings}
|