dsh-my-observability 0.3.0 → 0.3.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/lib/audit.js CHANGED
@@ -14,80 +14,77 @@
14
14
  * 返回值,next() 同步返回流;async listener 会让消费方(vision-toolkit 等
15
15
  * yield* 委托)拿到 Promise 而崩溃。tools/* 同理必须调用 next()。
16
16
  */
17
- import { MAX_ARG_KEYS, MAX_TEXT_LEN } from './constants.js'
18
-
17
+ import { MAX_ARG_KEYS, MAX_TEXT_LEN } from './constants.js';
19
18
  /**
20
19
  * 采集的插件事件名(按插件前缀显式注册,不依赖 cordis 通配符行为)。
21
20
  * 与 dsh-task-reliability/lib/emit.js 的 PLUGIN_EVENTS 保持一致;
22
21
  * 未来推广到其他插件(dsh-my-guardian 等)在此追加。
23
22
  */
24
23
  const PLUGIN_EVENT_NAMES = [
25
- 'task-reliability/intervention',
26
- 'task-reliability/ask-decision',
27
- 'task-reliability/rescue',
28
- 'task-reliability/verify',
29
- 'task-reliability/resume',
30
- ]
31
-
24
+ 'task-reliability/intervention',
25
+ 'task-reliability/ask-decision',
26
+ 'task-reliability/rescue',
27
+ 'task-reliability/verify',
28
+ 'task-reliability/resume',
29
+ ];
32
30
  /** 注册全部审计监听;返回 disposer 数组(全部经 ctx.on 注册)。 */
33
31
  export function attachAuditListeners(ctx, record) {
34
- return [
35
- ctx.on('session/event', (session, event) => handleSessionEvent(session, event, record)),
36
- ctx.on('agent/status', (payload) => handleStatus(payload, record)),
37
- ctx.on('llm/stream', (options, next) => handleStream(options, next, record)),
38
- ctx.on('tools/pre-execute', (exec, next) => handlePreExecute(exec, next, record)),
39
- ctx.on('tools/execute', (exec, next) => handleExecute(exec, next, record)),
40
- ...PLUGIN_EVENT_NAMES.map((name) => ctx.on(name, (payload) => handlePluginEvent(name, payload, record))),
41
- ]
32
+ return [
33
+ ctx.on('session/event', (session, event) => handleSessionEvent(session, event, record)),
34
+ ctx.on('agent/status', (payload) => handleStatus(payload, record)),
35
+ ctx.on('llm/stream', (options, next) => handleStream(options, next, record)),
36
+ ctx.on('tools/pre-execute', (exec, next) => handlePreExecute(exec, next, record)),
37
+ ctx.on('tools/execute', (exec, next) => handleExecute(exec, next, record)),
38
+ ...PLUGIN_EVENT_NAMES.map((name) => ctx.on(name, (payload) => handlePluginEvent(name, payload, record))),
39
+ ];
42
40
  }
43
-
44
41
  /** 插件事件名 → 插件名(前缀补 dsh-,如 task-reliability → dsh-task-reliability)。 */
45
42
  function pluginNameOf(name) {
46
- const prefix = name.split('/')[0]
47
- return prefix === '' ? name : `dsh-${prefix}`
43
+ const prefix = name.split('/')[0];
44
+ return prefix === '' ? name : `dsh-${prefix}`;
48
45
  }
49
-
50
46
  /** 插件事件名 → 事件名(去掉前缀,如 task-reliability/intervention → intervention)。 */
51
47
  function eventNameOf(name) {
52
- const parts = name.split('/')
53
- return parts.length > 1 ? parts.slice(1).join('/') : name
48
+ const parts = name.split('/');
49
+ return parts.length > 1 ? parts.slice(1).join('/') : name;
54
50
  }
55
-
56
51
  /** 插件事件 payload → 参数摘要(除 sessionId/action/reason 外的字段;字符串截断防膨胀)。 */
57
52
  function summarizeParams(payload) {
58
- const params = {}
59
- for (const [key, value] of Object.entries(payload ?? {})) {
60
- if (key === 'sessionId' || key === 'action' || key === 'reason') continue
61
- if (typeof value === 'string') params[key] = truncate(value)
62
- else if (typeof value === 'number' || typeof value === 'boolean') params[key] = value
63
- }
64
- return params
53
+ const params = {};
54
+ for (const [key, value] of Object.entries(payload ?? {})) {
55
+ if (key === 'sessionId' || key === 'action' || key === 'reason')
56
+ continue;
57
+ if (typeof value === 'string')
58
+ params[key] = truncate(value);
59
+ else if (typeof value === 'number' || typeof value === 'boolean')
60
+ params[key] = value;
61
+ }
62
+ return params;
65
63
  }
