dsh-session-bridge 0.2.0 → 0.3.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/README.md +52 -7
- package/README.zh.md +35 -5
- package/dsh.plugin.json +2 -1
- package/lib/index.js +477 -41
- package/package.json +1 -1
- package/src/core.ts +215 -13
- package/src/monitor.ts +126 -1
- package/src/tools.ts +153 -16
package/src/tools.ts
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
foldMessages,
|
|
23
23
|
maxSeq,
|
|
24
24
|
resolveTargetCwd,
|
|
25
|
+
segmentsSince,
|
|
26
|
+
sessionEvents,
|
|
25
27
|
statusSnapshot,
|
|
26
28
|
titleOf,
|
|
27
29
|
userMessage,
|
|
@@ -30,7 +32,7 @@ import {
|
|
|
30
32
|
workspaceBySession,
|
|
31
33
|
} from './core.ts'
|
|
32
34
|
import type { BridgeRegistry } from './registry.ts'
|
|
33
|
-
import type { SessionMonitor, MonitorConfig, MonitorEntryState } from './monitor.ts'
|
|
35
|
+
import type { SessionMonitor, MonitorConfig, MonitorEntryState, CoTRule } from './monitor.ts'
|
|
34
36
|
|
|
35
37
|
type SessionIdBrand = { readonly __sessionIdBrand?: never }
|
|
36
38
|
|
|
@@ -236,7 +238,7 @@ function registerCreate(env: BridgeEnv): void {
|
|
|
236
238
|
|
|
237
239
|
let reply: ReturnType<typeof renderWait> | undefined
|
|
238
240
|
if (typeof args.prompt === 'string' && args.prompt.trim() !== '') {
|
|
239
|
-
const baseline = maxSeq(agent.session
|
|
241
|
+
const baseline = maxSeq(sessionEvents(agent.session))
|
|
240
242
|
agent.followup(userMessage(args.prompt.trim()))
|
|
241
243
|
env.registry.touch(sessionId)
|
|
242
244
|
if (args.waitForReply === true) {
|
|
@@ -294,7 +296,7 @@ function registerSend(env: BridgeEnv): void {
|
|
|
294
296
|
if (agent === undefined) {
|
|
295
297
|
throw new Error('session ' + JSON.stringify(args.sessionId) + ' is not live — call session_bridge_resume first to bring it online (or session_bridge_find to locate it)')
|
|
296
298
|
}
|
|
297
|
-
const baseline = maxSeq(agent.session
|
|
299
|
+
const baseline = maxSeq(sessionEvents(agent.session))
|
|
298
300
|
if (args.mode === 'steer') agent.steer(userMessage(args.message.trim()))
|
|
299
301
|
else agent.followup(userMessage(args.message.trim()))
|
|
300
302
|
const headerCwd = agent.session.header.cwd
|
|
@@ -346,7 +348,7 @@ function registerResume(env: BridgeEnv): void {
|
|
|
346
348
|
sessionId: args.sessionId,
|
|
347
349
|
alreadyLive: true,
|
|
348
350
|
...(existing.session.header.cwd === undefined ? {} : { cwd: existing.session.header.cwd }),
|
|
349
|
-
title: titleOf(existing.session
|
|
351
|
+
title: titleOf(sessionEvents(existing.session)) ?? null,
|
|
350
352
|
running: existing.status === 'running',
|
|
351
353
|
})
|
|
352
354
|
}
|
|
@@ -381,7 +383,7 @@ function registerResume(env: BridgeEnv): void {
|
|
|
381
383
|
sessionId: args.sessionId,
|
|
382
384
|
resumed: true,
|
|
383
385
|
...(headerCwd === undefined ? {} : { cwd: headerCwd }),
|
|
384
|
-
title: titleOf(handle.agent.session
|
|
386
|
+
title: titleOf(sessionEvents(handle.agent.session)) ?? null,
|
|
385
387
|
})
|
|
386
388
|
},
|
|
387
389
|
}))
|
|
@@ -392,17 +394,19 @@ interface WaitArgsTool {
|
|
|
392
394
|
sinceSeq?: number
|
|
393
395
|
timeoutMs?: number
|
|
394
396
|
requireTurnEnd?: boolean
|
|
397
|
+
waitFor?: 'reply' | 'segment'
|
|
395
398
|
}
|
|
396
399
|
|
|
397
400
|
function registerWait(env: BridgeEnv): void {
|
|
398
401
|
env.ctx.tools.register(defineTool({
|
|
399
402
|
name: 'session_bridge_wait',
|
|
400
|
-
description: 'Wait for a session next assistant
|
|
403
|
+
description: 'Wait for a session next assistant output: blocks (polling the session log) until a NEW assistant output appears after sinceSeq (default: the latest seq at call time). waitFor=reply returns as soon as a new assistant TEXT reply is readable; waitFor=segment returns as soon as any new COMPLETED output segment appears (an assistant/message step — text, reasoning, or tool-call turn), i.e. it does NOT wait for the whole turn, so you can observe the chain-of-thought/output paragraph by paragraph as it is produced. Returns the output summary, or timedOut/aborted when the deadline or caller cancellation ends the wait. Use it to consume output produced asynchronously by another session (e.g. a session you sent a message to, or one working on its own).',
|
|
401
404
|
parameters: {
|
|
402
405
|
sessionId: { type: 'string', required: true, description: 'Session id to wait on.' },
|
|
403
406
|
sinceSeq: { type: 'number', description: 'Only replies after this event seq count (default: latest seq at call time).' },
|
|
404
407
|
timeoutMs: { type: 'number', description: 'Wait budget in milliseconds (default 180000, max 3600000); timed out waits return the partial result instead of failing.' },
|
|
405
408
|
requireTurnEnd: { type: 'boolean', description: 'When true, wait for the reply turn/end to settle before returning (default false; false returns as soon as the reply text is readable).' },
|
|
409
|
+
waitFor: { type: 'string', enum: ['reply', 'segment'], description: 'reply (default) waits for a new assistant TEXT reply; segment waits for any new completed output segment (an assistant/message step, incl. reasoning/tool turns) and returns it immediately, without waiting for the whole turn.' },
|
|
406
410
|
},
|
|
407
411
|
output: {
|
|
408
412
|
schema: { type: 'object', additionalProperties: true },
|
|
@@ -424,15 +428,19 @@ function registerWait(env: BridgeEnv): void {
|
|
|
424
428
|
if (agent === undefined) {
|
|
425
429
|
throw new Error('session ' + JSON.stringify(args.sessionId) + ' is not live — call session_bridge_resume first (waiting requires a live session)')
|
|
426
430
|
}
|
|
427
|
-
// 默认 baseline =
|
|
428
|
-
//
|
|
429
|
-
//
|
|
431
|
+
// 默认 baseline = 当前最后一条(带文本的)assistant 行的 seq:让 wait 只等待
|
|
432
|
+
// 之后新出现的输出,避免把"已存在的输出"当成待等内容,同时不被文本后追加的
|
|
433
|
+
// 无文本中间块(推理尾块/工具结果)干扰。segment 模式下以最后一个已完成段落为界。
|
|
434
|
+
const waitSegment = args.waitFor === 'segment'
|
|
430
435
|
let baseline: number
|
|
431
436
|
if (typeof args.sinceSeq === 'number' && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0) {
|
|
432
437
|
baseline = args.sinceSeq
|
|
438
|
+
} else if (waitSegment) {
|
|
439
|
+
const segs = segmentsSince(sessionEvents(agent.session))
|
|
440
|
+
baseline = segs.length === 0 ? -1 : (segs[segs.length - 1]?.seq ?? -1)
|
|
433
441
|
} else {
|
|
434
442
|
let lastText = -1
|
|
435
|
-
for (const row of foldMessages(agent.session
|
|
443
|
+
for (const row of foldMessages(sessionEvents(agent.session))) {
|
|
436
444
|
if (row.text !== undefined) lastText = row.seq
|
|
437
445
|
}
|
|
438
446
|
baseline = lastText
|
|
@@ -443,6 +451,7 @@ function registerWait(env: BridgeEnv): void {
|
|
|
443
451
|
timeoutMs: clampTimeout(args.timeoutMs),
|
|
444
452
|
signal: exec.signal,
|
|
445
453
|
requireTurnEnd: args.requireTurnEnd === true,
|
|
454
|
+
...(waitSegment ? { waitForSegment: true } : {}),
|
|
446
455
|
})
|
|
447
456
|
env.registry.touch(args.sessionId)
|
|
448
457
|
return asJson({
|
|
@@ -454,6 +463,77 @@ function registerWait(env: BridgeEnv): void {
|
|
|
454
463
|
}))
|
|
455
464
|
}
|
|
456
465
|
|
|
466
|
+
interface SegmentsArgs {
|
|
467
|
+
sessionId: string
|
|
468
|
+
sinceSeq?: number
|
|
469
|
+
limit?: number
|
|
470
|
+
}
|
|
471
|
+
function registerSegments(env: BridgeEnv): void {
|
|
472
|
+
env.ctx.tools.register(defineTool({
|
|
473
|
+
name: 'session_bridge_segments',
|
|
474
|
+
description: 'Read the completed output segments of a session incrementally - each finished assistant step (one assistant/message paragraph), without waiting for the whole turn. Pass sinceSeq to page forward. Use this to watch a session produce its chain-of-thought / output paragraph by paragraph as each step finishes.',
|
|
475
|
+
parameters: {
|
|
476
|
+
sessionId: { type: 'string', required: true, description: 'Session id to read segments from (live or offline).' },
|
|
477
|
+
sinceSeq: { type: 'number', description: 'Only return completed segments after this event seq (paging cursor).' },
|
|
478
|
+
limit: { type: 'number', description: 'Maximum number of segments to return (default 10, max 50).' },
|
|
479
|
+
},
|
|
480
|
+
output: {
|
|
481
|
+
schema: { type: 'object', additionalProperties: true },
|
|
482
|
+
render: (_args, value) => {
|
|
483
|
+
const v = value as Record<string, unknown>
|
|
484
|
+
const segs = (v.segments as Array<Record<string, unknown>> | null) ?? []
|
|
485
|
+
if (segs.length === 0) return [{ type: 'text' as const, text: '(no completed segments)' }]
|
|
486
|
+
const lines = segs.map((sg) => {
|
|
487
|
+
const head = 'seg #' + String(sg.seq) + ' (turn ' + String(sg.turn) + ' step ' + String(sg.step) + ')'
|
|
488
|
+
const text = typeof sg.text === 'string' ? sg.text : ''
|
|
489
|
+
const reason = typeof sg.reasoning === 'string' && sg.reasoning !== '' ? ' [reasoning ' + sg.reasoning.length + ' chars]' : ''
|
|
490
|
+
const tools = Array.isArray(sg.toolCalls) && sg.toolCalls.length > 0 ? ' [tools: ' + sg.toolCalls.join(',') + ']' : ''
|
|
491
|
+
return head + tools + reason + ': ' + text.slice(0, 160)
|
|
492
|
+
})
|
|
493
|
+
return [{ type: 'text' as const, text: lines.join('\n') }]
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
async execute(args: SegmentsArgs) {
|
|
497
|
+
const agent = liveAgent(env, args.sessionId)
|
|
498
|
+
let events: readonly SessionEvent[]
|
|
499
|
+
let live: boolean
|
|
500
|
+
if (agent !== undefined) {
|
|
501
|
+
events = sessionEvents(agent.session)
|
|
502
|
+
live = true
|
|
503
|
+
} else {
|
|
504
|
+
try {
|
|
505
|
+
const persistence = env.ctx.sessionPersistence as unknown as { inspect(id: string): Promise<{ events: readonly SessionEvent[] }> }
|
|
506
|
+
const inspection = await persistence.inspect(args.sessionId)
|
|
507
|
+
events = inspection.events
|
|
508
|
+
live = false
|
|
509
|
+
} catch (error) {
|
|
510
|
+
throw new Error(String(error))
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const sinceSeq = typeof args.sinceSeq === 'number' && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0 ? args.sinceSeq : 0
|
|
514
|
+
const limit = clampLimit(args.limit, 10, 50)
|
|
515
|
+
const segs = segmentsSince(events, sinceSeq)
|
|
516
|
+
env.registry.touch(args.sessionId)
|
|
517
|
+
const nextCursorSeq = segs.length === 0 ? sinceSeq : (segs[segs.length - 1]?.seq ?? sinceSeq)
|
|
518
|
+
return asJson({
|
|
519
|
+
sessionId: args.sessionId,
|
|
520
|
+
live,
|
|
521
|
+
nextCursorSeq,
|
|
522
|
+
segments: segs.map((sg) => ({
|
|
523
|
+
seq: sg.seq,
|
|
524
|
+
time: sg.time,
|
|
525
|
+
turn: sg.turn,
|
|
526
|
+
step: sg.step,
|
|
527
|
+
openTurn: sg.openTurn,
|
|
528
|
+
...(sg.text === undefined ? {} : { text: sg.text }),
|
|
529
|
+
...(sg.reasoning === undefined ? {} : { reasoning: sg.reasoning }),
|
|
530
|
+
...(sg.toolCalls.length > 0 ? { toolCalls: sg.toolCalls } : {}),
|
|
531
|
+
})),
|
|
532
|
+
})
|
|
533
|
+
},
|
|
534
|
+
}))
|
|
535
|
+
}
|
|
536
|
+
|
|
457
537
|
interface ReadArgs {
|
|
458
538
|
sessionId: string
|
|
459
539
|
sinceSeq?: number
|
|
@@ -494,7 +574,7 @@ function registerRead(env: BridgeEnv): void {
|
|
|
494
574
|
let live: boolean
|
|
495
575
|
let cwd: string | undefined
|
|
496
576
|
if (agent !== undefined) {
|
|
497
|
-
events = agent.session
|
|
577
|
+
events = sessionEvents(agent.session)
|
|
498
578
|
live = true
|
|
499
579
|
cwd = agent.session.header.cwd
|
|
500
580
|
} else {
|
|
@@ -592,7 +672,7 @@ function registerFind(env: BridgeEnv): void {
|
|
|
592
672
|
const wsId = wsById.get(agent.id) ?? workspaceByPath(env.ctx, headerCwd)
|
|
593
673
|
items.push({
|
|
594
674
|
sessionId: agent.id,
|
|
595
|
-
title: titleOf(agent.session
|
|
675
|
+
title: titleOf(sessionEvents(agent.session)),
|
|
596
676
|
...(headerCwd === undefined ? {} : { cwd: headerCwd }),
|
|
597
677
|
...(wsId === undefined ? {} : { workspaceId: wsId }),
|
|
598
678
|
live: true,
|
|
@@ -698,6 +778,7 @@ export function registerBridgeTools(env: BridgeEnv): void {
|
|
|
698
778
|
registerSend(env)
|
|
699
779
|
registerResume(env)
|
|
700
780
|
registerWait(env)
|
|
781
|
+
registerSegments(env)
|
|
701
782
|
registerRead(env)
|
|
702
783
|
registerFind(env)
|
|
703
784
|
registerStatus(env)
|
|
@@ -716,6 +797,25 @@ interface MonitorStartArgs {
|
|
|
716
797
|
onStallSteer?: string
|
|
717
798
|
onOffTrackSteer?: string
|
|
718
799
|
label?: string
|
|
800
|
+
coRules?: Array<{ match?: string; field?: string; value?: string; action?: string; message?: string }>
|
|
801
|
+
cotMinHits?: number
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** 校验并规范化 coRules 参数为 CoTRule[];非法项抛清晰错误。 */
|
|
805
|
+
function parseCoRules(raw: Array<{ match?: string; field?: string; value?: string; action?: string; message?: string }>): CoTRule[] {
|
|
806
|
+
return raw.map((rule, index) => {
|
|
807
|
+
const match = rule.match === 'contains' || rule.match === 'not-contains' ? rule.match : (() => { throw new Error('coRules[' + index + '].match must be "contains" or "not-contains"') })()
|
|
808
|
+
const field = rule.field === 'reasoning' || rule.field === 'text' || rule.field === 'both' ? rule.field : (() => { throw new Error('coRules[' + index + '].field must be "reasoning", "text" or "both"') })()
|
|
809
|
+
const action = rule.action === 'steer' || rule.action === 'cancel' ? rule.action : (() => { throw new Error('coRules[' + index + '].action must be "steer" or "cancel"') })()
|
|
810
|
+
const value = typeof rule.value === 'string' && rule.value.trim() !== '' ? rule.value.trim() : (() => { throw new Error('coRules[' + index + '].value must be a non-empty string') })()
|
|
811
|
+
return {
|
|
812
|
+
match,
|
|
813
|
+
field,
|
|
814
|
+
action,
|
|
815
|
+
value,
|
|
816
|
+
...(typeof rule.message === 'string' && rule.message.trim() !== '' ? { message: rule.message.trim() } : {}),
|
|
817
|
+
}
|
|
818
|
+
})
|
|
719
819
|
}
|
|
720
820
|
|
|
721
821
|
function renderMonitorState(entry: MonitorEntryState): string[] {
|
|
@@ -723,6 +823,8 @@ function renderMonitorState(entry: MonitorEntryState): string[] {
|
|
|
723
823
|
lines.push(`monitoring ${entry.config.sessionId}${entry.config.label === undefined ? '' : ' ("' + entry.config.label + '")'}`)
|
|
724
824
|
lines.push(`interval: ${entry.config.intervalMs}ms | stalled: ${String(entry.config.stalledMs ?? 60000)}ms | maxStuck: ${String(entry.config.maxStuckCycles ?? 3)}`)
|
|
725
825
|
lines.push(`cycles: ${entry.cycles} | stuck: ${entry.stuckCount} | lastAction: ${entry.lastAction}`)
|
|
826
|
+
const cotCount = (entry.config.coTRules ?? []).length
|
|
827
|
+
if (cotCount > 0) lines.push(`coRules: ${cotCount} rule(s) (minHits ${String(entry.config.cotMinHits ?? 1)})`)
|
|
726
828
|
if (entry.lastNote !== '') lines.push(`note: ${entry.lastNote}`)
|
|
727
829
|
if (entry.done) lines.push('done: yes')
|
|
728
830
|
return lines
|
|
@@ -731,7 +833,7 @@ function renderMonitorState(entry: MonitorEntryState): string[] {
|
|
|
731
833
|
function registerMonitor(env: BridgeEnv): void {
|
|
732
834
|
env.ctx.tools.register(defineTool({
|
|
733
835
|
name: 'session_bridge_monitor_start',
|
|
734
|
-
description: 'Start a background watchdog on a main session: poll its progress at an interval, and automatically schedule — steer the session when it stalls (or, with useLlm, when it drifts off-track), cancel it when it stays stuck past maxStuckCycles, and stop when a done keyword appears while idle. This is the "monitor worker" that watches a main task thread and corrects/stops it. Uses session_bridge_status-style facts; pass sessionId of a live session. Returns the watchdog state.',
|
|
836
|
+
description: 'Start a background watchdog on a main session: poll its progress at an interval, and automatically schedule — steer the session when it stalls (or, with useLlm, when it drifts off-track), cancel it when it stays stuck past maxStuckCycles, and stop when a done keyword appears while idle. It can also enforce chain-of-thought rules via coRules: e.g. coRules=[{match:"not-contains",field:"reasoning",value:"I\'m",action:"cancel"}] stops the session the moment its live reasoning stops containing I\'m (evaluated on each poll while running). This is the "monitor worker" that watches a main task thread and corrects/stops it. Uses session_bridge_status-style facts; pass sessionId of a live session. Returns the watchdog state.',
|
|
735
837
|
parameters: {
|
|
736
838
|
sessionId: { type: 'string', required: true, description: 'Target main session id to watch (must be live).' },
|
|
737
839
|
intervalMs: { type: 'number', description: 'Poll interval in ms (default 10000, min 5000).' },
|
|
@@ -742,6 +844,14 @@ function registerMonitor(env: BridgeEnv): void {
|
|
|
742
844
|
onStallSteer: { type: 'string', description: 'Steer text injected on a stall/nudge (default: ask to summarize progress and continue).' },
|
|
743
845
|
onOffTrackSteer: { type: 'string', description: 'Steer text injected when LLM judges the task off-track (default: ask to return to the original goal).' },
|
|
744
846
|
label: { type: 'string', description: 'Optional human label for logs/display.' },
|
|
847
|
+
coRules: { type: 'array', items: { type: 'object', additionalProperties: false, properties: {
|
|
848
|
+
match: { type: 'string', required: true, enum: ['contains', 'not-contains'], description: 'contains = must include value in the chosen field; not-contains = must NOT include it.' },
|
|
849
|
+
field: { type: 'string', required: true, enum: ['reasoning', 'text', 'both'], description: 'Match on reasoning (chain-of-thought), text (reply), or both (either).' },
|
|
850
|
+
value: { type: 'string', required: true, description: 'The substring to match (non-empty).' },
|
|
851
|
+
action: { type: 'string', required: true, enum: ['steer', 'cancel'], description: 'steer injects a guiding message; cancel terminates the session.' },
|
|
852
|
+
message: { type: 'string', description: 'Custom steer text (default is a guidance prompt).' },
|
|
853
|
+
} }, description: 'Chain-of-thought rules: when a rule stays matched for cotMinHits consecutive polls (default 1), trigger its action. Example: [{match:"not-contains",field:"reasoning",value:"I\'m",action:"cancel"}] stops the session the moment its live reasoning no longer contains I\'m.' },
|
|
854
|
+
cotMinHits: { type: 'number', description: 'How many consecutive matched polls before a CoT rule fires (default 1 = immediately).' },
|
|
745
855
|
},
|
|
746
856
|
output: {
|
|
747
857
|
schema: { type: 'object', additionalProperties: true },
|
|
@@ -764,6 +874,8 @@ function registerMonitor(env: BridgeEnv): void {
|
|
|
764
874
|
...(typeof args.onStallSteer === 'string' && args.onStallSteer.trim() !== '' ? { onStallSteer: args.onStallSteer.trim() } : {}),
|
|
765
875
|
...(typeof args.onOffTrackSteer === 'string' && args.onOffTrackSteer.trim() !== '' ? { onOffTrackSteer: args.onOffTrackSteer.trim() } : {}),
|
|
766
876
|
...(typeof args.label === 'string' && args.label.trim() !== '' ? { label: args.label.trim() } : {}),
|
|
877
|
+
...(Array.isArray(args.coRules) && args.coRules.length > 0 ? { coTRules: parseCoRules(args.coRules) } : {}),
|
|
878
|
+
...(typeof args.cotMinHits === 'number' && Number.isInteger(args.cotMinHits) && args.cotMinHits >= 1 ? { cotMinHits: Math.floor(args.cotMinHits) } : {}),
|
|
767
879
|
}
|
|
768
880
|
const entry = env.monitor.start(config)
|
|
769
881
|
env.registry.touch(args.sessionId)
|
|
@@ -811,13 +923,32 @@ function registerMonitor(env: BridgeEnv): void {
|
|
|
811
923
|
}))
|
|
812
924
|
}
|
|
813
925
|
|
|
926
|
+
type StatusReasoning = 'none' | 'last' | 'live' | 'tail'
|
|
927
|
+
|
|
814
928
|
interface StatusArgs {
|
|
815
929
|
sessionId: string
|
|
816
930
|
stalledMsThreshold?: number
|
|
817
931
|
recent?: number
|
|
932
|
+
reasoning?: StatusReasoning
|
|
818
933
|
}
|
|
819
934
|
|
|
820
|
-
/**
|
|
935
|
+
/** 按 reasoning 选项裁剪快照里的思维链字段(避免入 token 时有 diff 语义差)。 */
|
|
936
|
+
function pruneReasoning(snapshot: BridgeStatusSnapshot, mode: StatusReasoning): BridgeStatusSnapshot {
|
|
937
|
+
if (mode === 'tail') return snapshot
|
|
938
|
+
const rest: BridgeStatusSnapshot = { ...snapshot }
|
|
939
|
+
delete (rest as { reasoningTail?: unknown }).reasoningTail
|
|
940
|
+
if (mode === 'none') {
|
|
941
|
+
delete (rest as { lastReasoning?: unknown }).lastReasoning
|
|
942
|
+
delete (rest as { liveReasoning?: unknown }).liveReasoning
|
|
943
|
+
} else if (mode === 'last') {
|
|
944
|
+
delete (rest as { liveReasoning?: unknown }).liveReasoning
|
|
945
|
+
} else { // 'live'
|
|
946
|
+
delete (rest as { lastReasoning?: unknown }).lastReasoning
|
|
947
|
+
}
|
|
948
|
+
return rest
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/** 渲染监控快照为一行摘要:运行态 + openTurn + 卡住/待处理 + 最新回复(+ 思维链预览)。 */
|
|
821
952
|
function renderStatus(snapshot: BridgeStatusSnapshot, stalledMsThreshold: number): string[] {
|
|
822
953
|
const lines: string[] = []
|
|
823
954
|
const runLabel = snapshot.running === 'running' ? 'running' : 'idle'
|
|
@@ -830,17 +961,22 @@ function renderStatus(snapshot: BridgeStatusSnapshot, stalledMsThreshold: number
|
|
|
830
961
|
}
|
|
831
962
|
if (snapshot.pendingWork) lines.push(`pendingWork: ${snapshot.nextTurnCount} turn + ${snapshot.nextStepCount} step`)
|
|
832
963
|
if (snapshot.lastAssistantText !== undefined) lines.push(`lastReply: ${snapshot.lastAssistantText}`)
|
|
964
|
+
if (snapshot.reasoningTail !== undefined && snapshot.reasoningTail !== '') {
|
|
965
|
+
const preview = snapshot.reasoningTail.length > 160 ? snapshot.reasoningTail.slice(0, 160) + '…' : snapshot.reasoningTail
|
|
966
|
+
lines.push(`reasoning: ${preview}`)
|
|
967
|
+
}
|
|
833
968
|
return lines
|
|
834
969
|
}
|
|
835
970
|
|
|
836
971
|
function registerStatus(env: BridgeEnv): void {
|
|
837
972
|
env.ctx.tools.register(defineTool({
|
|
838
973
|
name: 'session_bridge_status',
|
|
839
|
-
description: 'Inspect a session\'s live progress for monitoring/scheduling. Returns running/idle, whether a turn is open, last turn number, time since the last event (for stall detection), pending queued work, and the latest text reply. When stalledMsThreshold is given, marks the session as stalled when the time since the last event exceeds it. Pass sessionId of a live session (use session_bridge_find to locate; session_bridge_resume to bring an offline one online). Use this as the "observe" step of a monitor→decide→steer/cancel loop.',
|
|
974
|
+
description: 'Inspect a session\'s live progress for monitoring/scheduling. Returns running/idle, whether a turn is open, last turn number, time since the last event (for stall detection), pending queued work, and the latest text reply. It also surfaces the session\'s chain-of-thought: lastReasoning is the most recent finalized reasoning block, liveReasoning is the in-flight reasoning streamed for the current handled turn (reasoning-delta), and reasoningTail is a compact merged preview. reasoning=none drops all three to keep tokens small. When stalledMsThreshold is given, marks the session as stalled when the time since the last event exceeds it. Pass sessionId of a live session (use session_bridge_find to locate; session_bridge_resume to bring an offline one online). Use this as the "observe" step of a monitor→decide→steer/cancel loop.',
|
|
840
975
|
parameters: {
|
|
841
976
|
sessionId: { type: 'string', required: true, description: 'Session id to inspect (must be live).' },
|
|
842
977
|
stalledMsThreshold: { type: 'number', description: 'Mark the session STALLED when time since the last event exceeds this many ms (default 60000).' },
|
|
843
978
|
recent: { type: 'number', description: 'Number of recent messages to include in the snapshot (default 8, max 20).' },
|
|
979
|
+
reasoning: { type: 'string', enum: ['none', 'last', 'live', 'tail'], description: 'Which chain-of-thought fields to include: tail (default) returns lastReasoning/liveReasoning/reasoningTail; last only the finalized reasoning; live only the in-flight reasoning; none drops all reasoning fields.' },
|
|
844
980
|
},
|
|
845
981
|
output: {
|
|
846
982
|
schema: { type: 'object', additionalProperties: true },
|
|
@@ -859,7 +995,8 @@ function registerStatus(env: BridgeEnv): void {
|
|
|
859
995
|
throw new Error('session ' + JSON.stringify(args.sessionId) + ' is not live — call session_bridge_resume first (status requires a live session)')
|
|
860
996
|
}
|
|
861
997
|
const threshold = typeof args.stalledMsThreshold === 'number' && Number.isFinite(args.stalledMsThreshold) && args.stalledMsThreshold >= 0 ? args.stalledMsThreshold : 60000
|
|
862
|
-
const
|
|
998
|
+
const reasoningMode: StatusReasoning = args.reasoning === 'none' || args.reasoning === 'last' || args.reasoning === 'live' ? args.reasoning : 'tail'
|
|
999
|
+
const snapshot = pruneReasoning(statusSnapshot(env.ctx, agent), reasoningMode)
|
|
863
1000
|
const shown = typeof args.recent === 'number' && Number.isInteger(args.recent) && args.recent >= 0 ? Math.min(args.recent, 20) : 8
|
|
864
1001
|
const envCwd = agent.session.header.cwd
|
|
865
1002
|
env.registry.touch(args.sessionId, {
|