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/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.
|
|
7
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-home-paths@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/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.
|
|
2203
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.1.5-alpha.2_@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.
|
|
2348
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/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.
|
|
2560
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-util-crypto@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/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,16 +2570,16 @@ 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.
|
|
2573
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-brand/lib/index.js
|
|
2574
2574
|
/**
|
|
2575
|
-
* Duplicate-install-safe nominal
|
|
2575
|
+
* Duplicate-install-safe nominal primitive helpers.
|
|
2576
2576
|
*
|
|
2577
|
-
* A brand makes structurally
|
|
2578
|
-
* level: a `SessionId` cannot be passed where a `ToolCallId` is
|
|
2579
|
-
*
|
|
2580
|
-
* serialization
|
|
2577
|
+
* A brand makes structurally identical strings or numbers non-interchangeable
|
|
2578
|
+
* at the type level: a `SessionId` cannot be passed where a `ToolCallId` is
|
|
2579
|
+
* expected, and an event sequence cannot be passed as a log offset. Comparison,
|
|
2580
|
+
* logging, and serialization retain the underlying primitive behavior.
|
|
2581
2581
|
*
|
|
2582
|
-
* This package owns no concrete
|
|
2582
|
+
* This package owns no concrete domain value and keeps no runtime identity or mutable
|
|
2583
2583
|
* state, so independently installed copies produce interchangeable values.
|
|
2584
2584
|
*
|
|
2585
2585
|
* @module @deepseek-ai/dsh-brand
|
|
@@ -3187,11 +3187,19 @@ 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.
|
|
3190
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.1.5-alpha.2_@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.
|
|
3194
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-llm/lib/index.js
|
|
3195
|
+
/**
|
|
3196
|
+
* Bound one `notice` summary to {@link CONTEXT_SUMMARY_MAX_CHARS}.
|
|
3197
|
+
* @param summary - the producer's one-line account, of any length.
|
|
3198
|
+
* @returns the account, ellipsized when it exceeds the bound.
|
|
3199
|
+
*/
|
|
3200
|
+
function boundContextSummary(summary) {
|
|
3201
|
+
return summary.length <= 120 ? summary : `${summary.slice(0, 119)}…`;
|
|
3202
|
+
}
|
|
3195
3203
|
/**
|
|
3196
3204
|
* Detach and deep-freeze a message whose identity already exists.
|
|
3197
3205
|
* @param message - complete message, including its stable identity.
|
|
@@ -3457,6 +3465,9 @@ function errorMessage$1(error) {
|
|
|
3457
3465
|
function harnessErrorCode(error) {
|
|
3458
3466
|
return error instanceof HarnessError ? error.code : "UNKNOWN";
|
|
3459
3467
|
}
|
|
3468
|
+
function quoted(value) {
|
|
3469
|
+
return JSON.stringify(value);
|
|
3470
|
+
}
|
|
3460
3471
|
/**
|
|
3461
3472
|
* Stable text shown to a model that cannot accept one durable image reference.
|
|
3462
3473
|
* @param ref - durable normalized attachment omitted from the request.
|
|
@@ -3476,6 +3487,76 @@ function textOnlyImageText(ref) {
|
|
|
3476
3487
|
function contentHasImage(content) {
|
|
3477
3488
|
return content.some((block) => block.type === "image" || block.type === "tool-result" && contentHasImage(block.content));
|
|
3478
3489
|
}
|
|
3490
|
+
/**
|
|
3491
|
+
* True when typed model content contains a file block, walking nested
|
|
3492
|
+
* tool-result content on the same recursion every file policy shares.
|
|
3493
|
+
* Reads current content on every call without retaining scan results.
|
|
3494
|
+
* @param content - typed model content blocks.
|
|
3495
|
+
* @returns whether any nested block is a file.
|
|
3496
|
+
*/
|
|
3497
|
+
function contentHasFile(content) {
|
|
3498
|
+
for (const block of content) if (block.type === "file" || block.type === "tool-result" && contentHasFile(block.content)) return true;
|
|
3499
|
+
return false;
|
|
3500
|
+
}
|
|
3501
|
+
/**
|
|
3502
|
+
* Stable model-facing handle for one durable file reference: the address of
|
|
3503
|
+
* the verbatim stored copy and the instruction to read it on demand. This is
|
|
3504
|
+
* the only representation a provider ever receives for a file.
|
|
3505
|
+
* @param ref - durable verbatim file reference.
|
|
3506
|
+
* @param readonlyPath - execution-world path of the stored copy, when resolvable.
|
|
3507
|
+
* @returns deterministic handle text naming the file, its size, and its address.
|
|
3508
|
+
*/
|
|
3509
|
+
function fileHandleText(ref, readonlyPath) {
|
|
3510
|
+
const digest = String(ref.attachmentId).slice(7, 15);
|
|
3511
|
+
const identity = `File ${quoted(ref.name)} (${ref.bytes} bytes, sha256:${digest})`;
|
|
3512
|
+
if (readonlyPath === void 0) return `[${identity} was uploaded, but the current execution environment cannot access a readable path. Report that limitation if its contents are needed; do not claim to have read it.]`;
|
|
3513
|
+
return `[${identity}: verbatim read-only copy saved at ${quoted(readonlyPath)}. Read that path with your file tools when its contents are needed; copy it to a writable location before modifying it. When delegating file work, include this saved path in the delegation prompt; only subagents sharing this execution environment can read it.]`;
|
|
3514
|
+
}
|
|
3515
|
+
/** Replace every file occurrence, including nested tool results, with handle text. */
|
|
3516
|
+
function replaceFilesWithHandles(blocks, resolvePath) {
|
|
3517
|
+
let next;
|
|
3518
|
+
for (const [index, block] of blocks.entries()) {
|
|
3519
|
+
if (block.type === "file") {
|
|
3520
|
+
next ??= blocks.slice(0, index);
|
|
3521
|
+
next.push({
|
|
3522
|
+
type: "text",
|
|
3523
|
+
text: fileHandleText(block.attachment, resolvePath(block.attachment))
|
|
3524
|
+
});
|
|
3525
|
+
continue;
|
|
3526
|
+
}
|
|
3527
|
+
if (block.type === "tool-result") {
|
|
3528
|
+
const content = replaceFilesWithHandles(block.content, resolvePath);
|
|
3529
|
+
if (content !== block.content) {
|
|
3530
|
+
next ??= blocks.slice(0, index);
|
|
3531
|
+
next.push({
|
|
3532
|
+
...block,
|
|
3533
|
+
content
|
|
3534
|
+
});
|
|
3535
|
+
continue;
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
next?.push(block);
|
|
3539
|
+
}
|
|
3540
|
+
return next ?? blocks;
|
|
3541
|
+
}
|
|
3542
|
+
/**
|
|
3543
|
+
* Project durable file history into deterministic handle text for every model
|
|
3544
|
+
* route. Unlike images, no provider receives file blocks natively, so this
|
|
3545
|
+
* projection is unconditional in request assembly.
|
|
3546
|
+
* @param messages - complete request history.
|
|
3547
|
+
* @param resolvePath - resolve one reference's current execution-world read path.
|
|
3548
|
+
* @returns the original list without files, otherwise shallow message copies with handle text.
|
|
3549
|
+
*/
|
|
3550
|
+
function projectFilesToText(messages, resolvePath) {
|
|
3551
|
+
if (!messages.some((message) => contentHasFile(message.content))) return messages;
|
|
3552
|
+
return messages.map((message) => {
|
|
3553
|
+
const content = replaceFilesWithHandles(message.content, resolvePath);
|
|
3554
|
+
return content === message.content ? message : {
|
|
3555
|
+
...message,
|
|
3556
|
+
content
|
|
3557
|
+
};
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3479
3560
|
/** Replace every image occurrence, including nested tool results, for a text-only model. */
|
|
3480
3561
|
function replaceImagesForTextModel(blocks) {
|
|
3481
3562
|
let next;
|
|
@@ -3916,6 +3997,15 @@ var LlmError = class extends HarnessError {
|
|
|
3916
3997
|
imageRequestPricing(provider, model) {
|
|
3917
3998
|
return this.adapters.get(provider)?.adapter.imageRequestPricing(provider, model);
|
|
3918
3999
|
}
|
|
4000
|
+
/**
|
|
4001
|
+
* Resolve the exact text one durable file occurrence contributes to every
|
|
4002
|
+
* provider request in the current execution environment.
|
|
4003
|
+
* @param ref - durable verbatim file reference from model history.
|
|
4004
|
+
* @returns the same deterministic handle text used at adapter dispatch.
|
|
4005
|
+
*/
|
|
4006
|
+
fileRequestText(ref) {
|
|
4007
|
+
return fileHandleText(ref, this.fileReadPath(ref));
|
|
4008
|
+
}
|
|
3919
4009
|
/** Detach typed adapter-owned modality metadata. */
|
|
3920
4010
|
detachedModalities(modalities) {
|
|
3921
4011
|
return modalities === void 0 ? void 0 : [...modalities];
|
|
@@ -3965,6 +4055,8 @@ var LlmError = class extends HarnessError {
|
|
|
3965
4055
|
const context = resolved.context;
|
|
3966
4056
|
if (context !== void 0 && (!Number.isInteger(context.contextWindow) || context.contextWindow <= 0)) throw new LlmError(`adapter returned invalid context metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_CONTEXT");
|
|
3967
4057
|
const inputModalities = this.detachedModalities(resolved.inputModalities);
|
|
4058
|
+
const systemPromptUpdate = resolved.systemPromptUpdate;
|
|
4059
|
+
if (systemPromptUpdate !== void 0 && systemPromptUpdate !== "in-history") throw new LlmError(`adapter returned invalid system prompt update mode for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
|
|
3968
4060
|
const defaultMaxTokens = resolved.defaultMaxTokens;
|
|
3969
4061
|
if (defaultMaxTokens !== void 0 && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) throw new LlmError(`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, "INVALID_MODEL_MAX_TOKENS");
|
|
3970
4062
|
const info = {
|
|
@@ -3974,7 +4066,8 @@ var LlmError = class extends HarnessError {
|
|
|
3974
4066
|
...resolved.description === void 0 ? {} : { description: resolved.description },
|
|
3975
4067
|
...inputModalities === void 0 ? {} : { inputModalities },
|
|
3976
4068
|
...context === void 0 ? {} : { context: { contextWindow: context.contextWindow } },
|
|
3977
|
-
...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens }
|
|
4069
|
+
...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens },
|
|
4070
|
+
...resolved.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: resolved.systemPromptUpdate }
|
|
3978
4071
|
};
|
|
3979
4072
|
const reasoning = resolved.reasoning;
|
|
3980
4073
|
if (reasoning === void 0) return info;
|
|
@@ -4068,6 +4161,7 @@ var LlmError = class extends HarnessError {
|
|
|
4068
4161
|
adapterDefaults,
|
|
4069
4162
|
...context === void 0 ? {} : { context },
|
|
4070
4163
|
...modelInfo.inputModalities === void 0 ? {} : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
|
|
4164
|
+
...modelInfo.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: modelInfo.systemPromptUpdate },
|
|
4071
4165
|
stream: (options) => {
|
|
4072
4166
|
if (dispatched) throw new LlmError("a prepared LLM call can only be dispatched once", "INVALID_PREPARED_CALL");
|
|
4073
4167
|
if (!callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
|
|
@@ -4109,6 +4203,20 @@ var LlmError = class extends HarnessError {
|
|
|
4109
4203
|
return Object.isFrozen(options) ? deepFreeze(filtered) : filtered;
|
|
4110
4204
|
}
|
|
4111
4205
|
/**
|
|
4206
|
+
* Resolve the current execution-world read path of one durable file
|
|
4207
|
+
* reference through the mounted attachment and filesystem providers.
|
|
4208
|
+
*/
|
|
4209
|
+
fileReadPath(ref) {
|
|
4210
|
+
let hostPath;
|
|
4211
|
+
try {
|
|
4212
|
+
hostPath = this.ctx.get("attachments")?.fileHostPath(ref);
|
|
4213
|
+
} catch {
|
|
4214
|
+
return;
|
|
4215
|
+
}
|
|
4216
|
+
if (hostPath === void 0) return void 0;
|
|
4217
|
+
return this.ctx.get("fs")?.processPathFromHostPath(hostPath);
|
|
4218
|
+
}
|
|
4219
|
+
/**
|
|
4112
4220
|
* Final adapter boundary. Adapter selection, dispatch, iterator construction,
|
|
4113
4221
|
* and iteration failures become one terminal failure chunk. Middleware and
|
|
4114
4222
|
* downstream consumer failures remain thrown plugin or consumer errors.
|
|
@@ -4139,13 +4247,16 @@ var LlmError = class extends HarnessError {
|
|
|
4139
4247
|
...options,
|
|
4140
4248
|
...resolvedConfig
|
|
4141
4249
|
};
|
|
4142
|
-
|
|
4250
|
+
let projectedMessages = resolvedOptions.messages;
|
|
4251
|
+
if (projectedMessages.some((message) => contentHasFile(message.content))) projectedMessages = projectFilesToText(projectedMessages, (ref) => this.fileReadPath(ref));
|
|
4252
|
+
if (modelInfo.inputModalities !== void 0 && !modelInfo.inputModalities.includes("image") && projectedMessages.some((message) => contentHasImage(message.content))) projectedMessages = projectImagesForTextModel(projectedMessages);
|
|
4253
|
+
const projectedOptions = projectedMessages === resolvedOptions.messages ? resolvedOptions : Object.isFrozen(resolvedOptions) ? deepFreeze({
|
|
4143
4254
|
...resolvedOptions,
|
|
4144
|
-
messages:
|
|
4255
|
+
messages: projectedMessages
|
|
4145
4256
|
}) : {
|
|
4146
4257
|
...resolvedOptions,
|
|
4147
|
-
messages:
|
|
4148
|
-
}
|
|
4258
|
+
messages: projectedMessages
|
|
4259
|
+
};
|
|
4149
4260
|
iterator = dispatch(this.forAdapter(projectedOptions, adapter))[Symbol.asyncIterator]();
|
|
4150
4261
|
} catch (error) {
|
|
4151
4262
|
yield adapterFailureChunk(error, options.signal);
|
|
@@ -8966,7 +9077,7 @@ function superRefine(fn, params) {
|
|
|
8966
9077
|
return /* @__PURE__ */ _superRefine(fn, params);
|
|
8967
9078
|
}
|
|
8968
9079
|
//#endregion
|
|
8969
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-storage@0.1.
|
|
9080
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-storage@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-storage/lib/index.js
|
|
8970
9081
|
/**
|
|
8971
9082
|
* Backend-facing vocabulary of the storage hub: a backend owns one medium
|
|
8972
9083
|
* (a file-tree root, a database file) and exposes operation groups over it.
|
|
@@ -8977,7 +9088,7 @@ function superRefine(fn, params) {
|
|
|
8977
9088
|
/** Allowed format for unit and table names: safe as a file name and as a SQL identifier segment without escaping. */
|
|
8978
9089
|
const UNIT_NAME_RE = /^[a-z][a-z0-9_]*$/;
|
|
8979
9090
|
//#endregion
|
|
8980
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-storage-domain@0.1.
|
|
9091
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-storage-domain@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+ds_8f094eabec231701d08ef59df76cca5d/node_modules/@deepseek-ai/dsh-storage-domain/lib/index.js
|
|
8981
9092
|
/**
|
|
8982
9093
|
* Domain declaration vocabulary. A spec object is the single source of a
|
|
8983
9094
|
* domain's identity, layout, and record schemas: the owning package defines
|
|
@@ -9121,6 +9232,27 @@ function sessionEvents(session) {
|
|
|
9121
9232
|
function asEventList(events) {
|
|
9122
9233
|
return Array.isArray(events) ? events : [];
|
|
9123
9234
|
}
|
|
9235
|
+
/**
|
|
9236
|
+
* 读取一个持久化(离线)会话的事件日志与头部元数据。兼容两代 persistence API:
|
|
9237
|
+
* - 旧版:`sessionPersistence.inspect(id)` 直接返回 { events, meta };
|
|
9238
|
+
* - dsh 0.1.5 之后:`inspect` 已删除,改用 `open(id, 'read')` → `handle.read()`
|
|
9239
|
+
* (+ `handle.header`),读句柄不抢写所有权,读后必须 close()。
|
|
9240
|
+
* 会话不存在时两个 API 都抛错,调用方按原语义处理。
|
|
9241
|
+
*/
|
|
9242
|
+
async function inspectPersistedSession(ctx, id) {
|
|
9243
|
+
const persistence = ctx.sessionPersistence;
|
|
9244
|
+
if (typeof persistence.inspect === "function") return await persistence.inspect(id);
|
|
9245
|
+
const handle = await persistence.open(id, "read");
|
|
9246
|
+
try {
|
|
9247
|
+
const { events } = await handle.read();
|
|
9248
|
+
return {
|
|
9249
|
+
events,
|
|
9250
|
+
meta: handle.header
|
|
9251
|
+
};
|
|
9252
|
+
} finally {
|
|
9253
|
+
await handle.close();
|
|
9254
|
+
}
|
|
9255
|
+
}
|
|
9124
9256
|
/** agent.followup / steer 接受的用户消息值。 */
|
|
9125
9257
|
function userMessage(text) {
|
|
9126
9258
|
return {
|
|
@@ -9144,6 +9276,18 @@ function blockText(content) {
|
|
|
9144
9276
|
}
|
|
9145
9277
|
return parts.length === 0 ? void 0 : parts.join("\n");
|
|
9146
9278
|
}
|
|
9279
|
+
/** 提取指定类型的 content block 文本(如 reasoning),用于从消息 content 里读出思维链。 */
|
|
9280
|
+
function blockTextByType(content, wantedType) {
|
|
9281
|
+
if (!Array.isArray(content)) return void 0;
|
|
9282
|
+
const parts = [];
|
|
9283
|
+
for (const raw of content) {
|
|
9284
|
+
const block = raw;
|
|
9285
|
+
if (block !== null && typeof block === "object" && block.type === wantedType && typeof block.text === "string") {
|
|
9286
|
+
if (block.text !== "") parts.push(block.text);
|
|
9287
|
+
}
|
|
9288
|
+
}
|
|
9289
|
+
return parts.length === 0 ? void 0 : parts.join("\n");
|
|
9290
|
+
}
|
|
9147
9291
|
function countImages(content) {
|
|
9148
9292
|
if (!Array.isArray(content)) return 0;
|
|
9149
9293
|
let n = 0;
|
|
@@ -9181,7 +9325,7 @@ function foldMessages(events) {
|
|
|
9181
9325
|
} else if (event.type === "assistant/message") {
|
|
9182
9326
|
const message = event.data.message;
|
|
9183
9327
|
const text = message === null ? void 0 : blockText(message.content);
|
|
9184
|
-
const reasoning = message === null ? void 0 : blockText(message.reasoning);
|
|
9328
|
+
const reasoning = message === null ? void 0 : blockTextByType(message.content, "reasoning") ?? blockText(message.reasoning);
|
|
9185
9329
|
const images = message === null ? 0 : countImages(message.content);
|
|
9186
9330
|
const toolCalls = message === null ? [] : toolNamesOf(message.toolCalls);
|
|
9187
9331
|
if (text === void 0 && reasoning === void 0 && toolCalls.length === 0) continue;
|
|
@@ -9198,6 +9342,143 @@ function foldMessages(events) {
|
|
|
9198
9342
|
}
|
|
9199
9343
|
return rows;
|
|
9200
9344
|
}
|
|
9345
|
+
/** 按已完成输出段落折叠事件日志:每个 assistant/message 割一段,返回 sinceSeq 后的所有段落(事件序)。 */
|
|
9346
|
+
function segmentsSince(events, sinceSeq = 0) {
|
|
9347
|
+
const list = asEventList(events);
|
|
9348
|
+
const out = [];
|
|
9349
|
+
let openTurn = false;
|
|
9350
|
+
for (const event of list) {
|
|
9351
|
+
if (event.type === "turn/start") {
|
|
9352
|
+
openTurn = true;
|
|
9353
|
+
continue;
|
|
9354
|
+
}
|
|
9355
|
+
if (event.type === "turn/end") {
|
|
9356
|
+
openTurn = false;
|
|
9357
|
+
continue;
|
|
9358
|
+
}
|
|
9359
|
+
if (event.type !== "assistant/message") continue;
|
|
9360
|
+
if (event.seq <= sinceSeq) continue;
|
|
9361
|
+
const message = event.data.message;
|
|
9362
|
+
const text = message === null ? void 0 : blockText(message.content);
|
|
9363
|
+
const reasoning = message === null ? void 0 : blockTextByType(message.content, "reasoning") ?? blockText(message.reasoning);
|
|
9364
|
+
const toolCalls = message === null ? [] : toolNamesOf(message.toolCalls);
|
|
9365
|
+
const turn = event.data?.turn;
|
|
9366
|
+
const step = event.data?.step;
|
|
9367
|
+
const seg = {
|
|
9368
|
+
seq: event.seq,
|
|
9369
|
+
time: event.time,
|
|
9370
|
+
turn: typeof turn === "number" ? turn : 0,
|
|
9371
|
+
step: typeof step === "number" ? step : 0,
|
|
9372
|
+
toolCalls,
|
|
9373
|
+
openTurn
|
|
9374
|
+
};
|
|
9375
|
+
if (text !== void 0) seg.text = text;
|
|
9376
|
+
if (reasoning !== void 0) seg.reasoning = reasoning;
|
|
9377
|
+
out.push(seg);
|
|
9378
|
+
}
|
|
9379
|
+
return out;
|
|
9380
|
+
}
|
|
9381
|
+
/** 取自 sinceSeq 之后最新一个已完成输出段落(无则 null)。 */
|
|
9382
|
+
function latestSegmentSince(events, sinceSeq = 0) {
|
|
9383
|
+
const segs = segmentsSince(events, sinceSeq);
|
|
9384
|
+
return segs.length === 0 ? null : segs[segs.length - 1] ?? null;
|
|
9385
|
+
}
|
|
9386
|
+
/**
|
|
9387
|
+
* 对一个 live 会话的事件日志折叠出其"最近推理/文本增量流"。兼容两代日志形状:
|
|
9388
|
+
* - dsh 0.1.5 之后:turn 内不再发布独立 chunk 事件,流式增量内嵌在已定型
|
|
9389
|
+
* assistant/message(或未定型 assistant/attempt)事件的 `stream` 记录里
|
|
9390
|
+
* (reasoning-chunks / text-chunks 批量段 + 原始 chunk:reasoning-delta / text-delta);
|
|
9391
|
+
* - 0.1.2-rc 之前:独立的 assistant/chunk 事件(turn 中途即可见、增量,
|
|
9392
|
+
* 这正是旧版监控思维链并按规则提前终止所需的粒度)。
|
|
9393
|
+
* 两者兼读、取最新一份;无任何增量(会话空闲、或该 provider 不流式推理/无推理)
|
|
9394
|
+
* 返回 null,调用方应回落为 foldMessages 里已定型的 message.reasoning。
|
|
9395
|
+
* 纯读、无副作用;事件形状不做任何假设,缺失/异常一律安全处理。
|
|
9396
|
+
*/
|
|
9397
|
+
function liveReasoningSnapshot(events) {
|
|
9398
|
+
const list = asEventList(events);
|
|
9399
|
+
let lastFinalizedSeq = -1;
|
|
9400
|
+
let reasoning = "";
|
|
9401
|
+
let text = "";
|
|
9402
|
+
let seq = -1;
|
|
9403
|
+
let time = 0;
|
|
9404
|
+
let turn = 0;
|
|
9405
|
+
let step = 0;
|
|
9406
|
+
let foundDelta = false;
|
|
9407
|
+
for (const event of list) {
|
|
9408
|
+
const type = event.type;
|
|
9409
|
+
if (type === "assistant/message" || type === "assistant/attempt") {
|
|
9410
|
+
const data = event.data;
|
|
9411
|
+
if (data !== null && data !== void 0 && typeof data === "object" && Array.isArray(data.stream)) {
|
|
9412
|
+
let r = "";
|
|
9413
|
+
let t = "";
|
|
9414
|
+
for (const raw of data.stream) {
|
|
9415
|
+
const record = raw;
|
|
9416
|
+
if (record === null || typeof record !== "object") continue;
|
|
9417
|
+
if (record.type === "reasoning-chunks" && Array.isArray(record.texts)) {
|
|
9418
|
+
for (const s of record.texts) if (typeof s === "string") r += s;
|
|
9419
|
+
} else if (record.type === "text-chunks" && Array.isArray(record.texts)) {
|
|
9420
|
+
for (const s of record.texts) if (typeof s === "string") t += s;
|
|
9421
|
+
} else if (record.type === "chunk") {
|
|
9422
|
+
const c = record.chunk;
|
|
9423
|
+
if (c !== null && typeof c === "object" && typeof c.type === "string" && typeof c.text === "string") {
|
|
9424
|
+
if (c.type === "reasoning-delta") r += c.text;
|
|
9425
|
+
else if (c.type === "text-delta") t += c.text;
|
|
9426
|
+
}
|
|
9427
|
+
}
|
|
9428
|
+
}
|
|
9429
|
+
if (r !== "" || t !== "") {
|
|
9430
|
+
if (typeof data.turn === "number") turn = data.turn;
|
|
9431
|
+
if (typeof data.step === "number") step = data.step;
|
|
9432
|
+
reasoning = r;
|
|
9433
|
+
text = t;
|
|
9434
|
+
seq = event.seq;
|
|
9435
|
+
time = event.time;
|
|
9436
|
+
foundDelta = true;
|
|
9437
|
+
}
|
|
9438
|
+
}
|
|
9439
|
+
if (type === "assistant/message") lastFinalizedSeq = event.seq;
|
|
9440
|
+
continue;
|
|
9441
|
+
}
|
|
9442
|
+
if (type !== "assistant/chunk") continue;
|
|
9443
|
+
if (event.seq <= lastFinalizedSeq) continue;
|
|
9444
|
+
const data = event.data;
|
|
9445
|
+
if (data === null || data === void 0 || typeof data !== "object") continue;
|
|
9446
|
+
const c = data.chunk;
|
|
9447
|
+
if (c === null || typeof c !== "object" || typeof c.type !== "string") continue;
|
|
9448
|
+
if (c.type === "reasoning-delta" || c.type === "text-delta") {
|
|
9449
|
+
if (typeof c.text !== "string") continue;
|
|
9450
|
+
if (c.type === "reasoning-delta") reasoning += c.text;
|
|
9451
|
+
else text += c.text;
|
|
9452
|
+
if (typeof data.turn === "number") turn = data.turn;
|
|
9453
|
+
if (typeof data.step === "number") step = data.step;
|
|
9454
|
+
foundDelta = true;
|
|
9455
|
+
if (event.seq > seq) seq = event.seq;
|
|
9456
|
+
if (event.time > time) time = event.time;
|
|
9457
|
+
}
|
|
9458
|
+
}
|
|
9459
|
+
if (!foundDelta) return null;
|
|
9460
|
+
return {
|
|
9461
|
+
turn,
|
|
9462
|
+
step,
|
|
9463
|
+
reasoning,
|
|
9464
|
+
text,
|
|
9465
|
+
seq,
|
|
9466
|
+
time
|
|
9467
|
+
};
|
|
9468
|
+
}
|
|
9469
|
+
/** 思维链尾巴默认截断上限(字符)。 */
|
|
9470
|
+
const REASONING_TAIL_LIMIT = 4e3;
|
|
9471
|
+
/**
|
|
9472
|
+
* 组装一条受字符上限约束的"思维链尾巴"用于展示/摘要:
|
|
9473
|
+
* 优先进行中的 liveReasoning,否则最近已定型的 lastReasoning;
|
|
9474
|
+
* 超出上限时截断并附省略标记。两者皆无返回 undefined。
|
|
9475
|
+
*/
|
|
9476
|
+
function reasoningTailOf(liveReasoning, lastReasoning, limit = REASONING_TAIL_LIMIT) {
|
|
9477
|
+
const source = liveReasoning ?? lastReasoning;
|
|
9478
|
+
if (source === void 0 || source === "") return void 0;
|
|
9479
|
+
if (source.length <= limit) return source;
|
|
9480
|
+
return source.slice(0, limit) + "…";
|
|
9481
|
+
}
|
|
9201
9482
|
/**
|
|
9202
9483
|
* 会话标题:优先 session/title 事件(last-wins,与 UI 一致);
|
|
9203
9484
|
* 无标题事件时回落为第一条用户消息(截断 80 字符)。
|
|
@@ -9249,8 +9530,10 @@ async function waitForReply(opts) {
|
|
|
9249
9530
|
const started = Date.now();
|
|
9250
9531
|
const deadline = started + opts.timeoutMs;
|
|
9251
9532
|
const requireTurnEnd = opts.requireTurnEnd ?? false;
|
|
9533
|
+
const waitForSegment = opts.waitForSegment ?? false;
|
|
9252
9534
|
let latest = null;
|
|
9253
9535
|
let textReply = null;
|
|
9536
|
+
let segment = null;
|
|
9254
9537
|
let turnEnded = false;
|
|
9255
9538
|
for (;;) {
|
|
9256
9539
|
if (opts.signal !== void 0 && opts.signal.aborted) break;
|
|
@@ -9260,22 +9543,38 @@ async function waitForReply(opts) {
|
|
|
9260
9543
|
if (latest === null || row.seq > latest.seq) latest = row;
|
|
9261
9544
|
if (row.text !== void 0 && (textReply === null || row.seq > textReply.seq)) textReply = row;
|
|
9262
9545
|
}
|
|
9546
|
+
if (waitForSegment) {
|
|
9547
|
+
const seg = latestSegmentSince(events, opts.baselineSeq);
|
|
9548
|
+
if (seg !== null && (segment === null || seg.seq > segment.seq)) segment = seg;
|
|
9549
|
+
}
|
|
9263
9550
|
if (requireTurnEnd && latest !== null) {
|
|
9264
9551
|
for (const event of events) if (event.seq > latest.seq && event.type === "turn/end") {
|
|
9265
9552
|
turnEnded = true;
|
|
9266
9553
|
break;
|
|
9267
9554
|
}
|
|
9268
9555
|
}
|
|
9269
|
-
|
|
9556
|
+
let done;
|
|
9557
|
+
if (requireTurnEnd) done = latest !== null && turnEnded;
|
|
9558
|
+
else if (waitForSegment) done = segment !== null;
|
|
9559
|
+
else done = textReply !== null;
|
|
9560
|
+
if (done) break;
|
|
9270
9561
|
if (Date.now() >= deadline) break;
|
|
9271
9562
|
await sleep(100);
|
|
9272
9563
|
}
|
|
9273
|
-
|
|
9564
|
+
let message = waitForSegment && segment !== null ? {
|
|
9565
|
+
seq: segment.seq,
|
|
9566
|
+
time: segment.time,
|
|
9567
|
+
role: "assistant",
|
|
9568
|
+
images: 0,
|
|
9569
|
+
...segment.text !== void 0 ? { text: segment.text } : {},
|
|
9570
|
+
...segment.reasoning !== void 0 ? { reasoning: segment.reasoning } : {},
|
|
9571
|
+
...segment.toolCalls.length > 0 ? { toolCalls: segment.toolCalls } : {}
|
|
9572
|
+
} : textReply ?? latest;
|
|
9274
9573
|
return {
|
|
9275
9574
|
message,
|
|
9276
9575
|
seq: message === null ? opts.baselineSeq : message.seq,
|
|
9277
9576
|
turnEnded,
|
|
9278
|
-
timedOut: requireTurnEnd ? latest !== null && !turnEnded : textReply === null,
|
|
9577
|
+
timedOut: requireTurnEnd ? latest !== null && !turnEnded : waitForSegment ? segment === null : textReply === null,
|
|
9279
9578
|
aborted: opts.signal !== void 0 && opts.signal.aborted,
|
|
9280
9579
|
waitedMs: Date.now() - started
|
|
9281
9580
|
};
|
|
@@ -9351,13 +9650,18 @@ function statusSnapshot(ctx, agent) {
|
|
|
9351
9650
|
}
|
|
9352
9651
|
const recent = rows.slice(-8);
|
|
9353
9652
|
let lastAssistantText;
|
|
9653
|
+
let lastReasoning;
|
|
9354
9654
|
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
9355
9655
|
const row = rows[i];
|
|
9356
|
-
if (row !== void 0 && row.role === "assistant"
|
|
9357
|
-
lastAssistantText = row.text;
|
|
9358
|
-
|
|
9656
|
+
if (row !== void 0 && row.role === "assistant") {
|
|
9657
|
+
if (lastAssistantText === void 0 && row.text !== void 0) lastAssistantText = row.text;
|
|
9658
|
+
if (lastReasoning === void 0 && row.reasoning !== void 0) lastReasoning = row.reasoning;
|
|
9659
|
+
if (lastAssistantText !== void 0 && lastReasoning !== void 0) break;
|
|
9359
9660
|
}
|
|
9360
9661
|
}
|
|
9662
|
+
const live = liveReasoningSnapshot(events);
|
|
9663
|
+
const liveReasoning = live !== null && live.reasoning !== "" ? live.reasoning : void 0;
|
|
9664
|
+
const reasoningTail = reasoningTailOf(liveReasoning, lastReasoning);
|
|
9361
9665
|
const inbox = agent.inbox;
|
|
9362
9666
|
const cwd = agent.session.header.cwd;
|
|
9363
9667
|
return {
|
|
@@ -9371,10 +9675,13 @@ function statusSnapshot(ctx, agent) {
|
|
|
9371
9675
|
lastTurn,
|
|
9372
9676
|
lastActivityAt,
|
|
9373
9677
|
stalledMs: lastActivityAt === null ? null : Date.now() - lastActivityAt,
|
|
9374
|
-
pendingWork: inbox?.hasPending ??
|
|
9678
|
+
pendingWork: inbox?.hasPending === true || (inbox?.nextTurn?.length ?? 0) + (inbox?.nextStep?.length ?? 0) > 0,
|
|
9375
9679
|
nextTurnCount: inbox?.nextTurn?.length ?? 0,
|
|
9376
9680
|
nextStepCount: inbox?.nextStep?.length ?? 0,
|
|
9377
9681
|
...lastAssistantText === void 0 ? {} : { lastAssistantText },
|
|
9682
|
+
...lastReasoning === void 0 ? {} : { lastReasoning },
|
|
9683
|
+
...liveReasoning === void 0 ? {} : { liveReasoning },
|
|
9684
|
+
...reasoningTail === void 0 ? {} : { reasoningTail },
|
|
9378
9685
|
messageCount: rows.length,
|
|
9379
9686
|
recent
|
|
9380
9687
|
};
|
|
@@ -9429,6 +9736,16 @@ var SessionMonitor = class {
|
|
|
9429
9736
|
start(config) {
|
|
9430
9737
|
const existing = this.entries.get(config.sessionId);
|
|
9431
9738
|
const now = Date.now();
|
|
9739
|
+
const cotInit = { rules: (config.coTRules ?? []).reduce((acc, rule, index) => {
|
|
9740
|
+
const key = String(index) + ":" + rule.match + ":" + rule.field + ":" + rule.value;
|
|
9741
|
+
acc[key] = {
|
|
9742
|
+
hits: 0,
|
|
9743
|
+
lastFiredAt: null,
|
|
9744
|
+
lastMatch: false,
|
|
9745
|
+
lastSubject: ""
|
|
9746
|
+
};
|
|
9747
|
+
return acc;
|
|
9748
|
+
}, {}) };
|
|
9432
9749
|
const entry = existing !== void 0 ? {
|
|
9433
9750
|
...existing,
|
|
9434
9751
|
config,
|
|
@@ -9442,8 +9759,10 @@ var SessionMonitor = class {
|
|
|
9442
9759
|
lastActionAt: null,
|
|
9443
9760
|
lastNote: "监控已启动",
|
|
9444
9761
|
done: false,
|
|
9445
|
-
cycles: 0
|
|
9762
|
+
cycles: 0,
|
|
9763
|
+
cot: cotInit
|
|
9446
9764
|
};
|
|
9765
|
+
if (entry.cot === void 0) entry.cot = cotInit;
|
|
9447
9766
|
this.entries.set(config.sessionId, entry);
|
|
9448
9767
|
this.ensureTimer();
|
|
9449
9768
|
this.log(entry, "monitor start session=" + config.sessionId + " interval=" + config.intervalMs + " stalled=" + String(config.stalledMs ?? 6e4) + " maxStuck=" + String(config.maxStuckCycles ?? 3));
|
|
@@ -9569,6 +9888,7 @@ var SessionMonitor = class {
|
|
|
9569
9888
|
this.log(entry, "STALL(#" + entry.stuckCount + ") → steer: " + steer);
|
|
9570
9889
|
return;
|
|
9571
9890
|
}
|
|
9891
|
+
if (this.evaluateCoTRules(entry, snapshot, sessionId)) return;
|
|
9572
9892
|
if (entry.stuckCount !== 0) entry.stuckCount = 0;
|
|
9573
9893
|
entry.lastAction = "steady";
|
|
9574
9894
|
entry.lastActionAt = Date.now();
|
|
@@ -9593,6 +9913,76 @@ var SessionMonitor = class {
|
|
|
9593
9913
|
if (!text) return false;
|
|
9594
9914
|
return keywords.some((kw) => kw !== "" && text.includes(kw));
|
|
9595
9915
|
}
|
|
9916
|
+
/**
|
|
9917
|
+
* 评估思维链(CoT)规则。规则只对 live 会话有意义:running 时用进行中的
|
|
9918
|
+
* reasoning-delta 流,空闲时回落到已定型 lastReasoning。not-contains 规则作用在
|
|
9919
|
+
* reasoning 字段时,若该会话本就没有推理(非推理模型/尚未产出),不判定为命中,
|
|
9920
|
+
* 避免误 cancel 一个根本不产生思维链的会话。
|
|
9921
|
+
* @returns true 表示本 tick 已被某条规则触发并处理(steer/cancel),调用方应 return。
|
|
9922
|
+
*/
|
|
9923
|
+
evaluateCoTRules(entry, snapshot, sessionId) {
|
|
9924
|
+
const rules = entry.config.coTRules ?? [];
|
|
9925
|
+
if (rules.length === 0) return false;
|
|
9926
|
+
const minHits = entry.config.cotMinHits ?? 1;
|
|
9927
|
+
const hasLive = (snapshot.liveReasoning ?? "") !== "";
|
|
9928
|
+
const subject = {
|
|
9929
|
+
reasoning: snapshot.liveReasoning ?? snapshot.lastReasoning ?? "",
|
|
9930
|
+
text: snapshot.lastAssistantText ?? "",
|
|
9931
|
+
hasReasoning: hasLive || (snapshot.lastReasoning ?? "") !== ""
|
|
9932
|
+
};
|
|
9933
|
+
const now = Date.now();
|
|
9934
|
+
const rulesState = entry.cot;
|
|
9935
|
+
let engaged = false;
|
|
9936
|
+
rules.forEach((rule, index) => {
|
|
9937
|
+
if (engaged) return;
|
|
9938
|
+
const key = String(index) + ":" + rule.match + ":" + rule.field + ":" + rule.value;
|
|
9939
|
+
const st = rulesState.rules[key] ?? {
|
|
9940
|
+
hits: 0,
|
|
9941
|
+
lastFiredAt: null,
|
|
9942
|
+
lastMatch: false
|
|
9943
|
+
};
|
|
9944
|
+
if (rule.field === "reasoning" && rule.match === "not-contains" && !subject.hasReasoning) {
|
|
9945
|
+
st.lastMatch = false;
|
|
9946
|
+
st.hits = 0;
|
|
9947
|
+
return;
|
|
9948
|
+
}
|
|
9949
|
+
let hit;
|
|
9950
|
+
if (rule.field === "reasoning") hit = rule.match === "contains" ? subject.reasoning.includes(rule.value) : !subject.reasoning.includes(rule.value);
|
|
9951
|
+
else if (rule.field === "text") hit = rule.match === "contains" ? subject.text.includes(rule.value) : !subject.text.includes(rule.value);
|
|
9952
|
+
else {
|
|
9953
|
+
const reasonHas = subject.reasoning.includes(rule.value);
|
|
9954
|
+
const textHas = subject.text.includes(rule.value);
|
|
9955
|
+
hit = rule.match === "contains" ? reasonHas || textHas : !reasonHas && !textHas;
|
|
9956
|
+
}
|
|
9957
|
+
st.lastMatch = hit;
|
|
9958
|
+
st.hits = hit ? st.hits + 1 : 0;
|
|
9959
|
+
this.log(entry, "CoT rule " + key + " hit=" + String(hit) + " hits=" + String(st.hits) + "/" + String(minHits));
|
|
9960
|
+
if (!(hit && st.hits >= minHits)) return;
|
|
9961
|
+
const coolMs = Math.max(entry.config.intervalMs, 5e3) * 2;
|
|
9962
|
+
if (st.lastFiredAt !== null && now - st.lastFiredAt < coolMs) return;
|
|
9963
|
+
st.lastFiredAt = now;
|
|
9964
|
+
if (rule.action === "cancel") {
|
|
9965
|
+
const reason = "reasoning-rule:" + rule.value;
|
|
9966
|
+
cancelLiveSession(this.ctx, sessionId, false, reason);
|
|
9967
|
+
entry.lastAction = "cot-cancel";
|
|
9968
|
+
entry.lastActionAt = Date.now();
|
|
9969
|
+
entry.lastNote = "思维链规则触发 cancel: " + rule.value;
|
|
9970
|
+
entry.stuckCount = 0;
|
|
9971
|
+
this.log(entry, "COT-CANCEL session=" + sessionId + " rule=" + key + " value=" + rule.value);
|
|
9972
|
+
} else {
|
|
9973
|
+
const steer = rule.message !== void 0 && rule.message.trim() !== "" ? rule.message.trim() : "监控判定:思维链/回复匹配规则「" + rule.value + "」,请据此调整方向并继续推进任务。";
|
|
9974
|
+
sendLiveMessage(this.ctx, sessionId, steer, "steer");
|
|
9975
|
+
entry.lastAction = "cot-steer";
|
|
9976
|
+
entry.lastActionAt = Date.now();
|
|
9977
|
+
entry.lastNote = "思维链规则触发 steer: " + rule.value;
|
|
9978
|
+
entry.stuckCount = 0;
|
|
9979
|
+
this.log(entry, "COT-STEER session=" + sessionId + " rule=" + key + " steer=" + steer);
|
|
9980
|
+
}
|
|
9981
|
+
engaged = true;
|
|
9982
|
+
st.hits = 0;
|
|
9983
|
+
});
|
|
9984
|
+
return engaged;
|
|
9985
|
+
}
|
|
9596
9986
|
hasLlm() {
|
|
9597
9987
|
return this.ctx.llm !== void 0;
|
|
9598
9988
|
}
|
|
@@ -9648,7 +10038,7 @@ var SessionMonitor = class {
|
|
|
9648
10038
|
}
|
|
9649
10039
|
};
|
|
9650
10040
|
//#endregion
|
|
9651
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.1.
|
|
10041
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invaria_a35c65afc1061b4908fad5a531841731/node_modules/@deepseek-ai/dsh-scope/lib/index.js
|
|
9652
10042
|
/**
|
|
9653
10043
|
* Shared insertion-ordered storage and effect ownership for scope-aware registries.
|
|
9654
10044
|
*
|
|
@@ -9927,7 +10317,7 @@ function scopeTarget(base, key) {
|
|
|
9927
10317
|
return carrier;
|
|
9928
10318
|
}
|
|
9929
10319
|
//#endregion
|
|
9930
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1.
|
|
10320
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1.5-alpha.2_2b754afda902b21956a20e8a701ed6ef/node_modules/@deepseek-ai/dsh-tools/lib/index.js
|
|
9931
10321
|
/**
|
|
9932
10322
|
* Enforced JSON Schema subset shared by tool outputs, generated PTC mode
|
|
9933
10323
|
* types, subagents, and workflows. The subset accepts any JSON root, an
|
|
@@ -11104,7 +11494,7 @@ function createRunCodeTool(registry, options) {
|
|
|
11104
11494
|
if (runOver()) throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} not dispatched`);
|
|
11105
11495
|
const normalized = jsonNormalizeArgs(rawArgs);
|
|
11106
11496
|
const n = ++dispatches;
|
|
11107
|
-
const subCallId = brandString(`${String(exec.callId)}:
|
|
11497
|
+
const subCallId = brandString(`${String(exec.callId)}:ptc:${n}`);
|
|
11108
11498
|
const input = {
|
|
11109
11499
|
callId: subCallId,
|
|
11110
11500
|
rootCallId: exec.rootCallId,
|
|
@@ -11136,7 +11526,7 @@ function createRunCodeTool(registry, options) {
|
|
|
11136
11526
|
isError: result.isError,
|
|
11137
11527
|
content: result.content
|
|
11138
11528
|
});
|
|
11139
|
-
agent.session.append("tool/
|
|
11529
|
+
agent.session.append("tool/ptc-dispatch", {
|
|
11140
11530
|
rootCallId: exec.rootCallId,
|
|
11141
11531
|
parentCallId: exec.callId,
|
|
11142
11532
|
subCallId,
|
|
@@ -11158,7 +11548,7 @@ function createRunCodeTool(registry, options) {
|
|
|
11158
11548
|
reject(/* @__PURE__ */ new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} tool call abandoned`));
|
|
11159
11549
|
},
|
|
11160
11550
|
async start() {
|
|
11161
|
-
exec.agent?.session.append("tool/
|
|
11551
|
+
exec.agent?.session.append("tool/ptc-dispatch-start", {
|
|
11162
11552
|
rootCallId: exec.rootCallId,
|
|
11163
11553
|
parentCallId: exec.callId,
|
|
11164
11554
|
subCallId,
|
|
@@ -11192,7 +11582,7 @@ function createRunCodeTool(registry, options) {
|
|
|
11192
11582
|
content: result.content,
|
|
11193
11583
|
source: {
|
|
11194
11584
|
kind: "plugin",
|
|
11195
|
-
plugin: "tools-
|
|
11585
|
+
plugin: "tools-ptc"
|
|
11196
11586
|
}
|
|
11197
11587
|
}));
|
|
11198
11588
|
for (const context of result.additionalContexts ?? []) exec.deferContext(context);
|
|
@@ -12596,8 +12986,7 @@ var ToolRuntime = class extends Service {
|
|
|
12596
12986
|
* reads return the same flavor — but a reload that swapped in a second
|
|
12597
12987
|
* language between them would hand a program written against one SDK to the
|
|
12598
12988
|
* other. Binding it is deferred until a second backend ships (the first
|
|
12599
|
-
* point it is testable)
|
|
12600
|
-
* [language-dispatch note](../../../../.agents/notes/implemented/feature/2026-07-31-ptc-language-dispatch.md).
|
|
12989
|
+
* point it is testable).
|
|
12601
12990
|
*/
|
|
12602
12991
|
requireCodeRuntime(mode) {
|
|
12603
12992
|
const runtime = this.ctx.get("codeRuntime");
|
|
@@ -12683,9 +13072,9 @@ var ToolRuntime = class extends Service {
|
|
|
12683
13072
|
* A restriction filters what a scope inherits — the global layer and every
|
|
12684
13073
|
* ancestor layer on its chain — and never what its OWN layer registers.
|
|
12685
13074
|
* That exemption is what a per-child capability filter has to keep intact:
|
|
12686
|
-
* the delegation runtime registers a child's
|
|
12687
|
-
*
|
|
12688
|
-
*
|
|
13075
|
+
* the delegation runtime registers a child's structured-output tool into the
|
|
13076
|
+
* child's own layer, and a filter naming the capabilities the child may use
|
|
13077
|
+
* must not strip the machinery it answers through.
|
|
12689
13078
|
*
|
|
12690
13079
|
* Reading the exempt set as "the global layer" instead of "not mine" held
|
|
12691
13080
|
* only while every model-facing tool sat in the host composition. Once
|
|
@@ -12803,7 +13192,7 @@ var ToolRuntime = class extends Service {
|
|
|
12803
13192
|
}
|
|
12804
13193
|
/**
|
|
12805
13194
|
* Run the `tools/ptc-dispatch-log` waterfall over one settled sub-dispatch
|
|
12806
|
-
* and return the content the bridge should log on `tool/
|
|
13195
|
+
* and return the content the bridge should log on `tool/ptc-dispatch`.
|
|
12807
13196
|
* Contained: when a listener throws, the method logs the original settled
|
|
12808
13197
|
* content; that failure must not fail the dispatch or omit the settle event. Private:
|
|
12809
13198
|
* the ONE consumer is the `run_code` bridge this registry constructs, which
|
|
@@ -13429,11 +13818,33 @@ function toolAbortedBeforeDispatchResult(prior) {
|
|
|
13429
13818
|
};
|
|
13430
13819
|
}
|
|
13431
13820
|
//#endregion
|
|
13432
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-agent@0.1.
|
|
13821
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-agent@0.1.5-alpha.2_cfc1690176e5c008b05d52ad11161b75/node_modules/@deepseek-ai/dsh-agent/lib/index.js
|
|
13433
13822
|
/**
|
|
13434
13823
|
* Agent-scoped model selection shared by runtime entry points.
|
|
13435
13824
|
* @module @deepseek-ai/dsh-agent/model-selection
|
|
13436
13825
|
*/
|
|
13826
|
+
function sameRoute(left, right) {
|
|
13827
|
+
return left.provider === right.provider && left.model === right.model;
|
|
13828
|
+
}
|
|
13829
|
+
function routeLabel(route, other) {
|
|
13830
|
+
return route.provider === other.provider ? route.model : `${route.provider}/${route.model}`;
|
|
13831
|
+
}
|
|
13832
|
+
function modelSwitchNotice(previous, selected) {
|
|
13833
|
+
const from = routeLabel(previous, selected);
|
|
13834
|
+
const to = routeLabel(selected, previous);
|
|
13835
|
+
return createUserMessage({
|
|
13836
|
+
content: [{
|
|
13837
|
+
type: "text",
|
|
13838
|
+
text: `[model changed: assistant turns above this point were generated by ${from}; the session continues with ${to}]`
|
|
13839
|
+
}],
|
|
13840
|
+
source: {
|
|
13841
|
+
kind: "plugin",
|
|
13842
|
+
plugin: "model-selection",
|
|
13843
|
+
form: "notice",
|
|
13844
|
+
summary: boundContextSummary(`${from} → ${to}`)
|
|
13845
|
+
}
|
|
13846
|
+
});
|
|
13847
|
+
}
|
|
13437
13848
|
/**
|
|
13438
13849
|
* Couple one mutable selection to Agent-scoped prompt assembly and request routing.
|
|
13439
13850
|
* Prompt assembly snapshots the selected model before delegating, then applies
|
|
@@ -13442,9 +13853,15 @@ function toolAbortedBeforeDispatchResult(prior) {
|
|
|
13442
13853
|
* surfaces. An absent selected effort clears any inherited effort, restoring
|
|
13443
13854
|
* the selected model's provider/default behavior.
|
|
13444
13855
|
*
|
|
13856
|
+
* A provider/model change appends a durable user-role notice to the next
|
|
13857
|
+
* admitted request. It compares the assembled selection with the latest
|
|
13858
|
+
* request header; effort-only changes and empty no-request decisions add no
|
|
13859
|
+
* notice. Failure before header persistence repeats the notice on the next
|
|
13860
|
+
* request.
|
|
13861
|
+
*
|
|
13445
13862
|
* @param agentCtx - The selected Agent's scoped context.
|
|
13446
13863
|
* @param selection - Mutable selection owned by the calling entry point.
|
|
13447
|
-
* @returns Disposer for
|
|
13864
|
+
* @returns Disposer for all scoped waterfall listeners.
|
|
13448
13865
|
*/
|
|
13449
13866
|
function installModelSelection(agentCtx, selection) {
|
|
13450
13867
|
const disposeAssembly = agentCtx.on("system-prompt/assemble", async (_assembly, _context, next) => {
|
|
@@ -13473,9 +13890,22 @@ function installModelSelection(agentCtx, selection) {
|
|
|
13473
13890
|
...selected.reasoningEffort === void 0 ? {} : { reasoningEffort: selected.reasoningEffort }
|
|
13474
13891
|
};
|
|
13475
13892
|
});
|
|
13893
|
+
const disposeNotice = agentCtx.on("agent/pre-step", async ({ agent, messages, signal, step }, next) => {
|
|
13894
|
+
const decision = await next();
|
|
13895
|
+
if (decision.kind === "reject" || signal.aborted) return decision;
|
|
13896
|
+
if (decision.messages.length === 0 && (step === 1 || messages.length > 0)) return decision;
|
|
13897
|
+
const selected = selection.assembled;
|
|
13898
|
+
const previous = agent.session.requestHeader()?.config;
|
|
13899
|
+
if (selected === void 0 || previous === void 0 || sameRoute(selected, previous)) return decision;
|
|
13900
|
+
return {
|
|
13901
|
+
...decision,
|
|
13902
|
+
messages: [...decision.messages, modelSwitchNotice(previous, selected)]
|
|
13903
|
+
};
|
|
13904
|
+
}, { prepend: true });
|
|
13476
13905
|
return () => {
|
|
13477
13906
|
disposeAssembly();
|
|
13478
13907
|
disposeRequest();
|
|
13908
|
+
disposeNotice();
|
|
13479
13909
|
};
|
|
13480
13910
|
}
|
|
13481
13911
|
//#endregion
|
|
@@ -13796,7 +14226,11 @@ function registerResume(env) {
|
|
|
13796
14226
|
}
|
|
13797
14227
|
let headers = [];
|
|
13798
14228
|
try {
|
|
13799
|
-
headers = await env.ctx.sessionPersistence.list()
|
|
14229
|
+
headers = (await env.ctx.sessionPersistence.list()).map((rec) => ({
|
|
14230
|
+
id: rec.header.id,
|
|
14231
|
+
...rec.header.cwd === void 0 ? {} : { cwd: rec.header.cwd },
|
|
14232
|
+
createdAt: rec.header.createdAt
|
|
14233
|
+
}));
|
|
13800
14234
|
} catch (error) {
|
|
13801
14235
|
throw new Error("session persistence unavailable: " + (error instanceof Error ? error.message : String(error)));
|
|
13802
14236
|
}
|
|
@@ -13831,7 +14265,7 @@ function registerResume(env) {
|
|
|
13831
14265
|
function registerWait(env) {
|
|
13832
14266
|
env.ctx.tools.register(defineTool({
|
|
13833
14267
|
name: "session_bridge_wait",
|
|
13834
|
-
description: "Wait for a session next assistant
|
|
14268
|
+
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).",
|
|
13835
14269
|
parameters: {
|
|
13836
14270
|
sessionId: {
|
|
13837
14271
|
type: "string",
|
|
@@ -13849,6 +14283,11 @@ function registerWait(env) {
|
|
|
13849
14283
|
requireTurnEnd: {
|
|
13850
14284
|
type: "boolean",
|
|
13851
14285
|
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)."
|
|
14286
|
+
},
|
|
14287
|
+
waitFor: {
|
|
14288
|
+
type: "string",
|
|
14289
|
+
enum: ["reply", "segment"],
|
|
14290
|
+
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."
|
|
13852
14291
|
}
|
|
13853
14292
|
},
|
|
13854
14293
|
output: {
|
|
@@ -13877,9 +14316,13 @@ function registerWait(env) {
|
|
|
13877
14316
|
if (typeof args.sessionId !== "string" || args.sessionId.trim() === "") throw new Error("invalid sessionId: expected a non-empty string");
|
|
13878
14317
|
const agent = liveAgent(env, args.sessionId);
|
|
13879
14318
|
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)");
|
|
14319
|
+
const waitSegment = args.waitFor === "segment";
|
|
13880
14320
|
let baseline;
|
|
13881
14321
|
if (typeof args.sinceSeq === "number" && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0) baseline = args.sinceSeq;
|
|
13882
|
-
else {
|
|
14322
|
+
else if (waitSegment) {
|
|
14323
|
+
const segs = segmentsSince(sessionEvents(agent.session));
|
|
14324
|
+
baseline = segs.length === 0 ? -1 : segs[segs.length - 1]?.seq ?? -1;
|
|
14325
|
+
} else {
|
|
13883
14326
|
let lastText = -1;
|
|
13884
14327
|
for (const row of foldMessages(sessionEvents(agent.session))) if (row.text !== void 0) lastText = row.seq;
|
|
13885
14328
|
baseline = lastText;
|
|
@@ -13889,7 +14332,8 @@ function registerWait(env) {
|
|
|
13889
14332
|
baselineSeq: baseline,
|
|
13890
14333
|
timeoutMs: clampTimeout(args.timeoutMs),
|
|
13891
14334
|
signal: exec.signal,
|
|
13892
|
-
requireTurnEnd: args.requireTurnEnd === true
|
|
14335
|
+
requireTurnEnd: args.requireTurnEnd === true,
|
|
14336
|
+
...waitSegment ? { waitForSegment: true } : {}
|
|
13893
14337
|
});
|
|
13894
14338
|
env.registry.touch(args.sessionId);
|
|
13895
14339
|
return asJson({
|
|
@@ -13900,6 +14344,83 @@ function registerWait(env) {
|
|
|
13900
14344
|
}
|
|
13901
14345
|
}));
|
|
13902
14346
|
}
|
|
14347
|
+
function registerSegments(env) {
|
|
14348
|
+
env.ctx.tools.register(defineTool({
|
|
14349
|
+
name: "session_bridge_segments",
|
|
14350
|
+
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.",
|
|
14351
|
+
parameters: {
|
|
14352
|
+
sessionId: {
|
|
14353
|
+
type: "string",
|
|
14354
|
+
required: true,
|
|
14355
|
+
description: "Session id to read segments from (live or offline)."
|
|
14356
|
+
},
|
|
14357
|
+
sinceSeq: {
|
|
14358
|
+
type: "number",
|
|
14359
|
+
description: "Only return completed segments after this event seq (paging cursor)."
|
|
14360
|
+
},
|
|
14361
|
+
limit: {
|
|
14362
|
+
type: "number",
|
|
14363
|
+
description: "Maximum number of segments to return (default 10, max 50)."
|
|
14364
|
+
}
|
|
14365
|
+
},
|
|
14366
|
+
output: {
|
|
14367
|
+
schema: {
|
|
14368
|
+
type: "object",
|
|
14369
|
+
additionalProperties: true
|
|
14370
|
+
},
|
|
14371
|
+
render: (_args, value) => {
|
|
14372
|
+
const segs = value.segments ?? [];
|
|
14373
|
+
if (segs.length === 0) return [{
|
|
14374
|
+
type: "text",
|
|
14375
|
+
text: "(no completed segments)"
|
|
14376
|
+
}];
|
|
14377
|
+
return [{
|
|
14378
|
+
type: "text",
|
|
14379
|
+
text: segs.map((sg) => {
|
|
14380
|
+
const head = "seg #" + String(sg.seq) + " (turn " + String(sg.turn) + " step " + String(sg.step) + ")";
|
|
14381
|
+
const text = typeof sg.text === "string" ? sg.text : "";
|
|
14382
|
+
const reason = typeof sg.reasoning === "string" && sg.reasoning !== "" ? " [reasoning " + sg.reasoning.length + " chars]" : "";
|
|
14383
|
+
return head + (Array.isArray(sg.toolCalls) && sg.toolCalls.length > 0 ? " [tools: " + sg.toolCalls.join(",") + "]" : "") + reason + ": " + text.slice(0, 160);
|
|
14384
|
+
}).join("\n")
|
|
14385
|
+
}];
|
|
14386
|
+
}
|
|
14387
|
+
},
|
|
14388
|
+
async execute(args) {
|
|
14389
|
+
const agent = liveAgent(env, args.sessionId);
|
|
14390
|
+
let events;
|
|
14391
|
+
let live;
|
|
14392
|
+
if (agent !== void 0) {
|
|
14393
|
+
events = sessionEvents(agent.session);
|
|
14394
|
+
live = true;
|
|
14395
|
+
} else try {
|
|
14396
|
+
events = (await inspectPersistedSession(env.ctx, args.sessionId)).events;
|
|
14397
|
+
live = false;
|
|
14398
|
+
} catch (error) {
|
|
14399
|
+
throw new Error(String(error));
|
|
14400
|
+
}
|
|
14401
|
+
const sinceSeq = typeof args.sinceSeq === "number" && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0 ? args.sinceSeq : 0;
|
|
14402
|
+
clampLimit(args.limit, 10, 50);
|
|
14403
|
+
const segs = segmentsSince(events, sinceSeq);
|
|
14404
|
+
env.registry.touch(args.sessionId);
|
|
14405
|
+
const nextCursorSeq = segs.length === 0 ? sinceSeq : segs[segs.length - 1]?.seq ?? sinceSeq;
|
|
14406
|
+
return asJson({
|
|
14407
|
+
sessionId: args.sessionId,
|
|
14408
|
+
live,
|
|
14409
|
+
nextCursorSeq,
|
|
14410
|
+
segments: segs.map((sg) => ({
|
|
14411
|
+
seq: sg.seq,
|
|
14412
|
+
time: sg.time,
|
|
14413
|
+
turn: sg.turn,
|
|
14414
|
+
step: sg.step,
|
|
14415
|
+
openTurn: sg.openTurn,
|
|
14416
|
+
...sg.text === void 0 ? {} : { text: sg.text },
|
|
14417
|
+
...sg.reasoning === void 0 ? {} : { reasoning: sg.reasoning },
|
|
14418
|
+
...sg.toolCalls.length > 0 ? { toolCalls: sg.toolCalls } : {}
|
|
14419
|
+
}))
|
|
14420
|
+
});
|
|
14421
|
+
}
|
|
14422
|
+
}));
|
|
14423
|
+
}
|
|
13903
14424
|
function registerRead(env) {
|
|
13904
14425
|
env.ctx.tools.register(defineTool({
|
|
13905
14426
|
name: "session_bridge_read",
|
|
@@ -13966,7 +14487,7 @@ function registerRead(env) {
|
|
|
13966
14487
|
} else {
|
|
13967
14488
|
let inspection;
|
|
13968
14489
|
try {
|
|
13969
|
-
inspection = await env.ctx
|
|
14490
|
+
inspection = await inspectPersistedSession(env.ctx, args.sessionId);
|
|
13970
14491
|
} catch (error) {
|
|
13971
14492
|
throw new Error("cannot read session " + JSON.stringify(args.sessionId) + ": " + (error instanceof Error ? error.message : String(error)));
|
|
13972
14493
|
}
|
|
@@ -14090,7 +14611,8 @@ function registerFind(env) {
|
|
|
14090
14611
|
}
|
|
14091
14612
|
if (args.liveOnly !== true) try {
|
|
14092
14613
|
const persistence = env.ctx.sessionPersistence;
|
|
14093
|
-
for (const
|
|
14614
|
+
for (const rec of await persistence.list()) {
|
|
14615
|
+
const header = rec.header;
|
|
14094
14616
|
if (items.some((item) => item.sessionId === header.id)) continue;
|
|
14095
14617
|
items.push({
|
|
14096
14618
|
sessionId: header.id,
|
|
@@ -14126,13 +14648,12 @@ function registerFind(env) {
|
|
|
14126
14648
|
const wsFilter = typeof args.workspaceId === "string" ? args.workspaceId.trim() : "";
|
|
14127
14649
|
const cwdFilter = typeof args.cwd === "string" ? args.cwd.trim().toLowerCase() : "";
|
|
14128
14650
|
if (titleFilter !== "" || query !== "" && items.some((item) => !item.live)) {
|
|
14129
|
-
const persistence = env.ctx.sessionPersistence;
|
|
14130
14651
|
let inspected = 0;
|
|
14131
14652
|
for (const item of items) {
|
|
14132
14653
|
if (inspected >= 30) break;
|
|
14133
14654
|
if (item.live || item.title !== void 0) continue;
|
|
14134
14655
|
try {
|
|
14135
|
-
item.title = titleOf((await
|
|
14656
|
+
item.title = titleOf((await inspectPersistedSession(env.ctx, item.sessionId)).events);
|
|
14136
14657
|
inspected += 1;
|
|
14137
14658
|
} catch {
|
|
14138
14659
|
inspected += 1;
|
|
@@ -14180,6 +14701,7 @@ function registerBridgeTools(env) {
|
|
|
14180
14701
|
registerSend(env);
|
|
14181
14702
|
registerResume(env);
|
|
14182
14703
|
registerWait(env);
|
|
14704
|
+
registerSegments(env);
|
|
14183
14705
|
registerRead(env);
|
|
14184
14706
|
registerFind(env);
|
|
14185
14707
|
registerStatus(env);
|
|
@@ -14187,11 +14709,33 @@ function registerBridgeTools(env) {
|
|
|
14187
14709
|
registerMonitor(env);
|
|
14188
14710
|
registerArchive(env);
|
|
14189
14711
|
}
|
|
14712
|
+
/** 校验并规范化 coRules 参数为 CoTRule[];非法项抛清晰错误。 */
|
|
14713
|
+
function parseCoRules(raw) {
|
|
14714
|
+
return raw.map((rule, index) => {
|
|
14715
|
+
return {
|
|
14716
|
+
match: rule.match === "contains" || rule.match === "not-contains" ? rule.match : (() => {
|
|
14717
|
+
throw new Error("coRules[" + index + "].match must be \"contains\" or \"not-contains\"");
|
|
14718
|
+
})(),
|
|
14719
|
+
field: rule.field === "reasoning" || rule.field === "text" || rule.field === "both" ? rule.field : (() => {
|
|
14720
|
+
throw new Error("coRules[" + index + "].field must be \"reasoning\", \"text\" or \"both\"");
|
|
14721
|
+
})(),
|
|
14722
|
+
action: rule.action === "steer" || rule.action === "cancel" ? rule.action : (() => {
|
|
14723
|
+
throw new Error("coRules[" + index + "].action must be \"steer\" or \"cancel\"");
|
|
14724
|
+
})(),
|
|
14725
|
+
value: typeof rule.value === "string" && rule.value.trim() !== "" ? rule.value.trim() : (() => {
|
|
14726
|
+
throw new Error("coRules[" + index + "].value must be a non-empty string");
|
|
14727
|
+
})(),
|
|
14728
|
+
...typeof rule.message === "string" && rule.message.trim() !== "" ? { message: rule.message.trim() } : {}
|
|
14729
|
+
};
|
|
14730
|
+
});
|
|
14731
|
+
}
|
|
14190
14732
|
function renderMonitorState(entry) {
|
|
14191
14733
|
const lines = [];
|
|
14192
14734
|
lines.push(`monitoring ${entry.config.sessionId}${entry.config.label === void 0 ? "" : " (\"" + entry.config.label + "\")"}`);
|
|
14193
14735
|
lines.push(`interval: ${entry.config.intervalMs}ms | stalled: ${String(entry.config.stalledMs ?? 6e4)}ms | maxStuck: ${String(entry.config.maxStuckCycles ?? 3)}`);
|
|
14194
14736
|
lines.push(`cycles: ${entry.cycles} | stuck: ${entry.stuckCount} | lastAction: ${entry.lastAction}`);
|
|
14737
|
+
const cotCount = (entry.config.coTRules ?? []).length;
|
|
14738
|
+
if (cotCount > 0) lines.push(`coRules: ${cotCount} rule(s) (minHits ${String(entry.config.cotMinHits ?? 1)})`);
|
|
14195
14739
|
if (entry.lastNote !== "") lines.push(`note: ${entry.lastNote}`);
|
|
14196
14740
|
if (entry.done) lines.push("done: yes");
|
|
14197
14741
|
return lines;
|
|
@@ -14199,7 +14743,7 @@ function renderMonitorState(entry) {
|
|
|
14199
14743
|
function registerMonitor(env) {
|
|
14200
14744
|
env.ctx.tools.register(defineTool({
|
|
14201
14745
|
name: "session_bridge_monitor_start",
|
|
14202
|
-
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.",
|
|
14746
|
+
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.",
|
|
14203
14747
|
parameters: {
|
|
14204
14748
|
sessionId: {
|
|
14205
14749
|
type: "string",
|
|
@@ -14238,6 +14782,51 @@ function registerMonitor(env) {
|
|
|
14238
14782
|
label: {
|
|
14239
14783
|
type: "string",
|
|
14240
14784
|
description: "Optional human label for logs/display."
|
|
14785
|
+
},
|
|
14786
|
+
coRules: {
|
|
14787
|
+
type: "array",
|
|
14788
|
+
items: {
|
|
14789
|
+
type: "object",
|
|
14790
|
+
additionalProperties: false,
|
|
14791
|
+
properties: {
|
|
14792
|
+
match: {
|
|
14793
|
+
type: "string",
|
|
14794
|
+
required: true,
|
|
14795
|
+
enum: ["contains", "not-contains"],
|
|
14796
|
+
description: "contains = must include value in the chosen field; not-contains = must NOT include it."
|
|
14797
|
+
},
|
|
14798
|
+
field: {
|
|
14799
|
+
type: "string",
|
|
14800
|
+
required: true,
|
|
14801
|
+
enum: [
|
|
14802
|
+
"reasoning",
|
|
14803
|
+
"text",
|
|
14804
|
+
"both"
|
|
14805
|
+
],
|
|
14806
|
+
description: "Match on reasoning (chain-of-thought), text (reply), or both (either)."
|
|
14807
|
+
},
|
|
14808
|
+
value: {
|
|
14809
|
+
type: "string",
|
|
14810
|
+
required: true,
|
|
14811
|
+
description: "The substring to match (non-empty)."
|
|
14812
|
+
},
|
|
14813
|
+
action: {
|
|
14814
|
+
type: "string",
|
|
14815
|
+
required: true,
|
|
14816
|
+
enum: ["steer", "cancel"],
|
|
14817
|
+
description: "steer injects a guiding message; cancel terminates the session."
|
|
14818
|
+
},
|
|
14819
|
+
message: {
|
|
14820
|
+
type: "string",
|
|
14821
|
+
description: "Custom steer text (default is a guidance prompt)."
|
|
14822
|
+
}
|
|
14823
|
+
}
|
|
14824
|
+
},
|
|
14825
|
+
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."
|
|
14826
|
+
},
|
|
14827
|
+
cotMinHits: {
|
|
14828
|
+
type: "number",
|
|
14829
|
+
description: "How many consecutive matched polls before a CoT rule fires (default 1 = immediately)."
|
|
14241
14830
|
}
|
|
14242
14831
|
},
|
|
14243
14832
|
output: {
|
|
@@ -14268,7 +14857,9 @@ function registerMonitor(env) {
|
|
|
14268
14857
|
...args.useLlm === true ? { useLlm: true } : {},
|
|
14269
14858
|
...typeof args.onStallSteer === "string" && args.onStallSteer.trim() !== "" ? { onStallSteer: args.onStallSteer.trim() } : {},
|
|
14270
14859
|
...typeof args.onOffTrackSteer === "string" && args.onOffTrackSteer.trim() !== "" ? { onOffTrackSteer: args.onOffTrackSteer.trim() } : {},
|
|
14271
|
-
...typeof args.label === "string" && args.label.trim() !== "" ? { label: args.label.trim() } : {}
|
|
14860
|
+
...typeof args.label === "string" && args.label.trim() !== "" ? { label: args.label.trim() } : {},
|
|
14861
|
+
...Array.isArray(args.coRules) && args.coRules.length > 0 ? { coTRules: parseCoRules(args.coRules) } : {},
|
|
14862
|
+
...typeof args.cotMinHits === "number" && Number.isInteger(args.cotMinHits) && args.cotMinHits >= 1 ? { cotMinHits: Math.floor(args.cotMinHits) } : {}
|
|
14272
14863
|
};
|
|
14273
14864
|
const entry = env.monitor.start(config);
|
|
14274
14865
|
env.registry.touch(args.sessionId);
|
|
@@ -14340,7 +14931,19 @@ function registerMonitor(env) {
|
|
|
14340
14931
|
}
|
|
14341
14932
|
}));
|
|
14342
14933
|
}
|
|
14343
|
-
/**
|
|
14934
|
+
/** 按 reasoning 选项裁剪快照里的思维链字段(避免入 token 时有 diff 语义差)。 */
|
|
14935
|
+
function pruneReasoning(snapshot, mode) {
|
|
14936
|
+
if (mode === "tail") return snapshot;
|
|
14937
|
+
const rest = { ...snapshot };
|
|
14938
|
+
delete rest.reasoningTail;
|
|
14939
|
+
if (mode === "none") {
|
|
14940
|
+
delete rest.lastReasoning;
|
|
14941
|
+
delete rest.liveReasoning;
|
|
14942
|
+
} else if (mode === "last") delete rest.liveReasoning;
|
|
14943
|
+
else delete rest.lastReasoning;
|
|
14944
|
+
return rest;
|
|
14945
|
+
}
|
|
14946
|
+
/** 渲染监控快照为一行摘要:运行态 + openTurn + 卡住/待处理 + 最新回复(+ 思维链预览)。 */
|
|
14344
14947
|
function renderStatus(snapshot, stalledMsThreshold) {
|
|
14345
14948
|
const lines = [];
|
|
14346
14949
|
const runLabel = snapshot.running === "running" ? "running" : "idle";
|
|
@@ -14353,12 +14956,16 @@ function renderStatus(snapshot, stalledMsThreshold) {
|
|
|
14353
14956
|
}
|
|
14354
14957
|
if (snapshot.pendingWork) lines.push(`pendingWork: ${snapshot.nextTurnCount} turn + ${snapshot.nextStepCount} step`);
|
|
14355
14958
|
if (snapshot.lastAssistantText !== void 0) lines.push(`lastReply: ${snapshot.lastAssistantText}`);
|
|
14959
|
+
if (snapshot.reasoningTail !== void 0 && snapshot.reasoningTail !== "") {
|
|
14960
|
+
const preview = snapshot.reasoningTail.length > 160 ? snapshot.reasoningTail.slice(0, 160) + "…" : snapshot.reasoningTail;
|
|
14961
|
+
lines.push(`reasoning: ${preview}`);
|
|
14962
|
+
}
|
|
14356
14963
|
return lines;
|
|
14357
14964
|
}
|
|
14358
14965
|
function registerStatus(env) {
|
|
14359
14966
|
env.ctx.tools.register(defineTool({
|
|
14360
14967
|
name: "session_bridge_status",
|
|
14361
|
-
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.",
|
|
14968
|
+
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.",
|
|
14362
14969
|
parameters: {
|
|
14363
14970
|
sessionId: {
|
|
14364
14971
|
type: "string",
|
|
@@ -14372,6 +14979,16 @@ function registerStatus(env) {
|
|
|
14372
14979
|
recent: {
|
|
14373
14980
|
type: "number",
|
|
14374
14981
|
description: "Number of recent messages to include in the snapshot (default 8, max 20)."
|
|
14982
|
+
},
|
|
14983
|
+
reasoning: {
|
|
14984
|
+
type: "string",
|
|
14985
|
+
enum: [
|
|
14986
|
+
"none",
|
|
14987
|
+
"last",
|
|
14988
|
+
"live",
|
|
14989
|
+
"tail"
|
|
14990
|
+
],
|
|
14991
|
+
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."
|
|
14375
14992
|
}
|
|
14376
14993
|
},
|
|
14377
14994
|
output: {
|
|
@@ -14397,7 +15014,8 @@ function registerStatus(env) {
|
|
|
14397
15014
|
const agent = liveAgent(env, args.sessionId);
|
|
14398
15015
|
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)");
|
|
14399
15016
|
const threshold = typeof args.stalledMsThreshold === "number" && Number.isFinite(args.stalledMsThreshold) && args.stalledMsThreshold >= 0 ? args.stalledMsThreshold : 6e4;
|
|
14400
|
-
const
|
|
15017
|
+
const reasoningMode = args.reasoning === "none" || args.reasoning === "last" || args.reasoning === "live" ? args.reasoning : "tail";
|
|
15018
|
+
const snapshot = pruneReasoning(statusSnapshot(env.ctx, agent), reasoningMode);
|
|
14401
15019
|
const shown = typeof args.recent === "number" && Number.isInteger(args.recent) && args.recent >= 0 ? Math.min(args.recent, 20) : 8;
|
|
14402
15020
|
const envCwd = agent.session.header.cwd;
|
|
14403
15021
|
env.registry.touch(args.sessionId, {
|
|
@@ -14540,7 +15158,6 @@ function registerArchive(env) {
|
|
|
14540
15158
|
async execute(args) {
|
|
14541
15159
|
const items = env.ctx.workspaceRegistry.archivedSessionIds.map(String).map((id) => ({ sessionId: id }));
|
|
14542
15160
|
if (args.resolveTitles === true && items.length > 0) {
|
|
14543
|
-
const persistence = env.ctx.sessionPersistence;
|
|
14544
15161
|
const registryTitles = /* @__PURE__ */ new Map();
|
|
14545
15162
|
try {
|
|
14546
15163
|
const records = await env.registry.all();
|
|
@@ -14552,7 +15169,7 @@ function registerArchive(env) {
|
|
|
14552
15169
|
continue;
|
|
14553
15170
|
}
|
|
14554
15171
|
try {
|
|
14555
|
-
item.title = titleOf((await
|
|
15172
|
+
item.title = titleOf((await inspectPersistedSession(env.ctx, item.sessionId)).events);
|
|
14556
15173
|
} catch {}
|
|
14557
15174
|
}
|
|
14558
15175
|
}
|