dsh-subagent-profile 0.3.1 → 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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <div align="center">
5
5
  <b style="font-size: 1.15em;">Subagent dispatch, profiled — the right agent for the right task (preset / model / reasoning effort)</b><br /><br />
6
6
  <img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" />
7
- <img alt="Version: v0.3.1" src="https://img.shields.io/badge/Version-v0.3.1-blue.svg" />
7
+ <img alt="Version: v0.3.2" src="https://img.shields.io/badge/Version-v0.3.2-blue.svg" />
8
8
  <img alt="npm" src="https://img.shields.io/npm/v/dsh-subagent-profile.svg" />
9
9
  <img alt="DSH" src="https://img.shields.io/badge/DSH-0.1.0--rc.6%20~%200.1.1--rc.2-blue.svg" />
10
10
  </div>
package/README.zh.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <div align="center">
5
5
  <b style="font-size: 1.15em;">子 Agent 派发方案化插件 —— 用对的人(预设 / 模型 / 推理强度)干对的事</b><br /><br />
6
6
  <img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" />
7
- <img alt="Version: v0.3.1" src="https://img.shields.io/badge/Version-v0.3.1-blue.svg" />
7
+ <img alt="Version: v0.3.2" src="https://img.shields.io/badge/Version-v0.3.2-blue.svg" />
8
8
  <img alt="npm" src="https://img.shields.io/npm/v/dsh-subagent-profile.svg" />
9
9
  <img alt="DSH" src="https://img.shields.io/badge/DSH-0.1.0--rc.6%20~%200.1.1--rc.2-blue.svg" />
10
10
  </div>
package/index.mjs CHANGED
@@ -106,7 +106,7 @@ function profileSectionText(store, gate, context) {
106
106
  return `- ${p.id}: ${desc}${p.preset !== undefined ? ` (preset: ${p.preset})` : ''}`;
107
107
  });
108
108
  if (rows.length === 0) return '';
109
- const note = '- 别把 1-2 步即可自查/可搜完的小事委派出去 —— 几分钟内能自查完的直接做。';
109
+ const note = '- 选择方案时先匹配任务复杂度与方案描述的能力边界,成本只在能力都胜任的方案之间比较——复杂任务不得为省 token 改用能力不足的便宜方案。\n- 别把 1-2 步即可自查/可搜完的小事委派出去 —— 几分钟内能自查完的直接做。';
110
110
  return `Available dispatch profiles (dispatch.profile):\n${rows.join('\n')}\n${note}`;
111
111
  }
112
112
 
