dsh-session-bridge 0.2.1 → 0.3.1
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 +3 -2
- package/lib/index.js +675 -58
- package/package.json +20 -20
- package/src/core.ts +246 -9
- package/src/monitor.ts +126 -1
- package/src/tools.ts +163 -23
package/src/tools.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
7
7
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
8
8
|
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
9
9
|
import type {} from '@deepseek-ai/dsh-agent-presets'
|
|
10
|
-
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
|
10
|
+
import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
|
11
11
|
import * as agentApi from '@deepseek-ai/dsh-agent'
|
|
12
12
|
import type {
|
|
13
13
|
BridgeFindItem,
|
|
@@ -15,13 +15,14 @@ import type {
|
|
|
15
15
|
BridgeStatusSnapshot,
|
|
16
16
|
BridgeWaitResult,
|
|
17
17
|
LiveAgentLike,
|
|
18
|
-
WaitForReplyOptions,
|
|
19
18
|
} from './core.ts'
|
|
20
19
|
import {
|
|
21
20
|
attachSessionToWorkspace,
|
|
22
21
|
foldMessages,
|
|
22
|
+
inspectPersistedSession,
|
|
23
23
|
maxSeq,
|
|
24
24
|
resolveTargetCwd,
|
|
25
|
+
segmentsSince,
|
|
25
26
|
sessionEvents,
|
|
26
27
|
statusSnapshot,
|
|
27
28
|
titleOf,
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
workspaceBySession,
|
|
32
33
|
} from './core.ts'
|
|
33
34
|
import type { BridgeRegistry } from './registry.ts'
|
|
34
|
-
import type { SessionMonitor, MonitorConfig, MonitorEntryState } from './monitor.ts'
|
|
35
|
+
import type { SessionMonitor, MonitorConfig, MonitorEntryState, CoTRule } from './monitor.ts'
|
|
35
36
|
|
|
36
37
|
type SessionIdBrand = { readonly __sessionIdBrand?: never }
|
|
37
38
|
|
|
@@ -101,10 +102,10 @@ interface WaitArgs {
|
|
|
101
102
|
timeoutMs?: number
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
async function maybeWait(env: BridgeEnv, session:
|
|
105
|
+
async function maybeWait(env: BridgeEnv, session: Session, baselineSeq: number, args: WaitArgs, signal: AbortSignal): Promise<ReturnType<typeof renderWait> | undefined> {
|
|
105
106
|
if (args.waitForReply !== true) return undefined
|
|
106
107
|
const result = await waitForReply({
|
|
107
|
-
session
|
|
108
|
+
session,
|
|
108
109
|
baselineSeq,
|
|
109
110
|
timeoutMs: clampTimeout(args.timeoutMs),
|
|
110
111
|
signal,
|
|
@@ -353,8 +354,13 @@ function registerResume(env: BridgeEnv): void {
|
|
|
353
354
|
}
|
|
354
355
|
let headers: readonly { id: string; cwd?: string; createdAt: number }[] = []
|
|
355
356
|
try {
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
// dsh >= 0.1.5-alpha.1:list() 返回 { header, ... } 快照;映射回平铺字段。
|
|
358
|
+
const persistence = env.ctx.sessionPersistence as unknown as { list(): Promise<readonly { header: { id: string; cwd?: string; createdAt: number } }[]> }
|
|
359
|
+
headers = (await persistence.list()).map((rec) => ({
|
|
360
|
+
id: rec.header.id,
|
|
361
|
+
...(rec.header.cwd === undefined ? {} : { cwd: rec.header.cwd }),
|
|
362
|
+
createdAt: rec.header.createdAt,
|
|
363
|
+
}))
|
|
358
364
|
} catch (error) {
|
|
359
365
|
throw new Error('session persistence unavailable: ' + (error instanceof Error ? error.message : String(error)))
|
|
360
366
|
}
|
|
@@ -393,17 +399,19 @@ interface WaitArgsTool {
|
|
|
393
399
|
sinceSeq?: number
|
|
394
400
|
timeoutMs?: number
|
|
395
401
|
requireTurnEnd?: boolean
|
|
402
|
+
waitFor?: 'reply' | 'segment'
|
|
396
403
|
}
|
|
397
404
|
|
|
398
405
|
function registerWait(env: BridgeEnv): void {
|
|
399
406
|
env.ctx.tools.register(defineTool({
|
|
400
407
|
name: 'session_bridge_wait',
|
|
401
|
-
description: 'Wait for a session next assistant
|
|
408
|
+
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).',
|
|
402
409
|
parameters: {
|
|
403
410
|
sessionId: { type: 'string', required: true, description: 'Session id to wait on.' },
|
|
404
411
|
sinceSeq: { type: 'number', description: 'Only replies after this event seq count (default: latest seq at call time).' },
|
|
405
412
|
timeoutMs: { type: 'number', description: 'Wait budget in milliseconds (default 180000, max 3600000); timed out waits return the partial result instead of failing.' },
|
|
406
413
|
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).' },
|
|
414
|
+
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.' },
|
|
407
415
|
},
|
|
408
416
|
output: {
|
|
409
417
|
schema: { type: 'object', additionalProperties: true },
|
|
@@ -425,12 +433,16 @@ function registerWait(env: BridgeEnv): void {
|
|
|
425
433
|
if (agent === undefined) {
|
|
426
434
|
throw new Error('session ' + JSON.stringify(args.sessionId) + ' is not live — call session_bridge_resume first (waiting requires a live session)')
|
|
427
435
|
}
|
|
428
|
-
// 默认 baseline =
|
|
429
|
-
//
|
|
430
|
-
//
|
|
436
|
+
// 默认 baseline = 当前最后一条(带文本的)assistant 行的 seq:让 wait 只等待
|
|
437
|
+
// 之后新出现的输出,避免把"已存在的输出"当成待等内容,同时不被文本后追加的
|
|
438
|
+
// 无文本中间块(推理尾块/工具结果)干扰。segment 模式下以最后一个已完成段落为界。
|
|
439
|
+
const waitSegment = args.waitFor === 'segment'
|
|
431
440
|
let baseline: number
|
|
432
441
|
if (typeof args.sinceSeq === 'number' && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0) {
|
|
433
442
|
baseline = args.sinceSeq
|
|
443
|
+
} else if (waitSegment) {
|
|
444
|
+
const segs = segmentsSince(sessionEvents(agent.session))
|
|
445
|
+
baseline = segs.length === 0 ? -1 : (segs[segs.length - 1]?.seq ?? -1)
|
|
434
446
|
} else {
|
|
435
447
|
let lastText = -1
|
|
436
448
|
for (const row of foldMessages(sessionEvents(agent.session))) {
|
|
@@ -444,6 +456,7 @@ function registerWait(env: BridgeEnv): void {
|
|
|
444
456
|
timeoutMs: clampTimeout(args.timeoutMs),
|
|
445
457
|
signal: exec.signal,
|
|
446
458
|
requireTurnEnd: args.requireTurnEnd === true,
|
|
459
|
+
...(waitSegment ? { waitForSegment: true } : {}),
|
|
447
460
|
})
|
|
448
461
|
env.registry.touch(args.sessionId)
|
|
449
462
|
return asJson({
|
|
@@ -455,6 +468,76 @@ function registerWait(env: BridgeEnv): void {
|
|
|
455
468
|
}))
|
|
456
469
|
}
|
|
457
470
|
|
|
471
|
+
interface SegmentsArgs {
|
|
472
|
+
sessionId: string
|
|
473
|
+
sinceSeq?: number
|
|
474
|
+
limit?: number
|
|
475
|
+
}
|
|
476
|
+
function registerSegments(env: BridgeEnv): void {
|
|
477
|
+
env.ctx.tools.register(defineTool({
|
|
478
|
+
name: 'session_bridge_segments',
|
|
479
|
+
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.',
|
|
480
|
+
parameters: {
|
|
481
|
+
sessionId: { type: 'string', required: true, description: 'Session id to read segments from (live or offline).' },
|
|
482
|
+
sinceSeq: { type: 'number', description: 'Only return completed segments after this event seq (paging cursor).' },
|
|
483
|
+
limit: { type: 'number', description: 'Maximum number of segments to return (default 10, max 50).' },
|
|
484
|
+
},
|
|
485
|
+
output: {
|
|
486
|
+
schema: { type: 'object', additionalProperties: true },
|
|
487
|
+
render: (_args, value) => {
|
|
488
|
+
const v = value as Record<string, unknown>
|
|
489
|
+
const segs = (v.segments as Array<Record<string, unknown>> | null) ?? []
|
|
490
|
+
if (segs.length === 0) return [{ type: 'text' as const, text: '(no completed segments)' }]
|
|
491
|
+
const lines = segs.map((sg) => {
|
|
492
|
+
const head = 'seg #' + String(sg.seq) + ' (turn ' + String(sg.turn) + ' step ' + String(sg.step) + ')'
|
|
493
|
+
const text = typeof sg.text === 'string' ? sg.text : ''
|
|
494
|
+
const reason = typeof sg.reasoning === 'string' && sg.reasoning !== '' ? ' [reasoning ' + sg.reasoning.length + ' chars]' : ''
|
|
495
|
+
const tools = Array.isArray(sg.toolCalls) && sg.toolCalls.length > 0 ? ' [tools: ' + sg.toolCalls.join(',') + ']' : ''
|
|
496
|
+
return head + tools + reason + ': ' + text.slice(0, 160)
|
|
497
|
+
})
|
|
498
|
+
return [{ type: 'text' as const, text: lines.join('\n') }]
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
async execute(args: SegmentsArgs) {
|
|
502
|
+
const agent = liveAgent(env, args.sessionId)
|
|
503
|
+
let events: readonly SessionEvent[]
|
|
504
|
+
let live: boolean
|
|
505
|
+
if (agent !== undefined) {
|
|
506
|
+
events = sessionEvents(agent.session)
|
|
507
|
+
live = true
|
|
508
|
+
} else {
|
|
509
|
+
try {
|
|
510
|
+
const inspection = await inspectPersistedSession(env.ctx, args.sessionId)
|
|
511
|
+
events = inspection.events
|
|
512
|
+
live = false
|
|
513
|
+
} catch (error) {
|
|
514
|
+
throw new Error(String(error))
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const sinceSeq = typeof args.sinceSeq === 'number' && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0 ? args.sinceSeq : 0
|
|
518
|
+
const limit = clampLimit(args.limit, 10, 50)
|
|
519
|
+
const segs = segmentsSince(events, sinceSeq)
|
|
520
|
+
env.registry.touch(args.sessionId)
|
|
521
|
+
const nextCursorSeq = segs.length === 0 ? sinceSeq : (segs[segs.length - 1]?.seq ?? sinceSeq)
|
|
522
|
+
return asJson({
|
|
523
|
+
sessionId: args.sessionId,
|
|
524
|
+
live,
|
|
525
|
+
nextCursorSeq,
|
|
526
|
+
segments: segs.map((sg) => ({
|
|
527
|
+
seq: sg.seq,
|
|
528
|
+
time: sg.time,
|
|
529
|
+
turn: sg.turn,
|
|
530
|
+
step: sg.step,
|
|
531
|
+
openTurn: sg.openTurn,
|
|
532
|
+
...(sg.text === undefined ? {} : { text: sg.text }),
|
|
533
|
+
...(sg.reasoning === undefined ? {} : { reasoning: sg.reasoning }),
|
|
534
|
+
...(sg.toolCalls.length > 0 ? { toolCalls: sg.toolCalls } : {}),
|
|
535
|
+
})),
|
|
536
|
+
})
|
|
537
|
+
},
|
|
538
|
+
}))
|
|
539
|
+
}
|
|
540
|
+
|
|
458
541
|
interface ReadArgs {
|
|
459
542
|
sessionId: string
|
|
460
543
|
sinceSeq?: number
|
|
@@ -501,8 +584,7 @@ function registerRead(env: BridgeEnv): void {
|
|
|
501
584
|
} else {
|
|
502
585
|
let inspection: { events: readonly SessionEvent[]; meta: { cwd?: string } }
|
|
503
586
|
try {
|
|
504
|
-
|
|
505
|
-
inspection = await persistence.inspect(args.sessionId)
|
|
587
|
+
inspection = await inspectPersistedSession(env.ctx, args.sessionId)
|
|
506
588
|
} catch (error) {
|
|
507
589
|
throw new Error('cannot read session ' + JSON.stringify(args.sessionId) + ': ' + (error instanceof Error ? error.message : String(error)))
|
|
508
590
|
}
|
|
@@ -606,8 +688,11 @@ function registerFind(env: BridgeEnv): void {
|
|
|
606
688
|
}
|
|
607
689
|
if (args.liveOnly !== true) {
|
|
608
690
|
try {
|
|
609
|
-
|
|
610
|
-
|
|
691
|
+
// dsh >= 0.1.5-alpha.1:list() 返回 { header: SessionHeader, revision, ... } 快照,
|
|
692
|
+
// 不再平铺 id/cwd;旧版直接返回 header 平铺字段。两者都按新形状读,向下兼容。
|
|
693
|
+
const persistence = env.ctx.sessionPersistence as unknown as { list(): Promise<readonly { header: { id: string; cwd?: string; parentSession?: string; origin?: 'subagent'; createdAt: number; agentPreset?: string } }[]> }
|
|
694
|
+
for (const rec of await persistence.list()) {
|
|
695
|
+
const header = rec.header
|
|
611
696
|
if (items.some((item) => item.sessionId === header.id)) continue
|
|
612
697
|
items.push({
|
|
613
698
|
sessionId: header.id,
|
|
@@ -641,13 +726,12 @@ function registerFind(env: BridgeEnv): void {
|
|
|
641
726
|
const cwdFilter = typeof args.cwd === 'string' ? args.cwd.trim().toLowerCase() : ''
|
|
642
727
|
const needsOfflineTitle = titleFilter !== '' || (query !== '' && items.some((item) => !item.live))
|
|
643
728
|
if (needsOfflineTitle) {
|
|
644
|
-
const persistence = env.ctx.sessionPersistence as unknown as { inspect(id: string): Promise<{ events: readonly SessionEvent[] }> }
|
|
645
729
|
let inspected = 0
|
|
646
730
|
for (const item of items) {
|
|
647
731
|
if (inspected >= 30) break
|
|
648
732
|
if (item.live || item.title !== undefined) continue
|
|
649
733
|
try {
|
|
650
|
-
const inspection = await
|
|
734
|
+
const inspection = await inspectPersistedSession(env.ctx, item.sessionId)
|
|
651
735
|
item.title = titleOf(inspection.events)
|
|
652
736
|
inspected += 1
|
|
653
737
|
} catch {
|
|
@@ -699,6 +783,7 @@ export function registerBridgeTools(env: BridgeEnv): void {
|
|
|
699
783
|
registerSend(env)
|
|
700
784
|
registerResume(env)
|
|
701
785
|
registerWait(env)
|
|
786
|
+
registerSegments(env)
|
|
702
787
|
registerRead(env)
|
|
703
788
|
registerFind(env)
|
|
704
789
|
registerStatus(env)
|
|
@@ -717,6 +802,25 @@ interface MonitorStartArgs {
|
|
|
717
802
|
onStallSteer?: string
|
|
718
803
|
onOffTrackSteer?: string
|
|
719
804
|
label?: string
|
|
805
|
+
coRules?: Array<{ match?: string; field?: string; value?: string; action?: string; message?: string }>
|
|
806
|
+
cotMinHits?: number
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/** 校验并规范化 coRules 参数为 CoTRule[];非法项抛清晰错误。 */
|
|
810
|
+
function parseCoRules(raw: Array<{ match?: string; field?: string; value?: string; action?: string; message?: string }>): CoTRule[] {
|
|
811
|
+
return raw.map((rule, index) => {
|
|
812
|
+
const match = rule.match === 'contains' || rule.match === 'not-contains' ? rule.match : (() => { throw new Error('coRules[' + index + '].match must be "contains" or "not-contains"') })()
|
|
813
|
+
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"') })()
|
|
814
|
+
const action = rule.action === 'steer' || rule.action === 'cancel' ? rule.action : (() => { throw new Error('coRules[' + index + '].action must be "steer" or "cancel"') })()
|
|
815
|
+
const value = typeof rule.value === 'string' && rule.value.trim() !== '' ? rule.value.trim() : (() => { throw new Error('coRules[' + index + '].value must be a non-empty string') })()
|
|
816
|
+
return {
|
|
817
|
+
match,
|
|
818
|
+
field,
|
|
819
|
+
action,
|
|
820
|
+
value,
|
|
821
|
+
...(typeof rule.message === 'string' && rule.message.trim() !== '' ? { message: rule.message.trim() } : {}),
|
|
822
|
+
}
|
|
823
|
+
})
|
|
720
824
|
}
|
|
721
825
|
|
|
722
826
|
function renderMonitorState(entry: MonitorEntryState): string[] {
|
|
@@ -724,6 +828,8 @@ function renderMonitorState(entry: MonitorEntryState): string[] {
|
|
|
724
828
|
lines.push(`monitoring ${entry.config.sessionId}${entry.config.label === undefined ? '' : ' ("' + entry.config.label + '")'}`)
|
|
725
829
|
lines.push(`interval: ${entry.config.intervalMs}ms | stalled: ${String(entry.config.stalledMs ?? 60000)}ms | maxStuck: ${String(entry.config.maxStuckCycles ?? 3)}`)
|
|
726
830
|
lines.push(`cycles: ${entry.cycles} | stuck: ${entry.stuckCount} | lastAction: ${entry.lastAction}`)
|
|
831
|
+
const cotCount = (entry.config.coTRules ?? []).length
|
|
832
|
+
if (cotCount > 0) lines.push(`coRules: ${cotCount} rule(s) (minHits ${String(entry.config.cotMinHits ?? 1)})`)
|
|
727
833
|
if (entry.lastNote !== '') lines.push(`note: ${entry.lastNote}`)
|
|
728
834
|
if (entry.done) lines.push('done: yes')
|
|
729
835
|
return lines
|
|
@@ -732,7 +838,7 @@ function renderMonitorState(entry: MonitorEntryState): string[] {
|
|
|
732
838
|
function registerMonitor(env: BridgeEnv): void {
|
|
733
839
|
env.ctx.tools.register(defineTool({
|
|
734
840
|
name: 'session_bridge_monitor_start',
|
|
735
|
-
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.',
|
|
841
|
+
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.',
|
|
736
842
|
parameters: {
|
|
737
843
|
sessionId: { type: 'string', required: true, description: 'Target main session id to watch (must be live).' },
|
|
738
844
|
intervalMs: { type: 'number', description: 'Poll interval in ms (default 10000, min 5000).' },
|
|
@@ -743,6 +849,14 @@ function registerMonitor(env: BridgeEnv): void {
|
|
|
743
849
|
onStallSteer: { type: 'string', description: 'Steer text injected on a stall/nudge (default: ask to summarize progress and continue).' },
|
|
744
850
|
onOffTrackSteer: { type: 'string', description: 'Steer text injected when LLM judges the task off-track (default: ask to return to the original goal).' },
|
|
745
851
|
label: { type: 'string', description: 'Optional human label for logs/display.' },
|
|
852
|
+
coRules: { type: 'array', items: { type: 'object', additionalProperties: false, properties: {
|
|
853
|
+
match: { type: 'string', required: true, enum: ['contains', 'not-contains'], description: 'contains = must include value in the chosen field; not-contains = must NOT include it.' },
|
|
854
|
+
field: { type: 'string', required: true, enum: ['reasoning', 'text', 'both'], description: 'Match on reasoning (chain-of-thought), text (reply), or both (either).' },
|
|
855
|
+
value: { type: 'string', required: true, description: 'The substring to match (non-empty).' },
|
|
856
|
+
action: { type: 'string', required: true, enum: ['steer', 'cancel'], description: 'steer injects a guiding message; cancel terminates the session.' },
|
|
857
|
+
message: { type: 'string', description: 'Custom steer text (default is a guidance prompt).' },
|
|
858
|
+
} }, 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.' },
|
|
859
|
+
cotMinHits: { type: 'number', description: 'How many consecutive matched polls before a CoT rule fires (default 1 = immediately).' },
|
|
746
860
|
},
|
|
747
861
|
output: {
|
|
748
862
|
schema: { type: 'object', additionalProperties: true },
|
|
@@ -765,6 +879,8 @@ function registerMonitor(env: BridgeEnv): void {
|
|
|
765
879
|
...(typeof args.onStallSteer === 'string' && args.onStallSteer.trim() !== '' ? { onStallSteer: args.onStallSteer.trim() } : {}),
|
|
766
880
|
...(typeof args.onOffTrackSteer === 'string' && args.onOffTrackSteer.trim() !== '' ? { onOffTrackSteer: args.onOffTrackSteer.trim() } : {}),
|
|
767
881
|
...(typeof args.label === 'string' && args.label.trim() !== '' ? { label: args.label.trim() } : {}),
|
|
882
|
+
...(Array.isArray(args.coRules) && args.coRules.length > 0 ? { coTRules: parseCoRules(args.coRules) } : {}),
|
|
883
|
+
...(typeof args.cotMinHits === 'number' && Number.isInteger(args.cotMinHits) && args.cotMinHits >= 1 ? { cotMinHits: Math.floor(args.cotMinHits) } : {}),
|
|
768
884
|
}
|
|
769
885
|
const entry = env.monitor.start(config)
|
|
770
886
|
env.registry.touch(args.sessionId)
|
|
@@ -812,13 +928,32 @@ function registerMonitor(env: BridgeEnv): void {
|
|
|
812
928
|
}))
|
|
813
929
|
}
|
|
814
930
|
|
|
931
|
+
type StatusReasoning = 'none' | 'last' | 'live' | 'tail'
|
|
932
|
+
|
|
815
933
|
interface StatusArgs {
|
|
816
934
|
sessionId: string
|
|
817
935
|
stalledMsThreshold?: number
|
|
818
936
|
recent?: number
|
|
937
|
+
reasoning?: StatusReasoning
|
|
819
938
|
}
|
|
820
939
|
|
|
821
|
-
/**
|
|
940
|
+
/** 按 reasoning 选项裁剪快照里的思维链字段(避免入 token 时有 diff 语义差)。 */
|
|
941
|
+
function pruneReasoning(snapshot: BridgeStatusSnapshot, mode: StatusReasoning): BridgeStatusSnapshot {
|
|
942
|
+
if (mode === 'tail') return snapshot
|
|
943
|
+
const rest: BridgeStatusSnapshot = { ...snapshot }
|
|
944
|
+
delete (rest as { reasoningTail?: unknown }).reasoningTail
|
|
945
|
+
if (mode === 'none') {
|
|
946
|
+
delete (rest as { lastReasoning?: unknown }).lastReasoning
|
|
947
|
+
delete (rest as { liveReasoning?: unknown }).liveReasoning
|
|
948
|
+
} else if (mode === 'last') {
|
|
949
|
+
delete (rest as { liveReasoning?: unknown }).liveReasoning
|
|
950
|
+
} else { // 'live'
|
|
951
|
+
delete (rest as { lastReasoning?: unknown }).lastReasoning
|
|
952
|
+
}
|
|
953
|
+
return rest
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** 渲染监控快照为一行摘要:运行态 + openTurn + 卡住/待处理 + 最新回复(+ 思维链预览)。 */
|
|
822
957
|
function renderStatus(snapshot: BridgeStatusSnapshot, stalledMsThreshold: number): string[] {
|
|
823
958
|
const lines: string[] = []
|
|
824
959
|
const runLabel = snapshot.running === 'running' ? 'running' : 'idle'
|
|
@@ -831,17 +966,22 @@ function renderStatus(snapshot: BridgeStatusSnapshot, stalledMsThreshold: number
|
|
|
831
966
|
}
|
|
832
967
|
if (snapshot.pendingWork) lines.push(`pendingWork: ${snapshot.nextTurnCount} turn + ${snapshot.nextStepCount} step`)
|
|
833
968
|
if (snapshot.lastAssistantText !== undefined) lines.push(`lastReply: ${snapshot.lastAssistantText}`)
|
|
969
|
+
if (snapshot.reasoningTail !== undefined && snapshot.reasoningTail !== '') {
|
|
970
|
+
const preview = snapshot.reasoningTail.length > 160 ? snapshot.reasoningTail.slice(0, 160) + '…' : snapshot.reasoningTail
|
|
971
|
+
lines.push(`reasoning: ${preview}`)
|
|
972
|
+
}
|
|
834
973
|
return lines
|
|
835
974
|
}
|
|
836
975
|
|
|
837
976
|
function registerStatus(env: BridgeEnv): void {
|
|
838
977
|
env.ctx.tools.register(defineTool({
|
|
839
978
|
name: 'session_bridge_status',
|
|
840
|
-
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.',
|
|
979
|
+
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.',
|
|
841
980
|
parameters: {
|
|
842
981
|
sessionId: { type: 'string', required: true, description: 'Session id to inspect (must be live).' },
|
|
843
982
|
stalledMsThreshold: { type: 'number', description: 'Mark the session STALLED when time since the last event exceeds this many ms (default 60000).' },
|
|
844
983
|
recent: { type: 'number', description: 'Number of recent messages to include in the snapshot (default 8, max 20).' },
|
|
984
|
+
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.' },
|
|
845
985
|
},
|
|
846
986
|
output: {
|
|
847
987
|
schema: { type: 'object', additionalProperties: true },
|
|
@@ -860,7 +1000,8 @@ function registerStatus(env: BridgeEnv): void {
|
|
|
860
1000
|
throw new Error('session ' + JSON.stringify(args.sessionId) + ' is not live — call session_bridge_resume first (status requires a live session)')
|
|
861
1001
|
}
|
|
862
1002
|
const threshold = typeof args.stalledMsThreshold === 'number' && Number.isFinite(args.stalledMsThreshold) && args.stalledMsThreshold >= 0 ? args.stalledMsThreshold : 60000
|
|
863
|
-
const
|
|
1003
|
+
const reasoningMode: StatusReasoning = args.reasoning === 'none' || args.reasoning === 'last' || args.reasoning === 'live' ? args.reasoning : 'tail'
|
|
1004
|
+
const snapshot = pruneReasoning(statusSnapshot(env.ctx, agent), reasoningMode)
|
|
864
1005
|
const shown = typeof args.recent === 'number' && Number.isInteger(args.recent) && args.recent >= 0 ? Math.min(args.recent, 20) : 8
|
|
865
1006
|
const envCwd = agent.session.header.cwd
|
|
866
1007
|
env.registry.touch(args.sessionId, {
|
|
@@ -982,7 +1123,6 @@ function registerArchive(env: BridgeEnv): void {
|
|
|
982
1123
|
const archived = env.ctx.workspaceRegistry.archivedSessionIds.map(String)
|
|
983
1124
|
const items: Array<{ sessionId: string; title?: string }> = archived.map((id) => ({ sessionId: id }))
|
|
984
1125
|
if (args.resolveTitles === true && items.length > 0) {
|
|
985
|
-
const persistence = env.ctx.sessionPersistence as unknown as { inspect(id: string): Promise<{ events: readonly SessionEvent[] }> }
|
|
986
1126
|
const registryTitles = new Map<string, string>()
|
|
987
1127
|
try {
|
|
988
1128
|
const records = await env.registry.all()
|
|
@@ -994,7 +1134,7 @@ function registerArchive(env: BridgeEnv): void {
|
|
|
994
1134
|
continue
|
|
995
1135
|
}
|
|
996
1136
|
try {
|
|
997
|
-
const inspection = await
|
|
1137
|
+
const inspection = await inspectPersistedSession(env.ctx, item.sessionId)
|
|
998
1138
|
item.title = titleOf(inspection.events)
|
|
999
1139
|
} catch { /* offline title unavailable */ }
|
|
1000
1140
|
}
|