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/lib/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-home-paths@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-
|
|
7
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-home-paths@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-in_7832b76c982cc1477c0eac59a4954baf/node_modules/@deepseek-ai/dsh-home-paths/lib/index.js
|
|
8
8
|
/**
|
|
9
9
|
* Shared filesystem path helpers for DeepSeek Harness user data.
|
|
10
10
|
*
|
|
@@ -2200,7 +2200,7 @@ var Service = class Service {
|
|
|
2200
2200
|
}
|
|
2201
2201
|
};
|
|
2202
2202
|
//#endregion
|
|
2203
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.1.2-
|
|
2203
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.1.2-rc.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
|
|
2204
2204
|
/** The one Remote failure class shared by owners, the Gateway, and consumers. */
|
|
2205
2205
|
/**
|
|
2206
2206
|
* One Remote call failure: a real Error carrying its stable code and typed
|
|
@@ -2345,7 +2345,7 @@ function validateName(subject, value) {
|
|
|
2345
2345
|
if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
|
|
2346
2346
|
}
|
|
2347
2347
|
//#endregion
|
|
2348
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-
|
|
2348
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-i_486cdbb63f2ab3a8eaad87d2b6e5a4b7/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
|
|
2349
2349
|
/** Duplicate-install-safe JSON and immutable-value helpers. @module @deepseek-ai/dsh-util-values */
|
|
2350
2350
|
/**
|
|
2351
2351
|
* Mark an unreachable closed-union branch.
|
|
@@ -2557,7 +2557,7 @@ function deepFreeze(value) {
|
|
|
2557
2557
|
return value;
|
|
2558
2558
|
}
|
|
2559
2559
|
//#endregion
|
|
2560
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-util-crypto@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-
|
|
2560
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-util-crypto@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-i_45952c52282327ee28c4189b1e05c80c/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
|
|
2561
2561
|
/**
|
|
2562
2562
|
* Random v4 UUID, minted from `crypto.getRandomValues`.
|
|
2563
2563
|
* @returns the UUID string.
|
|
@@ -2570,7 +2570,7 @@ function randomUUID$1() {
|
|
|
2570
2570
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
2571
2571
|
}
|
|
2572
2572
|
//#endregion
|
|
2573
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-
|
|
2573
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invaria_0a6acdb1edc9f1b3caaaafba5a0cd464/node_modules/@deepseek-ai/dsh-brand/lib/index.js
|
|
2574
2574
|
/**
|
|
2575
2575
|
* Duplicate-install-safe nominal string helpers.
|
|
2576
2576
|
*
|
|
@@ -3187,11 +3187,11 @@ defineMethod("transform", [
|
|
|
3187
3187
|
"preserve"
|
|
3188
3188
|
], ({ inner }, isInner) => inner.toString(isInner));
|
|
3189
3189
|
//#endregion
|
|
3190
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.1.2-
|
|
3190
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.1.2-rc.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
|
|
3191
3191
|
/** Largest delay Node schedules without clamping it to one millisecond. */
|
|
3192
3192
|
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
3193
3193
|
//#endregion
|
|
3194
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-
|
|
3194
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invariant_cc3ae850d309c6dee6d11afd096d0893/node_modules/@deepseek-ai/dsh-llm/lib/index.js
|
|
3195
3195
|
/**
|
|
3196
3196
|
* Detach and deep-freeze a message whose identity already exists.
|
|
3197
3197
|
* @param message - complete message, including its stable identity.
|
|
@@ -8966,7 +8966,7 @@ function superRefine(fn, params) {
|
|
|
8966
8966
|
return /* @__PURE__ */ _superRefine(fn, params);
|
|
8967
8967
|
}
|
|
8968
8968
|
//#endregion
|
|
8969
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-storage@0.1.2-
|
|
8969
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-storage@0.1.2-rc.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-storage/lib/index.js
|
|
8970
8970
|
/**
|
|
8971
8971
|
* Backend-facing vocabulary of the storage hub: a backend owns one medium
|
|
8972
8972
|
* (a file-tree root, a database file) and exposes operation groups over it.
|
|
@@ -8977,7 +8977,7 @@ function superRefine(fn, params) {
|
|
|
8977
8977
|
/** Allowed format for unit and table names: safe as a file name and as a SQL identifier segment without escaping. */
|
|
8978
8978
|
const UNIT_NAME_RE = /^[a-z][a-z0-9_]*$/;
|
|
8979
8979
|
//#endregion
|
|
8980
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-storage-domain@0.1.2-
|
|
8980
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-storage-domain@0.1.2-rc.1_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-i_9f44d404e0b20c2f6e8599234336f27b/node_modules/@deepseek-ai/dsh-storage-domain/lib/index.js
|
|
8981
8981
|
/**
|
|
8982
8982
|
* Domain declaration vocabulary. A spec object is the single source of a
|
|
8983
8983
|
* domain's identity, layout, and record schemas: the owning package defines
|
|
@@ -9098,6 +9098,29 @@ Service.init;
|
|
|
9098
9098
|
* dsh-session-bridge core: 会话创建 / 发消息 / 等待回复 / 读取 / 查找。
|
|
9099
9099
|
* 所有逻辑跑在插件宿主上下文(rootCtx),操作的是 DSH 真实的会话与 agent 注册表。
|
|
9100
9100
|
*/
|
|
9101
|
+
/**
|
|
9102
|
+
* 兼容读取不同 dsh-session 版本上的会话事件:
|
|
9103
|
+
* - 旧版 `Session` 暴露 `get events(): readonly SessionEvent[]`;
|
|
9104
|
+
* - 新版(如宿主实际运行的 0.1.2-rc.x)将 `get events` 改为
|
|
9105
|
+
* `snapshotEvents(fromSeq?, toSeqExclusive?)` 方法——`.events` 直接读取会
|
|
9106
|
+
* 得到 `undefined`,对 for...of 迭代即抛 `events is not iterable`。
|
|
9107
|
+
* 两者都读不到(或 session 不存在)时回退为空数组,绝不抛迭代错误。
|
|
9108
|
+
*/
|
|
9109
|
+
function sessionEvents(session) {
|
|
9110
|
+
const s = session;
|
|
9111
|
+
if (s === null || s === void 0) return [];
|
|
9112
|
+
if (Array.isArray(s.events)) return s.events;
|
|
9113
|
+
if (typeof s.snapshotEvents === "function") try {
|
|
9114
|
+
return s.snapshotEvents();
|
|
9115
|
+
} catch {
|
|
9116
|
+
return [];
|
|
9117
|
+
}
|
|
9118
|
+
return [];
|
|
9119
|
+
}
|
|
9120
|
+
/** 规约任意"事件列表"值为数组(非数组 → []),避免 events is not iterable。 */
|
|
9121
|
+
function asEventList(events) {
|
|
9122
|
+
return Array.isArray(events) ? events : [];
|
|
9123
|
+
}
|
|
9101
9124
|
/** agent.followup / steer 接受的用户消息值。 */
|
|
9102
9125
|
function userMessage(text) {
|
|
9103
9126
|
return {
|
|
@@ -9121,6 +9144,18 @@ function blockText(content) {
|
|
|
9121
9144
|
}
|
|
9122
9145
|
return parts.length === 0 ? void 0 : parts.join("\n");
|
|
9123
9146
|
}
|
|
9147
|
+
/** 提取指定类型的 content block 文本(如 reasoning),用于从消息 content 里读出思维链。 */
|
|
9148
|
+
function blockTextByType(content, wantedType) {
|
|
9149
|
+
if (!Array.isArray(content)) return void 0;
|
|
9150
|
+
const parts = [];
|
|
9151
|
+
for (const raw of content) {
|
|
9152
|
+
const block = raw;
|
|
9153
|
+
if (block !== null && typeof block === "object" && block.type === wantedType && typeof block.text === "string") {
|
|
9154
|
+
if (block.text !== "") parts.push(block.text);
|
|
9155
|
+
}
|
|
9156
|
+
}
|
|
9157
|
+
return parts.length === 0 ? void 0 : parts.join("\n");
|
|
9158
|
+
}
|
|
9124
9159
|
function countImages(content) {
|
|
9125
9160
|
if (!Array.isArray(content)) return 0;
|
|
9126
9161
|
let n = 0;
|
|
@@ -9141,8 +9176,9 @@ function toolNamesOf(toolCalls) {
|
|
|
9141
9176
|
}
|
|
9142
9177
|
/** 折叠会话事件日志为可读消息行(跳过非 user 来源与空消息)。 */
|
|
9143
9178
|
function foldMessages(events) {
|
|
9179
|
+
const list = asEventList(events);
|
|
9144
9180
|
const rows = [];
|
|
9145
|
-
for (const event of
|
|
9181
|
+
for (const event of list) if (event.type === "user/message") {
|
|
9146
9182
|
const source = event.data.source;
|
|
9147
9183
|
if (source !== void 0 && source.kind !== "user") continue;
|
|
9148
9184
|
const text = blockText(event.data.content);
|
|
@@ -9157,7 +9193,7 @@ function foldMessages(events) {
|
|
|
9157
9193
|
} else if (event.type === "assistant/message") {
|
|
9158
9194
|
const message = event.data.message;
|
|
9159
9195
|
const text = message === null ? void 0 : blockText(message.content);
|
|
9160
|
-
const reasoning = message === null ? void 0 : blockText(message.reasoning);
|
|
9196
|
+
const reasoning = message === null ? void 0 : blockTextByType(message.content, "reasoning") ?? blockText(message.reasoning);
|
|
9161
9197
|
const images = message === null ? 0 : countImages(message.content);
|
|
9162
9198
|
const toolCalls = message === null ? [] : toolNamesOf(message.toolCalls);
|
|
9163
9199
|
if (text === void 0 && reasoning === void 0 && toolCalls.length === 0) continue;
|
|
@@ -9174,13 +9210,119 @@ function foldMessages(events) {
|
|
|
9174
9210
|
}
|
|
9175
9211
|
return rows;
|
|
9176
9212
|
}
|
|
9213
|
+
/** 按已完成输出段落折叠事件日志:每个 assistant/message 割一段,返回 sinceSeq 后的所有段落(事件序)。 */
|
|
9214
|
+
function segmentsSince(events, sinceSeq = 0) {
|
|
9215
|
+
const list = asEventList(events);
|
|
9216
|
+
const out = [];
|
|
9217
|
+
let openTurn = false;
|
|
9218
|
+
for (const event of list) {
|
|
9219
|
+
if (event.type === "turn/start") {
|
|
9220
|
+
openTurn = true;
|
|
9221
|
+
continue;
|
|
9222
|
+
}
|
|
9223
|
+
if (event.type === "turn/end") {
|
|
9224
|
+
openTurn = false;
|
|
9225
|
+
continue;
|
|
9226
|
+
}
|
|
9227
|
+
if (event.type !== "assistant/message") continue;
|
|
9228
|
+
if (event.seq <= sinceSeq) continue;
|
|
9229
|
+
const message = event.data.message;
|
|
9230
|
+
const text = message === null ? void 0 : blockText(message.content);
|
|
9231
|
+
const reasoning = message === null ? void 0 : blockTextByType(message.content, "reasoning") ?? blockText(message.reasoning);
|
|
9232
|
+
const toolCalls = message === null ? [] : toolNamesOf(message.toolCalls);
|
|
9233
|
+
const turn = event.data?.turn;
|
|
9234
|
+
const step = event.data?.step;
|
|
9235
|
+
const seg = {
|
|
9236
|
+
seq: event.seq,
|
|
9237
|
+
time: event.time,
|
|
9238
|
+
turn: typeof turn === "number" ? turn : 0,
|
|
9239
|
+
step: typeof step === "number" ? step : 0,
|
|
9240
|
+
toolCalls,
|
|
9241
|
+
openTurn
|
|
9242
|
+
};
|
|
9243
|
+
if (text !== void 0) seg.text = text;
|
|
9244
|
+
if (reasoning !== void 0) seg.reasoning = reasoning;
|
|
9245
|
+
out.push(seg);
|
|
9246
|
+
}
|
|
9247
|
+
return out;
|
|
9248
|
+
}
|
|
9249
|
+
/** 取自 sinceSeq 之后最新一个已完成输出段落(无则 null)。 */
|
|
9250
|
+
function latestSegmentSince(events, sinceSeq = 0) {
|
|
9251
|
+
const segs = segmentsSince(events, sinceSeq);
|
|
9252
|
+
return segs.length === 0 ? null : segs[segs.length - 1] ?? null;
|
|
9253
|
+
}
|
|
9254
|
+
/**
|
|
9255
|
+
* 对一个 live 会话的事件日志折叠出其"实时思维链":
|
|
9256
|
+
* - 优先聚合流式 assistant/chunk 事件里的 reasoning-delta(turn 中途即可见、增量,
|
|
9257
|
+
* 这正是监控思维链并按规则提前终止所需的粒度);
|
|
9258
|
+
* - 同时聚合同窗口的 text-delta 增量;
|
|
9259
|
+
* - 若没有进行中的 chunk(会话空闲、或该 provider 不流式推理/无推理),返回 null,
|
|
9260
|
+
* 调用方应回落为 foldMessages 里的已定型 message.reasoning。
|
|
9261
|
+
* 纯读、无副作用;事件形状不做任何假设,缺失/异常一律安全处理。
|
|
9262
|
+
*/
|
|
9263
|
+
function liveReasoningSnapshot(events) {
|
|
9264
|
+
const list = asEventList(events);
|
|
9265
|
+
let lastFinalizedSeq = -1;
|
|
9266
|
+
let reasoning = "";
|
|
9267
|
+
let text = "";
|
|
9268
|
+
let seq = -1;
|
|
9269
|
+
let time = 0;
|
|
9270
|
+
let turn = 0;
|
|
9271
|
+
let step = 0;
|
|
9272
|
+
let foundDelta = false;
|
|
9273
|
+
for (const event of list) {
|
|
9274
|
+
if (event.type === "assistant/message") {
|
|
9275
|
+
lastFinalizedSeq = event.seq;
|
|
9276
|
+
continue;
|
|
9277
|
+
}
|
|
9278
|
+
if (event.type !== "assistant/chunk") continue;
|
|
9279
|
+
if (event.seq <= lastFinalizedSeq) continue;
|
|
9280
|
+
const data = event.data;
|
|
9281
|
+
if (data === null || data === void 0 || typeof data !== "object") continue;
|
|
9282
|
+
const c = data.chunk;
|
|
9283
|
+
if (c === null || typeof c !== "object" || typeof c.type !== "string") continue;
|
|
9284
|
+
if (c.type === "reasoning-delta" || c.type === "text-delta") {
|
|
9285
|
+
if (typeof c.text !== "string") continue;
|
|
9286
|
+
if (c.type === "reasoning-delta") reasoning += c.text;
|
|
9287
|
+
else text += c.text;
|
|
9288
|
+
if (typeof data.turn === "number") turn = data.turn;
|
|
9289
|
+
if (typeof data.step === "number") step = data.step;
|
|
9290
|
+
foundDelta = true;
|
|
9291
|
+
if (event.seq > seq) seq = event.seq;
|
|
9292
|
+
if (event.time > time) time = event.time;
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
9295
|
+
if (!foundDelta) return null;
|
|
9296
|
+
return {
|
|
9297
|
+
turn,
|
|
9298
|
+
step,
|
|
9299
|
+
reasoning,
|
|
9300
|
+
text,
|
|
9301
|
+
seq,
|
|
9302
|
+
time
|
|
9303
|
+
};
|
|
9304
|
+
}
|
|
9305
|
+
/** 思维链尾巴默认截断上限(字符)。 */
|
|
9306
|
+
const REASONING_TAIL_LIMIT = 4e3;
|
|
9307
|
+
/**
|
|
9308
|
+
* 组装一条受字符上限约束的"思维链尾巴"用于展示/摘要:
|
|
9309
|
+
* 优先进行中的 liveReasoning,否则最近已定型的 lastReasoning;
|
|
9310
|
+
* 超出上限时截断并附省略标记。两者皆无返回 undefined。
|
|
9311
|
+
*/
|
|
9312
|
+
function reasoningTailOf(liveReasoning, lastReasoning, limit = REASONING_TAIL_LIMIT) {
|
|
9313
|
+
const source = liveReasoning ?? lastReasoning;
|
|
9314
|
+
if (source === void 0 || source === "") return void 0;
|
|
9315
|
+
if (source.length <= limit) return source;
|
|
9316
|
+
return source.slice(0, limit) + "…";
|
|
9317
|
+
}
|
|
9177
9318
|
/**
|
|
9178
9319
|
* 会话标题:优先 session/title 事件(last-wins,与 UI 一致);
|
|
9179
9320
|
* 无标题事件时回落为第一条用户消息(截断 80 字符)。
|
|
9180
9321
|
*/
|
|
9181
9322
|
function titleOf(events) {
|
|
9323
|
+
const list = asEventList(events);
|
|
9182
9324
|
let title;
|
|
9183
|
-
for (const event of
|
|
9325
|
+
for (const event of list) {
|
|
9184
9326
|
const raw = event;
|
|
9185
9327
|
if (raw.type === "session/title") {
|
|
9186
9328
|
const t = raw.data.title;
|
|
@@ -9188,7 +9330,7 @@ function titleOf(events) {
|
|
|
9188
9330
|
}
|
|
9189
9331
|
}
|
|
9190
9332
|
if (title !== void 0) return title;
|
|
9191
|
-
for (const event of
|
|
9333
|
+
for (const event of list) if (event.type === "user/message") {
|
|
9192
9334
|
const source = event.data.source;
|
|
9193
9335
|
if (source !== void 0 && source.kind !== "user") continue;
|
|
9194
9336
|
const text = blockText(event.data.content);
|
|
@@ -9200,8 +9342,9 @@ function titleOf(events) {
|
|
|
9200
9342
|
}
|
|
9201
9343
|
/** 日志最大事件序号。 */
|
|
9202
9344
|
function maxSeq(events) {
|
|
9345
|
+
const list = asEventList(events);
|
|
9203
9346
|
let m = -1;
|
|
9204
|
-
for (const event of
|
|
9347
|
+
for (const event of list) if (event.seq > m) m = event.seq;
|
|
9205
9348
|
return m;
|
|
9206
9349
|
}
|
|
9207
9350
|
function sleep(ms) {
|
|
@@ -9223,33 +9366,51 @@ async function waitForReply(opts) {
|
|
|
9223
9366
|
const started = Date.now();
|
|
9224
9367
|
const deadline = started + opts.timeoutMs;
|
|
9225
9368
|
const requireTurnEnd = opts.requireTurnEnd ?? false;
|
|
9369
|
+
const waitForSegment = opts.waitForSegment ?? false;
|
|
9226
9370
|
let latest = null;
|
|
9227
9371
|
let textReply = null;
|
|
9372
|
+
let segment = null;
|
|
9228
9373
|
let turnEnded = false;
|
|
9229
9374
|
for (;;) {
|
|
9230
9375
|
if (opts.signal !== void 0 && opts.signal.aborted) break;
|
|
9231
|
-
const events = opts.session
|
|
9376
|
+
const events = sessionEvents(opts.session);
|
|
9232
9377
|
for (const row of foldMessages(events)) {
|
|
9233
9378
|
if (row.seq <= opts.baselineSeq || row.role !== "assistant") continue;
|
|
9234
9379
|
if (latest === null || row.seq > latest.seq) latest = row;
|
|
9235
9380
|
if (row.text !== void 0 && (textReply === null || row.seq > textReply.seq)) textReply = row;
|
|
9236
9381
|
}
|
|
9382
|
+
if (waitForSegment) {
|
|
9383
|
+
const seg = latestSegmentSince(events, opts.baselineSeq);
|
|
9384
|
+
if (seg !== null && (segment === null || seg.seq > segment.seq)) segment = seg;
|
|
9385
|
+
}
|
|
9237
9386
|
if (requireTurnEnd && latest !== null) {
|
|
9238
9387
|
for (const event of events) if (event.seq > latest.seq && event.type === "turn/end") {
|
|
9239
9388
|
turnEnded = true;
|
|
9240
9389
|
break;
|
|
9241
9390
|
}
|
|
9242
9391
|
}
|
|
9243
|
-
|
|
9392
|
+
let done;
|
|
9393
|
+
if (requireTurnEnd) done = latest !== null && turnEnded;
|
|
9394
|
+
else if (waitForSegment) done = segment !== null;
|
|
9395
|
+
else done = textReply !== null;
|
|
9396
|
+
if (done) break;
|
|
9244
9397
|
if (Date.now() >= deadline) break;
|
|
9245
9398
|
await sleep(100);
|
|
9246
9399
|
}
|
|
9247
|
-
|
|
9400
|
+
let message = waitForSegment && segment !== null ? {
|
|
9401
|
+
seq: segment.seq,
|
|
9402
|
+
time: segment.time,
|
|
9403
|
+
role: "assistant",
|
|
9404
|
+
images: 0,
|
|
9405
|
+
...segment.text !== void 0 ? { text: segment.text } : {},
|
|
9406
|
+
...segment.reasoning !== void 0 ? { reasoning: segment.reasoning } : {},
|
|
9407
|
+
...segment.toolCalls.length > 0 ? { toolCalls: segment.toolCalls } : {}
|
|
9408
|
+
} : textReply ?? latest;
|
|
9248
9409
|
return {
|
|
9249
9410
|
message,
|
|
9250
9411
|
seq: message === null ? opts.baselineSeq : message.seq,
|
|
9251
9412
|
turnEnded,
|
|
9252
|
-
timedOut: requireTurnEnd ? latest !== null && !turnEnded : textReply === null,
|
|
9413
|
+
timedOut: requireTurnEnd ? latest !== null && !turnEnded : waitForSegment ? segment === null : textReply === null,
|
|
9253
9414
|
aborted: opts.signal !== void 0 && opts.signal.aborted,
|
|
9254
9415
|
waitedMs: Date.now() - started
|
|
9255
9416
|
};
|
|
@@ -9309,7 +9470,7 @@ async function attachSessionToWorkspace(ctx, sessionId, cwd) {
|
|
|
9309
9470
|
* 纯读,无副作用。用于 `session_bridge_status`。
|
|
9310
9471
|
*/
|
|
9311
9472
|
function statusSnapshot(ctx, agent) {
|
|
9312
|
-
const events = agent.session
|
|
9473
|
+
const events = sessionEvents(agent.session);
|
|
9313
9474
|
const rows = foldMessages(events);
|
|
9314
9475
|
let lastActivityAt = null;
|
|
9315
9476
|
let lastTurn = 0;
|
|
@@ -9325,13 +9486,18 @@ function statusSnapshot(ctx, agent) {
|
|
|
9325
9486
|
}
|
|
9326
9487
|
const recent = rows.slice(-8);
|
|
9327
9488
|
let lastAssistantText;
|
|
9489
|
+
let lastReasoning;
|
|
9328
9490
|
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
9329
9491
|
const row = rows[i];
|
|
9330
|
-
if (row !== void 0 && row.role === "assistant"
|
|
9331
|
-
lastAssistantText = row.text;
|
|
9332
|
-
|
|
9492
|
+
if (row !== void 0 && row.role === "assistant") {
|
|
9493
|
+
if (lastAssistantText === void 0 && row.text !== void 0) lastAssistantText = row.text;
|
|
9494
|
+
if (lastReasoning === void 0 && row.reasoning !== void 0) lastReasoning = row.reasoning;
|
|
9495
|
+
if (lastAssistantText !== void 0 && lastReasoning !== void 0) break;
|
|
9333
9496
|
}
|
|
9334
9497
|
}
|
|
9498
|
+
const live = liveReasoningSnapshot(events);
|
|
9499
|
+
const liveReasoning = live !== null && live.reasoning !== "" ? live.reasoning : void 0;
|
|
9500
|
+
const reasoningTail = reasoningTailOf(liveReasoning, lastReasoning);
|
|
9335
9501
|
const inbox = agent.inbox;
|
|
9336
9502
|
const cwd = agent.session.header.cwd;
|
|
9337
9503
|
return {
|
|
@@ -9349,6 +9515,9 @@ function statusSnapshot(ctx, agent) {
|
|
|
9349
9515
|
nextTurnCount: inbox?.nextTurn?.length ?? 0,
|
|
9350
9516
|
nextStepCount: inbox?.nextStep?.length ?? 0,
|
|
9351
9517
|
...lastAssistantText === void 0 ? {} : { lastAssistantText },
|
|
9518
|
+
...lastReasoning === void 0 ? {} : { lastReasoning },
|
|
9519
|
+
...liveReasoning === void 0 ? {} : { liveReasoning },
|
|
9520
|
+
...reasoningTail === void 0 ? {} : { reasoningTail },
|
|
9352
9521
|
messageCount: rows.length,
|
|
9353
9522
|
recent
|
|
9354
9523
|
};
|
|
@@ -9403,6 +9572,16 @@ var SessionMonitor = class {
|
|
|
9403
9572
|
start(config) {
|
|
9404
9573
|
const existing = this.entries.get(config.sessionId);
|
|
9405
9574
|
const now = Date.now();
|
|
9575
|
+
const cotInit = { rules: (config.coTRules ?? []).reduce((acc, rule, index) => {
|
|
9576
|
+
const key = String(index) + ":" + rule.match + ":" + rule.field + ":" + rule.value;
|
|
9577
|
+
acc[key] = {
|
|
9578
|
+
hits: 0,
|
|
9579
|
+
lastFiredAt: null,
|
|
9580
|
+
lastMatch: false,
|
|
9581
|
+
lastSubject: ""
|
|
9582
|
+
};
|
|
9583
|
+
return acc;
|
|
9584
|
+
}, {}) };
|
|
9406
9585
|
const entry = existing !== void 0 ? {
|
|
9407
9586
|
...existing,
|
|
9408
9587
|
config,
|
|
@@ -9416,8 +9595,10 @@ var SessionMonitor = class {
|
|
|
9416
9595
|
lastActionAt: null,
|
|
9417
9596
|
lastNote: "监控已启动",
|
|
9418
9597
|
done: false,
|
|
9419
|
-
cycles: 0
|
|
9598
|
+
cycles: 0,
|
|
9599
|
+
cot: cotInit
|
|
9420
9600
|
};
|
|
9601
|
+
if (entry.cot === void 0) entry.cot = cotInit;
|
|
9421
9602
|
this.entries.set(config.sessionId, entry);
|
|
9422
9603
|
this.ensureTimer();
|
|
9423
9604
|
this.log(entry, "monitor start session=" + config.sessionId + " interval=" + config.intervalMs + " stalled=" + String(config.stalledMs ?? 6e4) + " maxStuck=" + String(config.maxStuckCycles ?? 3));
|
|
@@ -9543,6 +9724,7 @@ var SessionMonitor = class {
|
|
|
9543
9724
|
this.log(entry, "STALL(#" + entry.stuckCount + ") → steer: " + steer);
|
|
9544
9725
|
return;
|
|
9545
9726
|
}
|
|
9727
|
+
if (this.evaluateCoTRules(entry, snapshot, sessionId)) return;
|
|
9546
9728
|
if (entry.stuckCount !== 0) entry.stuckCount = 0;
|
|
9547
9729
|
entry.lastAction = "steady";
|
|
9548
9730
|
entry.lastActionAt = Date.now();
|
|
@@ -9567,6 +9749,76 @@ var SessionMonitor = class {
|
|
|
9567
9749
|
if (!text) return false;
|
|
9568
9750
|
return keywords.some((kw) => kw !== "" && text.includes(kw));
|
|
9569
9751
|
}
|
|
9752
|
+
/**
|
|
9753
|
+
* 评估思维链(CoT)规则。规则只对 live 会话有意义:running 时用进行中的
|
|
9754
|
+
* reasoning-delta 流,空闲时回落到已定型 lastReasoning。not-contains 规则作用在
|
|
9755
|
+
* reasoning 字段时,若该会话本就没有推理(非推理模型/尚未产出),不判定为命中,
|
|
9756
|
+
* 避免误 cancel 一个根本不产生思维链的会话。
|
|
9757
|
+
* @returns true 表示本 tick 已被某条规则触发并处理(steer/cancel),调用方应 return。
|
|
9758
|
+
*/
|
|
9759
|
+
evaluateCoTRules(entry, snapshot, sessionId) {
|
|
9760
|
+
const rules = entry.config.coTRules ?? [];
|
|
9761
|
+
if (rules.length === 0) return false;
|
|
9762
|
+
const minHits = entry.config.cotMinHits ?? 1;
|
|
9763
|
+
const hasLive = (snapshot.liveReasoning ?? "") !== "";
|
|
9764
|
+
const subject = {
|
|
9765
|
+
reasoning: snapshot.liveReasoning ?? snapshot.lastReasoning ?? "",
|
|
9766
|
+
text: snapshot.lastAssistantText ?? "",
|
|
9767
|
+
hasReasoning: hasLive || (snapshot.lastReasoning ?? "") !== ""
|
|
9768
|
+
};
|
|
9769
|
+
const now = Date.now();
|
|
9770
|
+
const rulesState = entry.cot;
|
|
9771
|
+
let engaged = false;
|
|
9772
|
+
rules.forEach((rule, index) => {
|
|
9773
|
+
if (engaged) return;
|
|
9774
|
+
const key = String(index) + ":" + rule.match + ":" + rule.field + ":" + rule.value;
|
|
9775
|
+
const st = rulesState.rules[key] ?? {
|
|
9776
|
+
hits: 0,
|
|
9777
|
+
lastFiredAt: null,
|
|
9778
|
+
lastMatch: false
|
|
9779
|
+
};
|
|
9780
|
+
if (rule.field === "reasoning" && rule.match === "not-contains" && !subject.hasReasoning) {
|
|
9781
|
+
st.lastMatch = false;
|
|
9782
|
+
st.hits = 0;
|
|
9783
|
+
return;
|
|
9784
|
+
}
|
|
9785
|
+
let hit;
|
|
9786
|
+
if (rule.field === "reasoning") hit = rule.match === "contains" ? subject.reasoning.includes(rule.value) : !subject.reasoning.includes(rule.value);
|
|
9787
|
+
else if (rule.field === "text") hit = rule.match === "contains" ? subject.text.includes(rule.value) : !subject.text.includes(rule.value);
|
|
9788
|
+
else {
|
|
9789
|
+
const reasonHas = subject.reasoning.includes(rule.value);
|
|
9790
|
+
const textHas = subject.text.includes(rule.value);
|
|
9791
|
+
hit = rule.match === "contains" ? reasonHas || textHas : !reasonHas && !textHas;
|
|
9792
|
+
}
|
|
9793
|
+
st.lastMatch = hit;
|
|
9794
|
+
st.hits = hit ? st.hits + 1 : 0;
|
|
9795
|
+
this.log(entry, "CoT rule " + key + " hit=" + String(hit) + " hits=" + String(st.hits) + "/" + String(minHits));
|
|
9796
|
+
if (!(hit && st.hits >= minHits)) return;
|
|
9797
|
+
const coolMs = Math.max(entry.config.intervalMs, 5e3) * 2;
|
|
9798
|
+
if (st.lastFiredAt !== null && now - st.lastFiredAt < coolMs) return;
|
|
9799
|
+
st.lastFiredAt = now;
|
|
9800
|
+
if (rule.action === "cancel") {
|
|
9801
|
+
const reason = "reasoning-rule:" + rule.value;
|
|
9802
|
+
cancelLiveSession(this.ctx, sessionId, false, reason);
|
|
9803
|
+
entry.lastAction = "cot-cancel";
|
|
9804
|
+
entry.lastActionAt = Date.now();
|
|
9805
|
+
entry.lastNote = "思维链规则触发 cancel: " + rule.value;
|
|
9806
|
+
entry.stuckCount = 0;
|
|
9807
|
+
this.log(entry, "COT-CANCEL session=" + sessionId + " rule=" + key + " value=" + rule.value);
|
|
9808
|
+
} else {
|
|
9809
|
+
const steer = rule.message !== void 0 && rule.message.trim() !== "" ? rule.message.trim() : "监控判定:思维链/回复匹配规则「" + rule.value + "」,请据此调整方向并继续推进任务。";
|
|
9810
|
+
sendLiveMessage(this.ctx, sessionId, steer, "steer");
|
|
9811
|
+
entry.lastAction = "cot-steer";
|
|
9812
|
+
entry.lastActionAt = Date.now();
|
|
9813
|
+
entry.lastNote = "思维链规则触发 steer: " + rule.value;
|
|
9814
|
+
entry.stuckCount = 0;
|
|
9815
|
+
this.log(entry, "COT-STEER session=" + sessionId + " rule=" + key + " steer=" + steer);
|
|
9816
|
+
}
|
|
9817
|
+
engaged = true;
|
|
9818
|
+
st.hits = 0;
|
|
9819
|
+
});
|
|
9820
|
+
return engaged;
|
|
9821
|
+
}
|
|
9570
9822
|
hasLlm() {
|
|
9571
9823
|
return this.ctx.llm !== void 0;
|
|
9572
9824
|
}
|
|
@@ -9622,7 +9874,7 @@ var SessionMonitor = class {
|
|
|
9622
9874
|
}
|
|
9623
9875
|
};
|
|
9624
9876
|
//#endregion
|
|
9625
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-
|
|
9877
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invaria_51e6fefef6ddce4f991b3d02344a1a72/node_modules/@deepseek-ai/dsh-scope/lib/index.js
|
|
9626
9878
|
/**
|
|
9627
9879
|
* Shared insertion-ordered storage and effect ownership for scope-aware registries.
|
|
9628
9880
|
*
|
|
@@ -9901,7 +10153,7 @@ function scopeTarget(base, key) {
|
|
|
9901
10153
|
return carrier;
|
|
9902
10154
|
}
|
|
9903
10155
|
//#endregion
|
|
9904
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1.2-alpha.
|
|
10156
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1.2-alpha.2_96d3de6f0f5da790676b5bfb20e4a981/node_modules/@deepseek-ai/dsh-tools/lib/index.js
|
|
9905
10157
|
/**
|
|
9906
10158
|
* Enforced JSON Schema subset shared by tool outputs, generated PTC mode
|
|
9907
10159
|
* types, subagents, and workflows. The subset accepts any JSON root, an
|
|
@@ -13403,7 +13655,7 @@ function toolAbortedBeforeDispatchResult(prior) {
|
|
|
13403
13655
|
};
|
|
13404
13656
|
}
|
|
13405
13657
|
//#endregion
|
|
13406
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-agent@0.1.2-alpha.
|
|
13658
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-agent@0.1.2-alpha.2_82401c175458b799db42b9e8f7dd7700/node_modules/@deepseek-ai/dsh-agent/lib/index.js
|
|
13407
13659
|
/**
|
|
13408
13660
|
* Agent-scoped model selection shared by runtime entry points.
|
|
13409
13661
|
* @module @deepseek-ai/dsh-agent/model-selection
|
|
@@ -13633,7 +13885,7 @@ function registerCreate(env) {
|
|
|
13633
13885
|
});
|
|
13634
13886
|
let reply;
|
|
13635
13887
|
if (typeof args.prompt === "string" && args.prompt.trim() !== "") {
|
|
13636
|
-
const baseline = maxSeq(agent.session
|
|
13888
|
+
const baseline = maxSeq(sessionEvents(agent.session));
|
|
13637
13889
|
agent.followup(userMessage(args.prompt.trim()));
|
|
13638
13890
|
env.registry.touch(sessionId);
|
|
13639
13891
|
if (args.waitForReply === true) reply = await maybeWait(env, agent.session, baseline, args, exec.signal);
|
|
@@ -13701,7 +13953,7 @@ function registerSend(env) {
|
|
|
13701
13953
|
if (typeof args.message !== "string" || args.message.trim() === "") throw new Error("invalid message: expected a non-empty string");
|
|
13702
13954
|
const agent = liveAgent(env, args.sessionId);
|
|
13703
13955
|
if (agent === void 0) 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)");
|
|
13704
|
-
const baseline = maxSeq(agent.session
|
|
13956
|
+
const baseline = maxSeq(sessionEvents(agent.session));
|
|
13705
13957
|
if (args.mode === "steer") agent.steer(userMessage(args.message.trim()));
|
|
13706
13958
|
else agent.followup(userMessage(args.message.trim()));
|
|
13707
13959
|
const headerCwd = agent.session.header.cwd;
|
|
@@ -13764,7 +14016,7 @@ function registerResume(env) {
|
|
|
13764
14016
|
sessionId: args.sessionId,
|
|
13765
14017
|
alreadyLive: true,
|
|
13766
14018
|
...existing.session.header.cwd === void 0 ? {} : { cwd: existing.session.header.cwd },
|
|
13767
|
-
title: titleOf(existing.session
|
|
14019
|
+
title: titleOf(sessionEvents(existing.session)) ?? null,
|
|
13768
14020
|
running: existing.status === "running"
|
|
13769
14021
|
});
|
|
13770
14022
|
}
|
|
@@ -13797,7 +14049,7 @@ function registerResume(env) {
|
|
|
13797
14049
|
sessionId: args.sessionId,
|
|
13798
14050
|
resumed: true,
|
|
13799
14051
|
...headerCwd === void 0 ? {} : { cwd: headerCwd },
|
|
13800
|
-
title: titleOf(handle.agent.session
|
|
14052
|
+
title: titleOf(sessionEvents(handle.agent.session)) ?? null
|
|
13801
14053
|
});
|
|
13802
14054
|
}
|
|
13803
14055
|
}));
|
|
@@ -13805,7 +14057,7 @@ function registerResume(env) {
|
|
|
13805
14057
|
function registerWait(env) {
|
|
13806
14058
|
env.ctx.tools.register(defineTool({
|
|
13807
14059
|
name: "session_bridge_wait",
|
|
13808
|
-
description: "Wait for a session next assistant
|
|
14060
|
+
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).",
|
|
13809
14061
|
parameters: {
|
|
13810
14062
|
sessionId: {
|
|
13811
14063
|
type: "string",
|
|
@@ -13823,6 +14075,11 @@ function registerWait(env) {
|
|
|
13823
14075
|
requireTurnEnd: {
|
|
13824
14076
|
type: "boolean",
|
|
13825
14077
|
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)."
|
|
14078
|
+
},
|
|
14079
|
+
waitFor: {
|
|
14080
|
+
type: "string",
|
|
14081
|
+
enum: ["reply", "segment"],
|
|
14082
|
+
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."
|
|
13826
14083
|
}
|
|
13827
14084
|
},
|
|
13828
14085
|
output: {
|
|
@@ -13851,11 +14108,15 @@ function registerWait(env) {
|
|
|
13851
14108
|
if (typeof args.sessionId !== "string" || args.sessionId.trim() === "") throw new Error("invalid sessionId: expected a non-empty string");
|
|
13852
14109
|
const agent = liveAgent(env, args.sessionId);
|
|
13853
14110
|
if (agent === void 0) throw new Error("session " + JSON.stringify(args.sessionId) + " is not live — call session_bridge_resume first (waiting requires a live session)");
|
|
14111
|
+
const waitSegment = args.waitFor === "segment";
|
|
13854
14112
|
let baseline;
|
|
13855
14113
|
if (typeof args.sinceSeq === "number" && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0) baseline = args.sinceSeq;
|
|
13856
|
-
else {
|
|
14114
|
+
else if (waitSegment) {
|
|
14115
|
+
const segs = segmentsSince(sessionEvents(agent.session));
|
|
14116
|
+
baseline = segs.length === 0 ? -1 : segs[segs.length - 1]?.seq ?? -1;
|
|
14117
|
+
} else {
|
|
13857
14118
|
let lastText = -1;
|
|
13858
|
-
for (const row of foldMessages(agent.session
|
|
14119
|
+
for (const row of foldMessages(sessionEvents(agent.session))) if (row.text !== void 0) lastText = row.seq;
|
|
13859
14120
|
baseline = lastText;
|
|
13860
14121
|
}
|
|
13861
14122
|
const result = await waitForReply({
|
|
@@ -13863,7 +14124,8 @@ function registerWait(env) {
|
|
|
13863
14124
|
baselineSeq: baseline,
|
|
13864
14125
|
timeoutMs: clampTimeout(args.timeoutMs),
|
|
13865
14126
|
signal: exec.signal,
|
|
13866
|
-
requireTurnEnd: args.requireTurnEnd === true
|
|
14127
|
+
requireTurnEnd: args.requireTurnEnd === true,
|
|
14128
|
+
...waitSegment ? { waitForSegment: true } : {}
|
|
13867
14129
|
});
|
|
13868
14130
|
env.registry.touch(args.sessionId);
|
|
13869
14131
|
return asJson({
|
|
@@ -13874,6 +14136,83 @@ function registerWait(env) {
|
|
|
13874
14136
|
}
|
|
13875
14137
|
}));
|
|
13876
14138
|
}
|
|
14139
|
+
function registerSegments(env) {
|
|
14140
|
+
env.ctx.tools.register(defineTool({
|
|
14141
|
+
name: "session_bridge_segments",
|
|
14142
|
+
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.",
|
|
14143
|
+
parameters: {
|
|
14144
|
+
sessionId: {
|
|
14145
|
+
type: "string",
|
|
14146
|
+
required: true,
|
|
14147
|
+
description: "Session id to read segments from (live or offline)."
|
|
14148
|
+
},
|
|
14149
|
+
sinceSeq: {
|
|
14150
|
+
type: "number",
|
|
14151
|
+
description: "Only return completed segments after this event seq (paging cursor)."
|
|
14152
|
+
},
|
|
14153
|
+
limit: {
|
|
14154
|
+
type: "number",
|
|
14155
|
+
description: "Maximum number of segments to return (default 10, max 50)."
|
|
14156
|
+
}
|
|
14157
|
+
},
|
|
14158
|
+
output: {
|
|
14159
|
+
schema: {
|
|
14160
|
+
type: "object",
|
|
14161
|
+
additionalProperties: true
|
|
14162
|
+
},
|
|
14163
|
+
render: (_args, value) => {
|
|
14164
|
+
const segs = value.segments ?? [];
|
|
14165
|
+
if (segs.length === 0) return [{
|
|
14166
|
+
type: "text",
|
|
14167
|
+
text: "(no completed segments)"
|
|
14168
|
+
}];
|
|
14169
|
+
return [{
|
|
14170
|
+
type: "text",
|
|
14171
|
+
text: segs.map((sg) => {
|
|
14172
|
+
const head = "seg #" + String(sg.seq) + " (turn " + String(sg.turn) + " step " + String(sg.step) + ")";
|
|
14173
|
+
const text = typeof sg.text === "string" ? sg.text : "";
|
|
14174
|
+
const reason = typeof sg.reasoning === "string" && sg.reasoning !== "" ? " [reasoning " + sg.reasoning.length + " chars]" : "";
|
|
14175
|
+
return head + (Array.isArray(sg.toolCalls) && sg.toolCalls.length > 0 ? " [tools: " + sg.toolCalls.join(",") + "]" : "") + reason + ": " + text.slice(0, 160);
|
|
14176
|
+
}).join("\n")
|
|
14177
|
+
}];
|
|
14178
|
+
}
|
|
14179
|
+
},
|
|
14180
|
+
async execute(args) {
|
|
14181
|
+
const agent = liveAgent(env, args.sessionId);
|
|
14182
|
+
let events;
|
|
14183
|
+
let live;
|
|
14184
|
+
if (agent !== void 0) {
|
|
14185
|
+
events = sessionEvents(agent.session);
|
|
14186
|
+
live = true;
|
|
14187
|
+
} else try {
|
|
14188
|
+
events = (await env.ctx.sessionPersistence.inspect(args.sessionId)).events;
|
|
14189
|
+
live = false;
|
|
14190
|
+
} catch (error) {
|
|
14191
|
+
throw new Error(String(error));
|
|
14192
|
+
}
|
|
14193
|
+
const sinceSeq = typeof args.sinceSeq === "number" && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0 ? args.sinceSeq : 0;
|
|
14194
|
+
clampLimit(args.limit, 10, 50);
|
|
14195
|
+
const segs = segmentsSince(events, sinceSeq);
|
|
14196
|
+
env.registry.touch(args.sessionId);
|
|
14197
|
+
const nextCursorSeq = segs.length === 0 ? sinceSeq : segs[segs.length - 1]?.seq ?? sinceSeq;
|
|
14198
|
+
return asJson({
|
|
14199
|
+
sessionId: args.sessionId,
|
|
14200
|
+
live,
|
|
14201
|
+
nextCursorSeq,
|
|
14202
|
+
segments: segs.map((sg) => ({
|
|
14203
|
+
seq: sg.seq,
|
|
14204
|
+
time: sg.time,
|
|
14205
|
+
turn: sg.turn,
|
|
14206
|
+
step: sg.step,
|
|
14207
|
+
openTurn: sg.openTurn,
|
|
14208
|
+
...sg.text === void 0 ? {} : { text: sg.text },
|
|
14209
|
+
...sg.reasoning === void 0 ? {} : { reasoning: sg.reasoning },
|
|
14210
|
+
...sg.toolCalls.length > 0 ? { toolCalls: sg.toolCalls } : {}
|
|
14211
|
+
}))
|
|
14212
|
+
});
|
|
14213
|
+
}
|
|
14214
|
+
}));
|
|
14215
|
+
}
|
|
13877
14216
|
function registerRead(env) {
|
|
13878
14217
|
env.ctx.tools.register(defineTool({
|
|
13879
14218
|
name: "session_bridge_read",
|
|
@@ -13934,7 +14273,7 @@ function registerRead(env) {
|
|
|
13934
14273
|
let live;
|
|
13935
14274
|
let cwd;
|
|
13936
14275
|
if (agent !== void 0) {
|
|
13937
|
-
events = agent.session
|
|
14276
|
+
events = sessionEvents(agent.session);
|
|
13938
14277
|
live = true;
|
|
13939
14278
|
cwd = agent.session.header.cwd;
|
|
13940
14279
|
} else {
|
|
@@ -14051,7 +14390,7 @@ function registerFind(env) {
|
|
|
14051
14390
|
const wsId = wsById.get(agent.id) ?? workspaceByPath(env.ctx, headerCwd);
|
|
14052
14391
|
items.push({
|
|
14053
14392
|
sessionId: agent.id,
|
|
14054
|
-
title: titleOf(agent.session
|
|
14393
|
+
title: titleOf(sessionEvents(agent.session)),
|
|
14055
14394
|
...headerCwd === void 0 ? {} : { cwd: headerCwd },
|
|
14056
14395
|
...wsId === void 0 ? {} : { workspaceId: wsId },
|
|
14057
14396
|
live: true,
|
|
@@ -14154,6 +14493,7 @@ function registerBridgeTools(env) {
|
|
|
14154
14493
|
registerSend(env);
|
|
14155
14494
|
registerResume(env);
|
|
14156
14495
|
registerWait(env);
|
|
14496
|
+
registerSegments(env);
|
|
14157
14497
|
registerRead(env);
|
|
14158
14498
|
registerFind(env);
|
|
14159
14499
|
registerStatus(env);
|
|
@@ -14161,11 +14501,33 @@ function registerBridgeTools(env) {
|
|
|
14161
14501
|
registerMonitor(env);
|
|
14162
14502
|
registerArchive(env);
|
|
14163
14503
|
}
|
|
14504
|
+
/** 校验并规范化 coRules 参数为 CoTRule[];非法项抛清晰错误。 */
|
|
14505
|
+
function parseCoRules(raw) {
|
|
14506
|
+
return raw.map((rule, index) => {
|
|
14507
|
+
return {
|
|
14508
|
+
match: rule.match === "contains" || rule.match === "not-contains" ? rule.match : (() => {
|
|
14509
|
+
throw new Error("coRules[" + index + "].match must be \"contains\" or \"not-contains\"");
|
|
14510
|
+
})(),
|
|
14511
|
+
field: rule.field === "reasoning" || rule.field === "text" || rule.field === "both" ? rule.field : (() => {
|
|
14512
|
+
throw new Error("coRules[" + index + "].field must be \"reasoning\", \"text\" or \"both\"");
|
|
14513
|
+
})(),
|
|
14514
|
+
action: rule.action === "steer" || rule.action === "cancel" ? rule.action : (() => {
|
|
14515
|
+
throw new Error("coRules[" + index + "].action must be \"steer\" or \"cancel\"");
|
|
14516
|
+
})(),
|
|
14517
|
+
value: typeof rule.value === "string" && rule.value.trim() !== "" ? rule.value.trim() : (() => {
|
|
14518
|
+
throw new Error("coRules[" + index + "].value must be a non-empty string");
|
|
14519
|
+
})(),
|
|
14520
|
+
...typeof rule.message === "string" && rule.message.trim() !== "" ? { message: rule.message.trim() } : {}
|
|
14521
|
+
};
|
|
14522
|
+
});
|
|
14523
|
+
}
|
|
14164
14524
|
function renderMonitorState(entry) {
|
|
14165
14525
|
const lines = [];
|
|
14166
14526
|
lines.push(`monitoring ${entry.config.sessionId}${entry.config.label === void 0 ? "" : " (\"" + entry.config.label + "\")"}`);
|
|
14167
14527
|
lines.push(`interval: ${entry.config.intervalMs}ms | stalled: ${String(entry.config.stalledMs ?? 6e4)}ms | maxStuck: ${String(entry.config.maxStuckCycles ?? 3)}`);
|
|
14168
14528
|
lines.push(`cycles: ${entry.cycles} | stuck: ${entry.stuckCount} | lastAction: ${entry.lastAction}`);
|
|
14529
|
+
const cotCount = (entry.config.coTRules ?? []).length;
|
|
14530
|
+
if (cotCount > 0) lines.push(`coRules: ${cotCount} rule(s) (minHits ${String(entry.config.cotMinHits ?? 1)})`);
|
|
14169
14531
|
if (entry.lastNote !== "") lines.push(`note: ${entry.lastNote}`);
|
|
14170
14532
|
if (entry.done) lines.push("done: yes");
|
|
14171
14533
|
return lines;
|
|
@@ -14173,7 +14535,7 @@ function renderMonitorState(entry) {
|
|
|
14173
14535
|
function registerMonitor(env) {
|
|
14174
14536
|
env.ctx.tools.register(defineTool({
|
|
14175
14537
|
name: "session_bridge_monitor_start",
|
|
14176
|
-
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.",
|
|
14538
|
+
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.",
|
|
14177
14539
|
parameters: {
|
|
14178
14540
|
sessionId: {
|
|
14179
14541
|
type: "string",
|
|
@@ -14212,6 +14574,51 @@ function registerMonitor(env) {
|
|
|
14212
14574
|
label: {
|
|
14213
14575
|
type: "string",
|
|
14214
14576
|
description: "Optional human label for logs/display."
|
|
14577
|
+
},
|
|
14578
|
+
coRules: {
|
|
14579
|
+
type: "array",
|
|
14580
|
+
items: {
|
|
14581
|
+
type: "object",
|
|
14582
|
+
additionalProperties: false,
|
|
14583
|
+
properties: {
|
|
14584
|
+
match: {
|
|
14585
|
+
type: "string",
|
|
14586
|
+
required: true,
|
|
14587
|
+
enum: ["contains", "not-contains"],
|
|
14588
|
+
description: "contains = must include value in the chosen field; not-contains = must NOT include it."
|
|
14589
|
+
},
|
|
14590
|
+
field: {
|
|
14591
|
+
type: "string",
|
|
14592
|
+
required: true,
|
|
14593
|
+
enum: [
|
|
14594
|
+
"reasoning",
|
|
14595
|
+
"text",
|
|
14596
|
+
"both"
|
|
14597
|
+
],
|
|
14598
|
+
description: "Match on reasoning (chain-of-thought), text (reply), or both (either)."
|
|
14599
|
+
},
|
|
14600
|
+
value: {
|
|
14601
|
+
type: "string",
|
|
14602
|
+
required: true,
|
|
14603
|
+
description: "The substring to match (non-empty)."
|
|
14604
|
+
},
|
|
14605
|
+
action: {
|
|
14606
|
+
type: "string",
|
|
14607
|
+
required: true,
|
|
14608
|
+
enum: ["steer", "cancel"],
|
|
14609
|
+
description: "steer injects a guiding message; cancel terminates the session."
|
|
14610
|
+
},
|
|
14611
|
+
message: {
|
|
14612
|
+
type: "string",
|
|
14613
|
+
description: "Custom steer text (default is a guidance prompt)."
|
|
14614
|
+
}
|
|
14615
|
+
}
|
|
14616
|
+
},
|
|
14617
|
+
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."
|
|
14618
|
+
},
|
|
14619
|
+
cotMinHits: {
|
|
14620
|
+
type: "number",
|
|
14621
|
+
description: "How many consecutive matched polls before a CoT rule fires (default 1 = immediately)."
|
|
14215
14622
|
}
|
|
14216
14623
|
},
|
|
14217
14624
|
output: {
|
|
@@ -14242,7 +14649,9 @@ function registerMonitor(env) {
|
|
|
14242
14649
|
...args.useLlm === true ? { useLlm: true } : {},
|
|
14243
14650
|
...typeof args.onStallSteer === "string" && args.onStallSteer.trim() !== "" ? { onStallSteer: args.onStallSteer.trim() } : {},
|
|
14244
14651
|
...typeof args.onOffTrackSteer === "string" && args.onOffTrackSteer.trim() !== "" ? { onOffTrackSteer: args.onOffTrackSteer.trim() } : {},
|
|
14245
|
-
...typeof args.label === "string" && args.label.trim() !== "" ? { label: args.label.trim() } : {}
|
|
14652
|
+
...typeof args.label === "string" && args.label.trim() !== "" ? { label: args.label.trim() } : {},
|
|
14653
|
+
...Array.isArray(args.coRules) && args.coRules.length > 0 ? { coTRules: parseCoRules(args.coRules) } : {},
|
|
14654
|
+
...typeof args.cotMinHits === "number" && Number.isInteger(args.cotMinHits) && args.cotMinHits >= 1 ? { cotMinHits: Math.floor(args.cotMinHits) } : {}
|
|
14246
14655
|
};
|
|
14247
14656
|
const entry = env.monitor.start(config);
|
|
14248
14657
|
env.registry.touch(args.sessionId);
|
|
@@ -14314,7 +14723,19 @@ function registerMonitor(env) {
|
|
|
14314
14723
|
}
|
|
14315
14724
|
}));
|
|
14316
14725
|
}
|
|
14317
|
-
/**
|
|
14726
|
+
/** 按 reasoning 选项裁剪快照里的思维链字段(避免入 token 时有 diff 语义差)。 */
|
|
14727
|
+
function pruneReasoning(snapshot, mode) {
|
|
14728
|
+
if (mode === "tail") return snapshot;
|
|
14729
|
+
const rest = { ...snapshot };
|
|
14730
|
+
delete rest.reasoningTail;
|
|
14731
|
+
if (mode === "none") {
|
|
14732
|
+
delete rest.lastReasoning;
|
|
14733
|
+
delete rest.liveReasoning;
|
|
14734
|
+
} else if (mode === "last") delete rest.liveReasoning;
|
|
14735
|
+
else delete rest.lastReasoning;
|
|
14736
|
+
return rest;
|
|
14737
|
+
}
|
|
14738
|
+
/** 渲染监控快照为一行摘要:运行态 + openTurn + 卡住/待处理 + 最新回复(+ 思维链预览)。 */
|
|
14318
14739
|
function renderStatus(snapshot, stalledMsThreshold) {
|
|
14319
14740
|
const lines = [];
|
|
14320
14741
|
const runLabel = snapshot.running === "running" ? "running" : "idle";
|
|
@@ -14327,12 +14748,16 @@ function renderStatus(snapshot, stalledMsThreshold) {
|
|
|
14327
14748
|
}
|
|
14328
14749
|
if (snapshot.pendingWork) lines.push(`pendingWork: ${snapshot.nextTurnCount} turn + ${snapshot.nextStepCount} step`);
|
|
14329
14750
|
if (snapshot.lastAssistantText !== void 0) lines.push(`lastReply: ${snapshot.lastAssistantText}`);
|
|
14751
|
+
if (snapshot.reasoningTail !== void 0 && snapshot.reasoningTail !== "") {
|
|
14752
|
+
const preview = snapshot.reasoningTail.length > 160 ? snapshot.reasoningTail.slice(0, 160) + "…" : snapshot.reasoningTail;
|
|
14753
|
+
lines.push(`reasoning: ${preview}`);
|
|
14754
|
+
}
|
|
14330
14755
|
return lines;
|
|
14331
14756
|
}
|
|
14332
14757
|
function registerStatus(env) {
|
|
14333
14758
|
env.ctx.tools.register(defineTool({
|
|
14334
14759
|
name: "session_bridge_status",
|
|
14335
|
-
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.",
|
|
14760
|
+
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.",
|
|
14336
14761
|
parameters: {
|
|
14337
14762
|
sessionId: {
|
|
14338
14763
|
type: "string",
|
|
@@ -14346,6 +14771,16 @@ function registerStatus(env) {
|
|
|
14346
14771
|
recent: {
|
|
14347
14772
|
type: "number",
|
|
14348
14773
|
description: "Number of recent messages to include in the snapshot (default 8, max 20)."
|
|
14774
|
+
},
|
|
14775
|
+
reasoning: {
|
|
14776
|
+
type: "string",
|
|
14777
|
+
enum: [
|
|
14778
|
+
"none",
|
|
14779
|
+
"last",
|
|
14780
|
+
"live",
|
|
14781
|
+
"tail"
|
|
14782
|
+
],
|
|
14783
|
+
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."
|
|
14349
14784
|
}
|
|
14350
14785
|
},
|
|
14351
14786
|
output: {
|
|
@@ -14371,7 +14806,8 @@ function registerStatus(env) {
|
|
|
14371
14806
|
const agent = liveAgent(env, args.sessionId);
|
|
14372
14807
|
if (agent === void 0) throw new Error("session " + JSON.stringify(args.sessionId) + " is not live — call session_bridge_resume first (status requires a live session)");
|
|
14373
14808
|
const threshold = typeof args.stalledMsThreshold === "number" && Number.isFinite(args.stalledMsThreshold) && args.stalledMsThreshold >= 0 ? args.stalledMsThreshold : 6e4;
|
|
14374
|
-
const
|
|
14809
|
+
const reasoningMode = args.reasoning === "none" || args.reasoning === "last" || args.reasoning === "live" ? args.reasoning : "tail";
|
|
14810
|
+
const snapshot = pruneReasoning(statusSnapshot(env.ctx, agent), reasoningMode);
|
|
14375
14811
|
const shown = typeof args.recent === "number" && Number.isInteger(args.recent) && args.recent >= 0 ? Math.min(args.recent, 20) : 8;
|
|
14376
14812
|
const envCwd = agent.session.header.cwd;
|
|
14377
14813
|
env.registry.touch(args.sessionId, {
|