package/lib/client.js CHANGED
@@ -89,6 +89,10 @@ const CSS = [
89
89
  '.sap-switch input:checked::before{left:18px}',
90
90
  '.sap-toolview{flex-direction:column;align-items:flex-start;gap:6px;padding:4px 8px;display:flex}',
91
91
  '.sap-toolText{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word}',
92
+ '.sap-toolSummary{align-items:center;gap:6px;flex-wrap:wrap;display:flex}',
93
+ '.sap-detailToggle{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);background:0 0;border-radius:10px;font:inherit;font-size:11px;line-height:16px;height:20px;padding:0 8px;cursor:pointer;flex:none;align-items:center;display:inline-flex}',
94
+ '.sap-detailToggle:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}',
95
+ '.sap-toolDetail{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);display:flex}',
92
96
  '.sap-toolModes{flex-wrap:wrap;gap:4px 16px;display:flex}',
93
97
  '.sap-toolGroups{flex-direction:column;gap:8px;display:flex}',
94
98
  '.sap-toolLayerCard{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px}',
@@ -119,6 +123,8 @@ const ZH = {
119
123
  chipDisabled: '已禁用',
120
124
  ignoredMark: '已忽略',
121
125
  toolLabel: '派发子 Agent',
126
+ detailOpen: '详情',
127
+ detailClose: '收起',
122
128
  cont: 'continuable 子 Agent',
123
129
  canContinue: '可续跑',
124
130
  toolBackground: '后台任务',
@@ -126,6 +132,12 @@ const ZH = {
126
132
  toolRunning: '运行中',
127
133
  tierZh: { cheap: '省 token(cheap)', balanced: '均衡(balanced)', premium: '高成本(premium)' },
128
134
  stopReasonZh: { completed: '完成', 'max-tokens': '超出预算', aborted: '已中止', refusal: '已拒绝', error: '出错' },
135
+ cacheHit: '缓存命中',
136
+ usageInput: '输入',
137
+ usageOutput: '输出',
138
+ usageCacheRead: '缓存读',
139
+ usageCacheWrite: '缓存写',
140
+ usageReasoning: '推理',
129
141
  persistFail: '已保存但未持久化',
130
142
  persistFailHint: '(磁盘写入未成功,重启 dsh web 后该改动可能丢失)'
131
143
  }
@@ -229,7 +241,7 @@ function readDispatchRequest(block) {
229
241
  // 生效值来源——结构化 value 只存在于执行局部(dsh-tools 约定),且宿主未
230
242
  // 定义 output.presentationMeta(block.meta 为空)。解析成功返回
231
243
  // {kind, id, fields, ignored};任何一步不符预期返回 null,调用方回退请求值。
232
- const DISPATCH_RESULT_KEYS = ['profile', 'preset', 'provider', 'model', 'reasoningEffort', 'tokenTier', 'childTotalTokens', 'elapsedMs', 'stopReason']
244
+ const DISPATCH_RESULT_KEYS = ['profile', 'preset', 'provider', 'model', 'reasoningEffort', 'tokenTier', 'childTotalTokens', 'childUsage', 'elapsedMs', 'stopReason']
233
245
  function parseDispatchText(text) {
234
246
  if (typeof text !== 'string') return null
235
247
  const firstLine = text.split('\n')[0] ?? ''
@@ -322,6 +334,65 @@ function observabilitySuffix(result, block) {
322
334
  return out
323
335
  }
324
336
 
337
+ // 把 childUsage 的(render 行 JSON 字符串或 block.meta 对象)归一为五段对象;
338
+ // 非 object / 非 number 段忽略,无任何有效段返回 null。可选字段(缓存读/写/推理)
339
+ // 缺值省略键,与 host 侧 collectChildUsage 输出对齐。
340
+ function parseChildUsage(raw) {
341
+ if (raw === undefined || raw === null || raw === '') return null
342
+ let usage = raw
343
+ if (typeof raw === 'string') {
344
+ try { usage = JSON.parse(raw) } catch { return null }
345
+ }
346
+ if (typeof usage !== 'object' || Array.isArray(usage)) return null
347
+ const out = {}
348
+ let any = false
349
+ for (const key of ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens']) {
350
+ const value = usage[key]
351
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
352
+ out[key] = value
353
+ any = true
354
+ }
355
+ }
356
+ return any ? out : null
357
+ }
358
+
359
+ // 读取真实计费五段分解:result.fields.childUsage(render 行 JSON 串)优先,回退
360
+ // block.meta.childUsage(对象,向前兼容旧结果/未来 presentationMeta 投射)。
361
+ function usageFrom(result, block) {
362
+ if (result && result.fields && result.fields.childUsage !== undefined && result.fields.childUsage !== '') {
363
+ const parsed = parseChildUsage(result.fields.childUsage)
364
+ if (parsed !== null) return parsed
365
+ }
366
+ if (block && block.meta && typeof block.meta === 'object' && block.meta.childUsage !== undefined) {
367
+ return parseChildUsage(block.meta.childUsage)
368
+ }
369
+ return null
370
+ }
371
+
372
+ // 缓存命中后缀:childUsage.cacheReadTokens 存在且 >0 时追加「 · 缓存命中 M」到
373
+ // ≈N tokens 之后(前导分隔符与 tokensSuffix/observabilitySuffix 同口径,即使
374
+ // tokens 段缺失也不会粘在一起);无分解或缓存读为 0/缺失时返回空串。
375
+ function cacheHitSuffix(usage) {
376
+ if (usage === null) return ''
377
+ const cacheRead = usage.cacheReadTokens
378
+ return typeof cacheRead === 'number' && cacheRead > 0 ? ` · ${ZH.cacheHit} ${cacheRead}` : ''
379
+ }
380
+
381
+ // 五段分解行文案(纯函数,供卡片详情区渲染);无任何有效段返回空串。
382
+ function usageBreakdownText(usage) {
383
+ if (usage === null || typeof usage !== 'object') return ''
384
+ const parts = []
385
+ const push = (label, value) => {
386
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) parts.push(`${label} ${value}`)
387
+ }
388
+ push(ZH.usageInput, usage.inputTokens)
389
+ push(ZH.usageOutput, usage.outputTokens)
390
+ push(ZH.usageCacheRead, usage.cacheReadTokens)
391
+ push(ZH.usageCacheWrite, usage.cacheWriteTokens)
392
+ push(ZH.usageReasoning, usage.reasoningTokens)
393
+ return parts.join(' · ')
394
+ }
395
+
325
396
  // ── dispatch 工具卡(React 依赖经参数注入)─────────────────────────────────
326
397
 
327
398
  // 生效值:结果解析成功用结果(优先),字段缺失回退请求值。
@@ -409,7 +480,7 @@ function makeChip(el) {
409
480
  return chip
410
481
  }
411
482
 
412
- function makeDispatchToolview(el, chip) {
483
+ function makeDispatchToolview(el, chip, useState) {
413
484
  function DispatchToolview(props) {
414
485
  const block = props && props.block
415
486
  const request = readDispatchRequest(block)
@@ -417,12 +488,33 @@ function makeDispatchToolview(el, chip) {
417
488
  const settled = !!(block && typeof block === 'object' && 'kind' in block)
418
489
  const { eff, ignored, kindLabel } = resolveDispatchState(request, result, settled)
419
490
  const chips = buildDispatchChips(el, chip, eff, result, block, ignored, request)
420
- const p = rawOrInherit(eff.provider)
421
- const m = rawOrInherit(eff.model)
422
- const text = `${ZH.toolLabel}:${kindLabel} · 方案=${profileZh(eff.profile)} · 预设=${presetZh(eff.preset)} · 提供方=${p} · 模型=${m} · 推理强度=${effortZh(eff.reasoningEffort)}${tokensSuffix(result, block)}${observabilitySuffix(result, block)}`
491
+ const usage = usageFrom(result, block)
492
+ const [open, setOpen] = useState(false)
493
+ // 摘要行只承载动态结果(成本/耗时/结束原因/缓存命中);静态配置(方案/预设/
494
+ // 提供方/模型/推理强度/档位)全部在 chip 行——避免同一组信息重复两遍。
495
+ const text = `${ZH.toolLabel}:${kindLabel}${tokensSuffix(result, block)}${cacheHitSuffix(usage)}${observabilitySuffix(result, block)}`
496
+ // 详情折叠入口:仅当存在真实计费分解(childUsage)时渲染;无分解数据
497
+ // (旧结果/无 usage 会话)不显示折叠区,向前兼容。
498
+ const detailToggle = usage === null ? null : el('button', {
499
+ type: 'button',
500
+ className: 'sap-detailToggle',
501
+ 'aria-expanded': open,
502
+ onClick: () => setOpen((prev) => !prev)
503
+ }, open ? ZH.detailClose : ZH.detailOpen)
504
+ // 无分解数据时保持原摘要行结构(零漂移);有分解时才加折叠入口与详情行。
505
+ const summary = usage === null
506
+ ? el('div', { className: 'sap-toolText' }, text)
507
+ : el('div', { className: 'sap-toolSummary' },
508
+ el('div', { className: 'sap-toolText' }, text),
509
+ detailToggle
510
+ )
511
+ const detailBody = open && usage !== null
512
+ ? el('div', { className: 'sap-toolDetail' }, usageBreakdownText(usage))
513
+ : null
423
514
  return el('div', { className: 'sap-toolview' },
424
515
  el('div', { className: 'sap-chips' }, chips),
425
- el('div', { className: 'sap-toolText' }, text)
516
+ summary,
517
+ detailBody
426
518
  )
427
519
  }
428
520
  return DispatchToolview
@@ -904,7 +996,7 @@ window.__ModuleLoader__.load({
904
996
  const inject = ['slots']
905
997
 
906
998
  const chip = makeChip(el)
907
- const DispatchToolview = makeDispatchToolview(el, chip)
999
+ const DispatchToolview = makeDispatchToolview(el, chip, useState)
908
1000
  const ProfilesSection = makeProfilesSection(el, api, chip, useState, useEffect, useCallback)
909
1001
 
910
1002
  function apply(ctx) {
@@ -7,6 +7,52 @@
7
7
 
8
8
  import { stopReasonError, withPartialText, textFrom } from './pure.mjs';
9
9
 
10
+ // 子 Agent 会话的真实计费 token 五段分解:累加 session.events 里每条
11
+ // assistant/message 事件携带的 provider usage(inputTokens / outputTokens /
12
+ // cacheReadTokens / cacheWriteTokens / reasoningTokens)。这是宿主 dsh-llm
13
+ // assembler 产出、dsh-session-stats 同源读取的真实计费口径,与
14
+ // childTotalTokens(tokenMeter 对 surface 的字符密度启发式估算)并存:前者是
15
+ // provider 实际报告的用量,后者是估算值,二者语义不同、互不替代。任何字段缺失
16
+ // 按 0 累加;可选字段(缓存读/写/推理)全程无一条事件报告时省略该键;无任何
17
+ // 事件带 usage 时返回 undefined(调用方不写 meta,向前兼容)。非 object / 非
18
+ // number 一律忽略(fail-soft),使累加绝不阻断派发结算。
19
+ //
20
+ // 只累加 assistant/message:assistant/chunk 的 usage 是同一 step 的早期分片、
21
+ // 随后会被该 step 的 message 事件替换——两者都累加会重复计数(若未来改成
22
+ // 前缀匹配或累加 chunk,此守卫必须同步改并补测试)。
23
+ export function collectChildUsage(session) {
24
+ if (session === undefined || session === null) return undefined;
25
+ const events = session.events;
26
+ if (!Array.isArray(events)) return undefined;
27
+ const totals = { inputTokens: 0, outputTokens: 0 };
28
+ const optionalTotals = { cacheReadTokens: 0, cacheWriteTokens: 0, reasoningTokens: 0 };
29
+ const seenOptional = new Set();
30
+ let anyUsage = false;
31
+ for (const event of events) {
32
+ if (event === null || typeof event !== 'object' || event.type !== 'assistant/message') continue;
33
+ const usage = event.data?.usage;
34
+ if (usage === null || typeof usage !== 'object' || Array.isArray(usage)) continue;
35
+ anyUsage = true;
36
+ for (const key of ['inputTokens', 'outputTokens']) {
37
+ const value = usage[key];
38
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) totals[key] += value;
39
+ }
40
+ for (const key of ['cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens']) {
41
+ const value = usage[key];
42
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
43
+ optionalTotals[key] += value;
44
+ seenOptional.add(key);
45
+ }
46
+ }
47
+ }
48
+ if (!anyUsage) return undefined;
49
+ const out = { inputTokens: totals.inputTokens, outputTokens: totals.outputTokens };
50
+ for (const key of ['cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens']) {
51
+ if (seenOptional.has(key)) out[key] = optionalTotals[key];
52
+ }
53
+ return out;
54
+ }
55
+
10
56
  // Settle one background one-shot run into a job outcome with the same
11
57
  // observability metadata the foreground path reports. Non-completed stop reasons
12
58
  // become failed (aborted => killed, shipped vocabulary) with partial output
@@ -34,11 +80,15 @@ export async function settleStart(start, signal, meta, prune = (blocks) => block
34
80
  };
35
81
  }
36
82
  // 仅 completed 结算时测量(非 completed 走上方失败分支,不测);在 dispose
37
- // 之前读子 sessionmeasureChild 缺失/失败返回 undefined → 省略字段(fail-soft)。
38
- const childTotalTokens = measureChild(run.localAgent?.session);
83
+ // 之前读子 session,保证测量拿到完整事件流。measureChild 缺失/失败返回
84
+ // undefined 省略字段(fail-soft);childUsage 同样只在可累加时携带。
85
+ const childSession = run.localAgent?.session;
86
+ const childTotalTokens = measureChild(childSession);
87
+ const childUsage = collectChildUsage(childSession);
39
88
  const metaOut = {
40
89
  ...meta,
41
90
  ...(childTotalTokens !== undefined ? { childTotalTokens } : {}),
91
+ ...(childUsage !== undefined ? { childUsage } : {}),
42
92
  elapsedMs: Date.now() - t0,
43
93
  stopReason: 'completed'
44
94
  };
@@ -27,6 +27,18 @@ export const DISPATCH_OUTPUT_SCHEMA = {
27
27
  reasoningEffort: { type: 'string' },
28
28
  tokenTier: { type: 'string', enum: ['cheap', 'balanced', 'premium'], description: '成本/深度分层(估算口径,非计费)。' },
29
29
  childTotalTokens: { type: 'number', description: '子 Agent 会话 token 估算总量(宿主启发式估算,非计费 usage token);后台结算经 job 结果携带,dispatch 结果不带。' },
30
+ childUsage: {
31
+ type: 'object',
32
+ additionalProperties: false,
33
+ description: '真实计费 token 五段分解(缓存读/写为可选);后台结算经 job 结果携带,dispatch 结果不带。',
34
+ properties: {
35
+ inputTokens: { type: 'number' },
36
+ outputTokens: { type: 'number' },
37
+ cacheReadTokens: { type: 'number' },
38
+ cacheWriteTokens: { type: 'number' },
39
+ reasoningTokens: { type: 'number' }
40
+ }
41
+ },
30
42
  elapsedMs: { type: 'number', description: '派发耗时(毫秒,execute 入口到结算时刻);后台结算经 job 结果携带,dispatch 结果不带。' },
31
43
  stopReason: { type: 'string', enum: ['completed', 'max-tokens', 'aborted', 'refusal', 'error'], description: '子 Agent 结束原因;后台结算经 job 结果携带,dispatch 结果不带。' },
32
44
  ignored: { type: 'array', items: { type: 'string' } }
@@ -45,6 +57,18 @@ export const DISPATCH_OUTPUT_SCHEMA = {
45
57
  reasoningEffort: { type: 'string' },
46
58
  tokenTier: { type: 'string', enum: ['cheap', 'balanced', 'premium'], description: '成本/深度分层(估算口径,非计费)。' },
47
59
  childTotalTokens: { type: 'number', description: '子 Agent 会话 token 估算总量(宿主启发式估算,非计费 usage token);仅前台/后台结算时携带,continuable 实际省略。' },
60
+ childUsage: {
61
+ type: 'object',
62
+ additionalProperties: false,
63
+ description: '真实计费 token 五段分解(缓存读/写为可选);仅前台/后台 completed 结算时携带,continuable 实际省略。',
64
+ properties: {
65
+ inputTokens: { type: 'number' },
66
+ outputTokens: { type: 'number' },
67
+ cacheReadTokens: { type: 'number' },
68
+ cacheWriteTokens: { type: 'number' },
69
+ reasoningTokens: { type: 'number' }
70
+ }
71
+ },
48
72
  elapsedMs: { type: 'number', description: '派发耗时(毫秒,execute 入口到结算时刻);continuable 不结算,实际省略。' },
49
73
  stopReason: { type: 'string', enum: ['completed', 'max-tokens', 'aborted', 'refusal', 'error'], description: '子 Agent 结束原因;continuable 不结算,实际省略。' },
50
74
  ignored: { type: 'array', items: { type: 'string' } }
@@ -62,6 +86,18 @@ export const DISPATCH_OUTPUT_SCHEMA = {
62
86
  reasoningEffort: { type: 'string' },
63
87
  tokenTier: { type: 'string', enum: ['cheap', 'balanced', 'premium'], description: '成本/深度分层(估算口径,非计费)。' },
64
88
  childTotalTokens: { type: 'number', description: '子 Agent 会话 token 估算总量(宿主启发式估算,非计费 usage token);仅前台 completed 结算时携带。' },
89
+ childUsage: {
90
+ type: 'object',
91
+ additionalProperties: false,
92
+ description: '真实计费 token 五段分解(缓存读/写为可选);前台 completed 结算时携带。',
93
+ properties: {
94
+ inputTokens: { type: 'number' },
95
+ outputTokens: { type: 'number' },
96
+ cacheReadTokens: { type: 'number' },
97
+ cacheWriteTokens: { type: 'number' },
98
+ reasoningTokens: { type: 'number' }
99
+ }
100
+ },
65
101
  elapsedMs: { type: 'number', description: '派发耗时(毫秒,execute 入口到结算时刻);前台 completed 结算时携带。' },
66
102
  stopReason: { type: 'string', enum: ['completed', 'max-tokens', 'aborted', 'refusal', 'error'], description: '子 Agent 结束原因;前台 completed 结算时携带。' },
67
103
  ignored: { type: 'array', items: { type: 'string' } }
@@ -25,7 +25,7 @@ import { defineTool } from './shims.mjs';
25
25
  import { computeContinuableAllow, textFrom, stopReasonError, withPartialText, pruneBlocks, assertResultSchemaConsistency } from './pure.mjs';
26
26
  import { resolveWhitelist } from './whitelist.mjs';
27
27
  import { assertCostGuard } from './cost-guard.mjs';
28
- import { settleStart } from './delegation.mjs';
28
+ import { settleStart, collectChildUsage } from './delegation.mjs';
29
29
  import { DISPATCH_OUTPUT_SCHEMA } from './dispatch-schema.mjs';
30
30
 
31
31
  // --- 工具声明纯数据(从工厂提到模块级;defineTool 只读不改)----------------------
@@ -85,11 +85,17 @@ const DISPATCH_RENDER = (_args, value) => {
85
85
  const stopReason = typeof value.stopReason === 'string' && value.stopReason !== ''
86
86
  ? ` · stopReason=${value.stopReason}`
87
87
  : '';
88
+ // childUsage 是嵌套对象,render 行是扁平的 key=value,故 JSON 序列化为单段
89
+ // 供 client 侧 parseDispatchText 解析回对象(五段分解)。仅前台 completed 结算
90
+ // 携带;后台结算经 job 结果携带,continuable 不携带,故该段只在 foreground 行。
91
+ const usage = value.childUsage !== undefined && value.childUsage !== null && typeof value.childUsage === 'object'
92
+ ? ` · childUsage=${JSON.stringify(value.childUsage)}`
93
+ : '';
88
94
  const text = value.kind === 'background'
89
95
  ? `[dispatch] background job ${value.jobId} · profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}${tier}`
90
96
  : value.kind === 'continuable'
91
97
  ? `[dispatch] started subagent ${value.subagentId} · profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}${tier}`
92
- : `[dispatch] profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}${tier}${tokens}${elapsed}${stopReason}\n\n${value.output}`;
98
+ : `[dispatch] profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}${tier}${tokens}${usage}${elapsed}${stopReason}\n\n${value.output}`;
93
99
  return [{ type: 'text', text }];
94
100
  };
95
101
 
@@ -292,25 +298,27 @@ async function runForeground(meta, request, parent, deps, pruneResultOutput, t0)
292
298
  const run = await deps.subagents.start('profile', request);
293
299
  let result;
294
300
  let childTotalTokens;
301
+ let childUsage;
295
302
  try {
296
303
  result = await run.result;
297
- // 仅 completed 结算时测量(stopReason completed 走失败路径,不测);
298
- // 在 dispose 之前读子 session,保证 measure 拿到完整 surface。
304
+ // 仅 completed 结算时测量;dispose 前读子 session 保证事件流完整。
299
305
  if (result.stopReason === 'completed') {
300
- childTotalTokens = measureChildTokens(deps.getService, run.localAgent?.session);
306
+ const childSession = run.localAgent?.session;
307
+ childTotalTokens = measureChildTokens(deps.getService, childSession);
308
+ childUsage = collectChildUsage(childSession);
301
309
  }
302
310
  } finally {
303
311
  await run.dispose().catch(() => {});
304
312
  }
305
- // A non-'completed' stop reason is a failure; attach the child's
306
- // partial output text (withPartialText style).
313
+ // completed 视为失败,附子输出部分文本。
307
314
  const failure = stopReasonError(result);
308
315
  if (failure !== undefined) throw new Error(withPartialText(failure, result.output));
309
- // 结算 meta:elapsedMs execute 入口 t0 计;stopReason 取底层结算值
310
- // (此分支已达 return 必为 completed)。childTotalTokens 仍仅测量时携带。
316
+ // 结算 meta:elapsedMs t0stopReason 取底层值(此分支必 completed);
317
+ // childTotalTokens / childUsage 仅可测量时携带。
311
318
  const metaOut = {
312
319
  ...meta,
313
320
  ...(childTotalTokens !== undefined ? { childTotalTokens } : {}),
321
+ ...(childUsage !== undefined ? { childUsage } : {}),
314
322
  elapsedMs: Date.now() - t0,
315
323
  stopReason: result.stopReason
316
324
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-subagent-profile",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Dispatch one-shot subtasks to derived subagents with per-task overrides (preset/model/provider/reasoningEffort/persona/tool whitelist), a runtime-derived cost guard, a subagent-profiles service, observability metadata, and a web-GUI settings page plus a dispatch tool-call card.",
5
5
  "repository": {
6
6
  "type": "git",