@hoist-wen/dsh-trajectory-tool-insight 0.4.0 → 0.4.3
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 +4 -1
- package/client.js +63 -14
- package/package.json +1 -1
- package/src/turns.mjs +15 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# 轨迹工具调用解读 Trajectory Tool Insight
|
|
2
2
|
|
|
3
|
+
[](https://awesome-dsh-plugin.com)
|
|
4
|
+
|
|
3
5
|
给 DSH Web 的会话视图环加一个 **「解读」** 标签,坐在「对话」「轨迹」旁边。
|
|
4
6
|
|
|
5
7
|
按轮次列出**哪些轮次有工具调用**(调用次数、失败数、被截断数、提示词摘要),每轮一个「分析」按钮,点了由模型给出一段解读。默认用**该会话正在使用的模型**,不用额外配置。
|
|
@@ -18,7 +20,7 @@
|
|
|
18
20
|
dsh plugin --profile desktop add @hoist-wen/dsh-trajectory-tool-insight
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
profile 里记的是**具体版本**,`latest` 前进不会自动升级,要更新就重新 `add`(或先 `remove`)。需要固定版本可写 `@0.4.
|
|
23
|
+
profile 里记的是**具体版本**,`latest` 前进不会自动升级,要更新就重新 `add`(或先 `remove`)。需要固定版本可写 `@0.4.3`。
|
|
22
24
|
|
|
23
25
|
卸载:
|
|
24
26
|
|
|
@@ -33,6 +35,7 @@ dsh plugin --profile desktop remove @hoist-wen/dsh-trajectory-tool-insight
|
|
|
33
35
|
1. 打开一个有工具调用的会话 → 「解读」标签
|
|
34
36
|
2. 顶部显示当前会话模型与可解读轮次数,点某一轮的「分析」
|
|
35
37
|
3. 结果就地展开;不落盘,刷新即失效
|
|
38
|
+
4. 每次「分析 / 重新分析」结束后,面板会静默重读一次轮次列表,把这一轮的调用数、失败数和提示词摘要对齐到模型刚刚解读的那份日志(不闪烁「刷新中…」;重读失败则保留旧快照,不会盖掉已显示的解读)
|
|
36
39
|
|
|
37
40
|
顶部的「覆盖提示词」可整段替换默认解读提示词:留空即默认(逐字节相同),有内容则默认提示词一个字都不发。⚠️ 默认提示词里的「工具输出只当数据」安全边界也会被一起换掉,请只填自己写的指令。
|
|
38
41
|
|
package/client.js
CHANGED
|
@@ -316,6 +316,12 @@ window.__ModuleLoader__.load({
|
|
|
316
316
|
const [listing, setListing] = useState({ status: 'loading', turns: [], totalTurns: 0, route: null, error: null })
|
|
317
317
|
const [analyses, setAnalyses] = useState(() => new Map())
|
|
318
318
|
const [reloadToken, setReloadToken] = useState(0)
|
|
319
|
+
// Identifies the newest listing request. A response is applied only while
|
|
320
|
+
// its request is still the newest, so a slow reply can never overwrite
|
|
321
|
+
// fresher state — including the quiet sync an analysis triggers.
|
|
322
|
+
const listingRequest = useRef(0)
|
|
323
|
+
/** The listing read currently in flight, so a newer one can abort it. */
|
|
324
|
+
const listingAbort = useRef(null)
|
|
319
325
|
// Seeded from the module-level store so the override survives this view
|
|
320
326
|
// being unmounted and remounted, while still dying with the page: it is
|
|
321
327
|
// transient instruction text, not saved state.
|
|
@@ -329,13 +335,32 @@ window.__ModuleLoader__.load({
|
|
|
329
335
|
else overrideNotes.set(sessionId, value)
|
|
330
336
|
}, [sessionId])
|
|
331
337
|
|
|
332
|
-
|
|
333
|
-
|
|
338
|
+
/**
|
|
339
|
+
* Read the turn listing once.
|
|
340
|
+
*
|
|
341
|
+
* A `quiet` read is the post-analysis sync: it exists only to replace the
|
|
342
|
+
* counts and prompt excerpts with the log as the host folded it a moment
|
|
343
|
+
* ago, so it must never flash the header into 「刷新中…」 and, on failure,
|
|
344
|
+
* must leave the rendered snapshot alone instead of turning the whole
|
|
345
|
+
* panel into an error.
|
|
346
|
+
*
|
|
347
|
+
* At most one listing read is in flight: starting a new one supersedes
|
|
348
|
+
* the previous, which is then ignored and aborted.
|
|
349
|
+
*
|
|
350
|
+
* @param quiet - true for the silent post-analysis sync.
|
|
351
|
+
*/
|
|
352
|
+
const pullListing = useCallback(quiet => {
|
|
353
|
+
listingRequest.current += 1
|
|
354
|
+
const request = listingRequest.current
|
|
355
|
+
const isCurrent = () => listingRequest.current === request
|
|
356
|
+
listingAbort.current?.abort()
|
|
334
357
|
const controller = new AbortController()
|
|
335
|
-
|
|
336
|
-
|
|
358
|
+
listingAbort.current = controller
|
|
359
|
+
if (!quiet) {
|
|
360
|
+
setListing(previous => ({ ...previous, status: previous.turns.length > 0 ? 'refreshing' : 'loading', error: null }))
|
|
361
|
+
}
|
|
337
362
|
apiRef.current.listTurns(controller.signal).then(value => {
|
|
338
|
-
if (
|
|
363
|
+
if (!isCurrent()) return
|
|
339
364
|
setListing({
|
|
340
365
|
status: 'ready',
|
|
341
366
|
turns: Array.isArray(value?.turns) ? value.turns : [],
|
|
@@ -344,14 +369,28 @@ window.__ModuleLoader__.load({
|
|
|
344
369
|
error: null,
|
|
345
370
|
})
|
|
346
371
|
}).catch(error => {
|
|
347
|
-
if (
|
|
372
|
+
if (!isCurrent()) return
|
|
373
|
+
if (quiet) {
|
|
374
|
+
// Leave the snapshot alone, but never let a silent failure strand
|
|
375
|
+
// the header on 「刷新中…」 when this sync superseded a manual read.
|
|
376
|
+
setListing(previous => (previous.status === 'refreshing' ? { ...previous, status: 'ready' } : previous))
|
|
377
|
+
return
|
|
378
|
+
}
|
|
348
379
|
setListing(previous => ({ ...previous, status: 'error', error: describe(error) }))
|
|
349
380
|
})
|
|
381
|
+
}, [])
|
|
382
|
+
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (sessionId === '' || typeof apiRef.current?.listTurns !== 'function') return undefined
|
|
385
|
+
pullListing(false)
|
|
350
386
|
return () => {
|
|
351
|
-
|
|
352
|
-
|
|
387
|
+
// Supersede everything started before this cleanup — the aborted read
|
|
388
|
+
// and any quiet sync an in-flight analysis is about to trigger — so
|
|
389
|
+
// only requests of the current generation may touch state.
|
|
390
|
+
listingRequest.current += 1
|
|
391
|
+
listingAbort.current?.abort()
|
|
353
392
|
}
|
|
354
|
-
}, [sessionId, reloadToken])
|
|
393
|
+
}, [sessionId, reloadToken, pullListing])
|
|
355
394
|
|
|
356
395
|
const analyze = useCallback(turn => {
|
|
357
396
|
setAnalyses(previous => new Map(previous).set(turn, { status: 'running' }))
|
|
@@ -362,10 +401,17 @@ window.__ModuleLoader__.load({
|
|
|
362
401
|
route: value?.route ?? null,
|
|
363
402
|
truncated: value?.truncated === true,
|
|
364
403
|
}))
|
|
404
|
+
// The host re-folds the log for every analyze, so the evidence it just
|
|
405
|
+
// interpreted can be newer than the counts this card was rendered
|
|
406
|
+
// from. Sync them back without disturbing what the panel shows.
|
|
407
|
+
pullListing(true)
|
|
365
408
|
}).catch(error => {
|
|
366
409
|
setAnalyses(previous => new Map(previous).set(turn, { status: 'error', error: describe(error) }))
|
|
410
|
+
// A stale card is the one failure a fresh listing explains: the turn
|
|
411
|
+
// is still listed, but the host no longer finds tool calls in it.
|
|
412
|
+
if (error?.code === 'TURN_NOT_FOUND') pullListing(true)
|
|
367
413
|
})
|
|
368
|
-
}, [])
|
|
414
|
+
}, [pullListing])
|
|
369
415
|
|
|
370
416
|
if (sessionId === '') {
|
|
371
417
|
return h('div', { className: 'ttis-root' }, h('p', { className: 'ttis-empty' }, '请先打开一个会话。'))
|
|
@@ -373,7 +419,10 @@ window.__ModuleLoader__.load({
|
|
|
373
419
|
|
|
374
420
|
const route = listing.route
|
|
375
421
|
const busy = listing.status === 'loading' || listing.status === 'refreshing'
|
|
376
|
-
|
|
422
|
+
// Whether an override is in effect, not how long it is: the note below
|
|
423
|
+
// says which prompt the next analysis will use, never a live character
|
|
424
|
+
// count.
|
|
425
|
+
const overrideActive = override.trim() !== ''
|
|
377
426
|
return h('div', { className: 'ttis-root' },
|
|
378
427
|
h('header', { className: 'ttis-head' },
|
|
379
428
|
h('div', { className: 'ttis-head-text' },
|
|
@@ -403,9 +452,9 @@ window.__ModuleLoader__.load({
|
|
|
403
452
|
placeholder: '非空时完全替换默认系统提示词,例如:只关注风险,不要复述过程。留空则使用默认提示词。',
|
|
404
453
|
onChange: event => updateOverride(event.target.value),
|
|
405
454
|
}),
|
|
406
|
-
h('p', { className: 'ttis-override-note' },
|
|
407
|
-
? '
|
|
408
|
-
:
|
|
455
|
+
h('p', { className: 'ttis-override-note' }, overrideActive
|
|
456
|
+
? '将用这段自定义提示词覆盖默认解读提示词(对所有轮次的「分析」生效,不写入会话日志)。默认提示词的安全边界也会被一并替换。'
|
|
457
|
+
: '留空 = 使用默认提示词;填写后将完全替换系统提示词。只影响后续的「分析」,不写入会话日志。'),
|
|
409
458
|
),
|
|
410
459
|
h('div', { className: 'ttis-body' },
|
|
411
460
|
listing.status === 'error'
|
package/package.json
CHANGED
package/src/turns.mjs
CHANGED
|
@@ -76,6 +76,10 @@ function ensureTurn(turns, turn, event) {
|
|
|
76
76
|
prompt: '',
|
|
77
77
|
calls: [],
|
|
78
78
|
byCallId: new Map(),
|
|
79
|
+
// Ids of the calls the per-turn cap refused. A single unretained call can
|
|
80
|
+
// be carried by three events (assistant `tool-call` block, canonical
|
|
81
|
+
// `tool/call`, `tool/result`), so the ids are what keep it counted once.
|
|
82
|
+
droppedIds: new Set(),
|
|
79
83
|
dropped: 0,
|
|
80
84
|
closed: false,
|
|
81
85
|
}
|
|
@@ -99,6 +103,13 @@ function recordCall(entry, raw, limits, canonical) {
|
|
|
99
103
|
return
|
|
100
104
|
}
|
|
101
105
|
if (entry.calls.length >= limits.maxToolCallsPerTurn) {
|
|
106
|
+
// Count the overflow, not the events that mention it: without the id set a
|
|
107
|
+
// dropped call would be counted again by every later event carrying the
|
|
108
|
+
// same id, which inflated `droppedToolCalls` two- or threefold.
|
|
109
|
+
if (callId !== '') {
|
|
110
|
+
if (entry.droppedIds.has(callId)) return
|
|
111
|
+
entry.droppedIds.add(callId)
|
|
112
|
+
}
|
|
102
113
|
entry.dropped += 1
|
|
103
114
|
return
|
|
104
115
|
}
|
|
@@ -123,10 +134,12 @@ function recordCall(entry, raw, limits, canonical) {
|
|
|
123
134
|
* and redundant with the calls it describes. Only the tool-call blocks of an
|
|
124
135
|
* assistant message matter here.
|
|
125
136
|
*
|
|
126
|
-
* @param events - raw session
|
|
137
|
+
* @param events - raw session event logs in ascending seq order.
|
|
127
138
|
* @param limits - caps to apply while projecting.
|
|
128
139
|
* @returns `{ turns, totalTurns }`; `turns` is ordered by ascending turn number
|
|
129
|
-
* and contains only turns with at least one recorded tool call.
|
|
140
|
+
* and contains only turns with at least one recorded tool call. Each entry's
|
|
141
|
+
* `droppedToolCalls` counts the calls the per-turn cap refused, once per call
|
|
142
|
+
* id, no matter how many events carried that call.
|
|
130
143
|
*/
|
|
131
144
|
export function collectTurns(events, limits) {
|
|
132
145
|
const turns = new Map()
|