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 +1 -1
- package/README.zh.md +1 -1
- package/index.mjs +1 -1
- package/lib/client.js +99 -7
- package/lib/core/delegation.mjs +52 -2
- package/lib/core/dispatch-schema.mjs +36 -0
- package/lib/core/dispatch-tool.mjs +17 -9
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
|
421
|
-
const
|
|
422
|
-
|
|
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
|
-
|
|
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) {
|
package/lib/core/delegation.mjs
CHANGED
|
@@ -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
|
-
// 之前读子 session
|
|
38
|
-
|
|
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
|
|
298
|
-
// 在 dispose 之前读子 session,保证 measure 拿到完整 surface。
|
|
304
|
+
// 仅 completed 结算时测量;dispose 前读子 session 保证事件流完整。
|
|
299
305
|
if (result.stopReason === 'completed') {
|
|
300
|
-
|
|
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
|
-
//
|
|
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
|
|
310
|
-
//
|
|
316
|
+
// 结算 meta:elapsedMs 自 t0;stopReason 取底层值(此分支必 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.
|
|
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",
|