66
-
67
64
  /**
68
65
  * 插件事件 → plugin_event 审计事件(与现有事件同时间线/同过滤/同导出)。
69
66
  * 无 sessionId 的插件事件不记录(与 agent_status 等一致)。
70
67
  */
71
68
  function handlePluginEvent(name, payload, record) {
72
- const sessionId = payload?.sessionId
73
- if (typeof sessionId !== 'string' || sessionId === '') return
74
- const { type, data } = buildPluginEventData(name, payload)
75
- record({ type, sessionId, data })
69
+ const sessionId = payload?.sessionId;
70
+ if (typeof sessionId !== 'string' || sessionId === '')
71
+ return;
72
+ const { type, data } = buildPluginEventData(name, payload);
73
+ record({ type, sessionId, data });
76
74
  }
77
-
78
75
  function buildPluginEventData(name, payload) {
79
- const data = {
80
- plugin: pluginNameOf(name),
81
- event: eventNameOf(name),
82
- action: truncate(String(payload?.action ?? '')),
83
- reason: truncate(String(payload?.reason ?? '')),
84
- params: summarizeParams(payload),
85
- }
86
- const error = typeof payload?.error === 'string' && payload.error !== '' ? truncate(payload.error) : null
87
- if (error !== null) data.error = error
88
- return { type: error !== null ? 'plugin_error' : 'plugin_event', data }
76
+ const data = {
77
+ plugin: pluginNameOf(name),
78
+ event: eventNameOf(name),
79
+ action: truncate(String(payload?.action ?? '')),
80
+ reason: truncate(String(payload?.reason ?? '')),
81
+ params: summarizeParams(payload),
82
+ };
83
+ const error = typeof payload?.error === 'string' && payload.error !== '' ? truncate(payload.error) : null;
84
+ if (error !== null)
85
+ data.error = error;
86
+ return { type: error !== null ? 'plugin_error' : 'plugin_event', data };
89
87
  }
90
-
91
88
  /**
92
89
  * session/event → user_message 事件(会话标题来源)。
93
90
  * 轨迹回放面板需要"对话可读标题"而非 UUID:从每个会话真实用户的首条
@@ -95,163 +92,175 @@ function buildPluginEventData(name, payload) {
95
92
  * title。不作为独立存储字段,走现有事件通路,重启后自然恢复。
96
93
  */
97
94
  function handleSessionEvent(session, event, record) {
98
- if (event === null || typeof event !== 'object' || event.type !== 'user/message') return
99
- const message = event.data
100
- if (isPluginMessage(message)) return
101
- const sessionId = session?.id
102
- if (typeof sessionId !== 'string' || sessionId === '') return
103
- const text = userTextOf(message)
104
- if (text === '') return
105
- record({ type: 'user_message', sessionId, data: { text: truncate(text) } })
95
+ if (event === null || typeof event !== 'object' || event.type !== 'user/message')
96
+ return;
97
+ const message = event.data;
98
+ if (isPluginMessage(message))
99
+ return;
100
+ const sessionId = session?.id;
101
+ if (typeof sessionId !== 'string' || sessionId === '')
102
+ return;
103
+ const text = userTextOf(message);
104
+ if (text === '')
105
+ return;
106
+ record({ type: 'user_message', sessionId, data: { text: truncate(text) } });
106
107
  }
107
-
108
108
  /** 是否为插件注入的消息(非真实用户输入,不作为标题)。 */
109
109
  function isPluginMessage(message) {
110
- const source = message?.source
111
- return source !== null && typeof source === 'object' && source.kind === 'plugin'
110
+ const source = message?.source;
111
+ return source !== null && typeof source === 'object' && source.kind === 'plugin';
112
112
  }
113
-
114
113
  /** 从 user message 提取文本(content 中全部 text block 拼接)。 */
115
114
  function userTextOf(message) {
116
- if (message === null || typeof message !== 'object') return ''
117
- const content = message.content
118
- if (!Array.isArray(content)) return ''
119
- const parts = []
120
- for (const block of content) {
121
- if (block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') {
122
- parts.push(block.text)
115
+ if (message === null || typeof message !== 'object')
116
+ return '';
117
+ const content = message.content;
118
+ if (!Array.isArray(content))
119
+ return '';
120
+ const parts = [];
121
+ for (const block of content) {
122
+ if (block !== null &&
123
+ typeof block === 'object' &&
124
+ block.type === 'text' &&
125
+ typeof block.text === 'string') {
126
+ parts.push(block.text);
127
+ }
123
128
  }
124
- }
125
- return parts.join(' ').trim()
129
+ return parts.join(' ').trim();
126
130
  }
127
-
128
131
  /** agent/status → agent_status 事件(含顶层/子代理标记)。 */
129
132
  function handleStatus(payload, record) {
130
- const agent = payload?.agent
131
- if (agent === null || typeof agent !== 'object') return
132
- record({
133
- type: 'agent_status',
134
- sessionId: typeof agent.id === 'string' ? agent.id : '',
135
- data: { status: String(payload?.status ?? ''), agentType: agentTypeOf(agent) },
136
- })
133
+ const agent = payload?.agent;
134
+ if (agent === null || typeof agent !== 'object')
135
+ return;
136
+ record({
137
+ type: 'agent_status',
138
+ sessionId: typeof agent.id === 'string' ? agent.id : '',
139
+ data: { status: String(payload?.status ?? ''), agentType: agentTypeOf(agent) },
140
+ });
137
141
  }
138
-
139
142
  /** llm/stream → 包装流(同步返回;无 sessionId 时原样透传)。 */
140
143
  function handleStream(options, next, record) {
141
- const sessionId = typeof options?.sessionId === 'string' ? options.sessionId : ''
142
- if (sessionId === '') return next()
143
- const stream = next()
144
- return wrapStream(sessionId, stream, record)
144
+ const sessionId = typeof options?.sessionId === 'string' ? options.sessionId : '';
145
+ if (sessionId === '')
146
+ return next();
147
+ const stream = next();
148
+ return wrapStream(sessionId, stream, record);
145
149
  }
146
-
147
150
  /** tools/pre-execute → tool_call 事件(透传 next)。 */
148
151
  async function handlePreExecute(exec, next, record) {
149
- if (exec !== null && typeof exec === 'object' && typeof exec.agent?.id === 'string') {
150
- record({
151
- type: 'tool_call',
152
- sessionId: exec.agent.id,
153
- data: { name: String(exec.name ?? ''), args: summarizeArguments(exec.arguments) },
154
- })
155
- }
156
- return next()
152
+ if (exec !== null && typeof exec === 'object' && typeof exec.agent?.id === 'string') {
153
+ record({
154
+ type: 'tool_call',
155
+ sessionId: exec.agent.id,
156
+ data: { name: String(exec.name ?? ''), args: summarizeArguments(exec.arguments) },
157
+ });
158
+ }
159
+ return next();
157
160
  }
158
-
159
161
  /** tools/execute → tool_result 事件(透传 next 结果)。 */
160
162
  async function handleExecute(exec, next, record) {
161
- const sessionId = exec?.agent?.id
162
- const name = exec?.name
163
- if (typeof sessionId !== 'string' || sessionId === '') return next()
164
- const startedAt = Date.now()
165
- const result = await next()
166
- record({
167
- type: 'tool_result',
168
- sessionId,
169
- data: {
170
- name: String(name ?? ''),
171
- ok: isToolOk(result),
172
- ms: Date.now() - startedAt,
173
- },
174
- })
175
- return result
163
+ const sessionId = exec?.agent?.id;
164
+ const name = exec?.name;
165
+ if (typeof sessionId !== 'string' || sessionId === '')
166
+ return next();
167
+ const startedAt = Date.now();
168
+ const result = await next();
169
+ record({
170
+ type: 'tool_result',
171
+ sessionId,
172
+ data: {
173
+ name: String(name ?? ''),
174
+ ok: isToolOk(result),
175
+ ms: Date.now() - startedAt,
176
+ },
177
+ });
178
+ return result;
176
179
  }
177
-
178
180
  /** 顶层/子代理标记(白名单化:任何子代理标记命中即 subagent)。 */
179
181
  function agentTypeOf(agent) {
180
- const header = agent.session?.header
181
- if (header === undefined || header === null) return 'unknown'
182
- if (header.origin === 'subagent') return 'subagent'
183
- if (typeof header.delegationDepth === 'number' && header.delegationDepth > 0) return 'subagent'
184
- return typeof agent.options?.subagentDepth === 'number' && agent.options.subagentDepth > 0 ? 'subagent' : 'top'
182
+ const header = agent.session?.header;
183
+ if (header === undefined || header === null)
184
+ return 'unknown';
185
+ if (header.origin === 'subagent')
186
+ return 'subagent';
187
+ if (typeof header.delegationDepth === 'number' && header.delegationDepth > 0)
188
+ return 'subagent';
189
+ return typeof agent.options?.subagentDepth === 'number' && agent.options.subagentDepth > 0 ? 'subagent' : 'top';
185
190
  }
186
-
187
191
  /** 工具结果 ok 判定:非对象视为成功;error 字段非空视为失败。 */
188
192
  function isToolOk(result) {
189
- if (result === null || typeof result !== 'object') return true
190
- const error = result.error
191
- if (error === undefined || error === null) return true
192
- return typeof error === 'object' && Object.keys(error).length === 0
193
+ if (result === null || typeof result !== 'object')
194
+ return true;
195
+ const error = result.error;
196
+ if (error === undefined || error === null)
197
+ return true;
198
+ return typeof error === 'object' && Object.keys(error).length === 0;
193
199
  }
194
-
195
200
  /** 参数摘要:键列表(上限)+ 主要文本参数摘要(上限),防审计膨胀。 */
196
201
  function summarizeArguments(args) {
197
- if (args === null || typeof args !== 'object') return { keys: [] }
198
- const keys = Object.keys(args).slice(0, MAX_ARG_KEYS)
199
- const summary = textSummaryOf(args)
200
- return summary === '' ? { keys } : { keys, summary }
202
+ if (args === null || typeof args !== 'object')
203
+ return { keys: [] };
204
+ const keys = Object.keys(args).slice(0, MAX_ARG_KEYS);
205
+ const summary = textSummaryOf(args);
206
+ return summary === '' ? { keys } : { keys, summary };
201
207
  }
202
-
203
208
  /** 主要文本参数(command/message/content/question 等)的截断摘要。 */
204
209
  function textSummaryOf(args) {
205
- for (const key of ['command', 'message', 'content', 'question', 'description', 'prompt']) {
206
- const value = args[key]
207
- if (typeof value === 'string' && value !== '') return truncate(value)
208
- }
209
- return ''
210
+ for (const key of ['command', 'message', 'content', 'question', 'description', 'prompt']) {
211
+ const value = args[key];
212
+ if (typeof value === 'string' && value !== '')
213
+ return truncate(value);
214
+ }
215
+ return '';
210
216
  }
211
-
212
217
  /** 截断长文本(保留首段,去换行)。 */
213
218
  function truncate(text) {
214
- const oneLine = text.split('\n')[0].trim()
215
- return oneLine.length > MAX_TEXT_LEN ? `${oneLine.slice(0, MAX_TEXT_LEN)}…` : oneLine
219
+ const oneLine = text.split('\n')[0].trim();
220
+ return oneLine.length > MAX_TEXT_LEN ? `${oneLine.slice(0, MAX_TEXT_LEN)}…` : oneLine;
216
221
  }
217
-
218
222
  /** 包装 llm 流:透传全部 chunk,记录开始/结束/错误统计。 */
219
223
  function wrapStream(sessionId, stream, record) {
220
- let chunks = 0
221
- let chars = 0
222
- const startedAt = Date.now()
223
- record({ type: 'llm_stream', sessionId, data: { phase: 'start' } })
224
- return (async function* () {
225
- try {
226
- for await (const chunk of stream) {
227
- chunks += 1
228
- if (chunk !== null && typeof chunk === 'object' && typeof chunk.text === 'string') {
229
- chars += chunk.text.length
224
+ let chunks = 0;
225
+ let chars = 0;
226
+ const startedAt = Date.now();
227
+ record({ type: 'llm_stream', sessionId, data: { phase: 'start' } });
228
+ return (async function* () {
229
+ try {
230
+ for await (const chunk of stream) {
231
+ chunks += 1;
232
+ if (chunk !== null && typeof chunk === 'object' && typeof chunk.text === 'string') {
233
+ chars += chunk.text.length;
234
+ }
235
+ yield chunk;
236
+ }
237
+ record({
238
+ type: 'llm_stream',
239
+ sessionId,
240
+ data: streamSummary('end', startedAt, chunks, chars),
241
+ });
230
242
  }
231
- yield chunk
232
- }
233
- record({
234
- type: 'llm_stream',
235
- sessionId,
236
- data: streamSummary('end', startedAt, chunks, chars),
237
- })
238
- } catch (error) {
239
- record({
240
- type: 'llm_stream',
241
- sessionId,
242
- data: streamSummary('error', startedAt, chunks, chars, error),
243
- })
244
- throw error
245
- }
246
- })()
243
+ catch (error) {
244
+ record({
245
+ type: 'llm_stream',
246
+ sessionId,
247
+ data: streamSummary('error', startedAt, chunks, chars, error),
248
+ });
249
+ throw error;
250
+ }
251
+ })();
247
252
  }
248
-
249
253
  /** 流结束/错误统计(错误消息截断)。 */
250
254
  function streamSummary(phase, startedAt, chunks, chars, error) {
251
- const summary = { phase, chunks, chars, ms: Date.now() - startedAt }
252
- if (error !== undefined) {
253
- const message = error instanceof Error ? error.message : String(error)
254
- summary.message = message.length > MAX_TEXT_LEN ? `${message.slice(0, MAX_TEXT_LEN)}…` : message
255
- }
256
- return summary
255
+ const summary = {
256
+ phase,
257
+ chunks,
258
+ chars,
259
+ ms: Date.now() - startedAt,
260
+ };
261
+ if (error !== undefined) {
262
+ const message = error instanceof Error ? error.message : String(error);
263
+ summary.message = message.length > MAX_TEXT_LEN ? `${message.slice(0, MAX_TEXT_LEN)}…` : message;
264
+ }
265
+ return summary;
257
266
  }