evolcore 0.0.28 → 0.0.29
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/CHANGELOG.md +12 -0
- package/bin/ec-safe-output.js +270 -54
- package/dist/agents/baseagent-availability.js +37 -1
- package/dist/agents/baseagent.js +19 -9
- package/dist/agents/claude-runner.js +171 -13
- package/dist/agents/codex/app-server-client.js +6 -2
- package/dist/agents/codex/diagnostics.js +4 -3
- package/dist/agents/codex-runner.js +396 -95
- package/dist/agents/gemini-runner.js +35 -17
- package/dist/aun/group/history.js +130 -90
- package/dist/aun/group/operations.js +1 -3
- package/dist/aun/menu/transport.js +5 -3
- package/dist/aun/msg/managed-operation.js +70 -0
- package/dist/aun/msg/p2p.js +1 -3
- package/dist/aun/msg/upload.js +2 -2
- package/dist/aun/outbox.js +1 -37
- package/dist/channels/aun.js +249 -89
- package/dist/channels/daemon.js +18 -1
- package/dist/channels/feishu.js +1 -2
- package/dist/cli/agent-command.js +23 -3
- package/dist/cli/agent-create.js +68 -2
- package/dist/cli/agent.js +12 -2
- package/dist/cli/aun-commands.js +87 -13
- package/dist/cli/cli-argv.js +26 -0
- package/dist/cli/daemon-commands.js +40 -10
- package/dist/cli/data-command.js +4 -4
- package/dist/cli/handoff-command.js +2 -0
- package/dist/cli/index.js +18 -1
- package/dist/cli/managed-command-guard.js +10 -0
- package/dist/cli/memory-command.js +441 -0
- package/dist/cli/restart-monitor.js +62 -6
- package/dist/cli/skill-command.js +41 -0
- package/dist/cli/task-context.js +2 -23
- package/dist/cli/trigger-command.js +9 -1
- package/dist/cli/watch-msg-diag.js +209 -0
- package/dist/config/config-field-policy.js +52 -3
- package/dist/config/config-manager.js +17 -4
- package/dist/config/contact-book-store.js +2 -2
- package/dist/config/mention-mode.js +7 -2
- package/dist/config/merge.js +28 -9
- package/dist/config/resolved-config-op.js +1 -1
- package/dist/config/runtime-env.js +137 -0
- package/dist/config/schema-registry.js +11 -1
- package/dist/config-store.js +10 -10
- package/dist/core/audit/event-key.js +3 -1
- package/dist/core/audit/log-integrity.js +4 -0
- package/dist/core/auth/agent-delegation.js +2 -1
- package/dist/core/auth/auth-gateway.js +13 -6
- package/dist/core/auth/authorization-audit.js +54 -19
- package/dist/core/auth/operation-authorizer.js +17 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/causation/context.js +5 -1
- package/dist/core/command/agent-control.js +1 -2
- package/dist/core/command/command-handler.js +51 -23
- package/dist/core/command/memory-slash.js +270 -0
- package/dist/core/command/menu-catalog.js +1 -0
- package/dist/core/command/menu-handler.js +315 -60
- package/dist/core/command/menu-protocol.js +42 -4
- package/dist/core/command/slash-gate.js +10 -10
- package/dist/core/command/slash-handler.js +206 -64
- package/dist/core/data-migration.js +282 -505
- package/dist/core/event-catalog.js +26 -0
- package/dist/core/evolagent-registry.js +6 -0
- package/dist/core/evolagent.js +0 -10
- package/dist/core/handoff/store.js +1 -20
- package/dist/core/inference/text-inference.js +57 -1
- package/dist/core/memory/consolidation-context.js +505 -0
- package/dist/core/memory/consolidation-ops-records.js +124 -0
- package/dist/core/memory/consolidation-trigger.js +394 -0
- package/dist/core/memory/index-manager.js +308 -0
- package/dist/core/memory/launch-consolidation.js +395 -0
- package/dist/core/memory/memory-admin.js +234 -0
- package/dist/core/memory/memory-document.js +100 -0
- package/dist/core/memory/memory-session-policy.js +41 -0
- package/dist/core/memory/memory-signature.js +257 -0
- package/dist/core/memory/memory-writer.js +432 -0
- package/dist/core/memory/staleness.js +117 -0
- package/dist/core/message/auto-mention-gate.js +454 -0
- package/dist/core/message/im-renderer.js +69 -16
- package/dist/core/message/message-bridge.js +287 -21
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +114 -27
- package/dist/core/message/pending-hints.js +329 -161
- package/dist/core/message/response-engine.js +567 -80
- package/dist/core/permission/ec-command-parser.js +1029 -80
- package/dist/core/permission/protected-paths.js +376 -126
- package/dist/core/permission/sandbox-runtime.js +40 -12
- package/dist/core/permission/tool-error-code.js +96 -13
- package/dist/core/permission/tool-policy.js +276 -23
- package/dist/core/role/runtime-policy.js +3 -7
- package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
- package/dist/core/session/session-fs-store.js +0 -76
- package/dist/core/session/session-idle-monitor.js +355 -0
- package/dist/core/session/session-manager.js +478 -17
- package/dist/core/session/session-mapper.js +33 -16
- package/dist/core/session/session-renew.js +217 -155
- package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
- package/dist/core/skill/global-watcher.js +282 -0
- package/dist/core/skill/index-generator.js +84 -0
- package/dist/core/skill/startup.js +73 -0
- package/dist/core/skill/switch-resolver.js +115 -0
- package/dist/core/task/task-activity-store.js +219 -0
- package/dist/eck/kit-renderer.js +30 -3
- package/dist/eck/manifest-engine.js +26 -4
- package/dist/index.js +708 -18
- package/dist/ipc.js +564 -2
- package/dist/paths.js +30 -9
- package/dist/response-system/engines/v1/proactive-flow.js +116 -15
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +1 -0
- package/dist/trigger/manager.js +0 -10
- package/dist/trigger/scheduler.js +11 -0
- package/dist/trigger/validation.js +22 -0
- package/dist/utils/avatar-upload.js +59 -18
- package/dist/utils/error-dict.json +6 -3
- package/dist/utils/error-utils.js +27 -0
- package/dist/utils/logger.js +32 -10
- package/dist/utils/media-cache.js +84 -0
- package/dist/utils/msg-diag.js +97 -0
- package/dist/utils/welcome.js +37 -8
- package/kits/docs/AUTHORING.md +314 -0
- package/kits/docs/GUIDE.md +2 -0
- package/kits/docs/INDEX.md +2 -0
- package/kits/docs/aun/CHEATSHEET.md +4 -0
- package/kits/docs/aun/SDK.md +60 -0
- package/kits/docs/context-assembly.md +10 -6
- package/kits/docs/evolcore/INDEX.md +17 -3
- package/kits/docs/evolcore/agent-create.md +24 -0
- package/kits/docs/evolcore/agent.md +5 -2
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/ec-capabilities.md +364 -0
- package/kits/docs/evolcore/group.md +34 -18
- package/kits/docs/evolcore/memory-architecture.md +78 -0
- package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
- package/kits/docs/evolcore/memory-consolidation.md +263 -0
- package/kits/docs/evolcore/memory-tags.md +63 -0
- package/kits/docs/evolcore/memory.md +110 -0
- package/kits/docs/evolcore/menu-client.md +27 -11
- package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
- package/kits/docs/evolcore/menu-protocol.md +213 -0
- package/kits/docs/evolcore/msg.md +2 -0
- package/kits/docs/evolcore/trigger.md +7 -5
- package/kits/docs/path-registry.md +6 -8
- package/kits/docs/prompt-loading-architecture.md +5 -3
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +294 -88
- package/kits/eck_manifest.memory.json +257 -0
- package/kits/rules/03-identity.md +8 -8
- package/kits/rules/05-venue.md +4 -2
- package/kits/rules/06-channel.md +1 -1
- package/kits/schemas/_meta.json +276 -47
- package/kits/schemas/agent-config.schema.12.json +38 -2
- package/kits/schemas/agent-config.schema.13.json +591 -0
- package/kits/schemas/daemon.schema.6.json +9 -9
- package/kits/schemas/defaults.schema.5.json +29 -0
- package/kits/schemas/relation-config.schema.8.json +14 -1
- package/kits/schemas/relation-config.schema.9.json +129 -0
- package/kits/schemas/role-config.schema.1.json +3 -3
- package/kits/skills/.switches.json +3 -0
- package/kits/skills/ec-skill/SKILL.md +217 -0
- package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
- package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
- package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
- package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
- package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
- package/kits/templates/decision/auto-mention.md +25 -0
- package/kits/templates/decision/session-renew.md +22 -0
- package/kits/templates/memory-consolidation/forget.md +104 -0
- package/kits/templates/memory-consolidation/generalize.md +76 -0
- package/kits/templates/memory-consolidation/notes.md +133 -0
- package/kits/templates/memory-consolidation/profile.md +58 -0
- package/kits/templates/memory-consolidation/promote.md +72 -0
- package/kits/templates/memory-consolidation/workflow.md +75 -0
- package/kits/templates/system-fragments/bootstrap.md +14 -5
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/group-history.md +6 -0
- package/kits/templates/system-fragments/identity.md +0 -3
- package/kits/templates/system-fragments/memory.md +79 -0
- package/kits/templates/system-fragments/reply.md +1 -0
- package/kits/templates/system-fragments/venue.md +1 -1
- package/package.json +4 -6
- package/dist/trigger/legacy-session-history.js +0 -19
- package/kits/templates/system-fragments/group-unread-history.md +0 -7
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import { formatTimestamp } from './session-fs-store.js';
|
|
2
2
|
/** Default topic component used by the stable session routing key. */
|
|
3
3
|
export const DEFAULT_THREAD_ID = 'main';
|
|
4
|
-
|
|
5
|
-
export function normalizeTopicLable(value) {
|
|
4
|
+
export function normalizeTopicLabel(value) {
|
|
6
5
|
if (value === undefined || value === null)
|
|
7
6
|
return undefined;
|
|
8
7
|
if (typeof value !== 'string')
|
|
9
|
-
throw new Error('
|
|
10
|
-
const
|
|
11
|
-
if (
|
|
12
|
-
throw new Error('
|
|
8
|
+
throw new Error('label 必须是字符串或 null');
|
|
9
|
+
const label = value.trim();
|
|
10
|
+
if (label.length > 100 || /[\x00-\x1F\x7F]/.test(label)) {
|
|
11
|
+
throw new Error('label 最多 100 个字符,且不能包含控制字符');
|
|
13
12
|
}
|
|
14
|
-
return
|
|
13
|
+
return label || undefined;
|
|
15
14
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
export function normalizeTopicStatus(value) {
|
|
16
|
+
if (value === 'pending' || value === 'in_progress' || value === 'completed' || value === 'archived')
|
|
17
|
+
return value;
|
|
18
|
+
throw new Error('topicStatus 必须是 pending、in_progress、completed 或 archived');
|
|
20
19
|
}
|
|
21
20
|
/** Resolve the public archived query flag while keeping omitted values backward compatible. */
|
|
22
21
|
export function resolveTopicArchiveFilter(value) {
|
|
@@ -28,13 +27,26 @@ export function resolveTopicArchiveFilter(value) {
|
|
|
28
27
|
return 'all';
|
|
29
28
|
throw new Error('archived 必须是布尔值或 "all"');
|
|
30
29
|
}
|
|
31
|
-
|
|
32
|
-
export function readTopicLable(payload) {
|
|
30
|
+
export function readTopicLabel(payload) {
|
|
33
31
|
if (!payload || typeof payload !== 'object')
|
|
34
32
|
return undefined;
|
|
35
33
|
const fields = payload;
|
|
36
34
|
try {
|
|
37
|
-
|
|
35
|
+
const label = fields.label === undefined ? fields.lable : fields.label;
|
|
36
|
+
return label === '归档' && fields.label === undefined ? undefined : normalizeTopicLabel(label);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function readTopicStatus(payload) {
|
|
43
|
+
if (!payload || typeof payload !== 'object')
|
|
44
|
+
return undefined;
|
|
45
|
+
try {
|
|
46
|
+
const fields = payload;
|
|
47
|
+
if (fields.topicStatus === undefined && fields.lable === '归档')
|
|
48
|
+
return 'archived';
|
|
49
|
+
return normalizeTopicStatus(fields.topicStatus);
|
|
38
50
|
}
|
|
39
51
|
catch {
|
|
40
52
|
return undefined;
|
|
@@ -103,7 +115,8 @@ export function sessionToFile(session) {
|
|
|
103
115
|
projectPath: session.projectPath,
|
|
104
116
|
agentSessionId: session.agentSessionId ?? null,
|
|
105
117
|
name: session.name ?? null,
|
|
106
|
-
...(session.
|
|
118
|
+
...(session.label ? { label: session.label } : {}),
|
|
119
|
+
...(session.threadId ? { topicStatus: session.topicStatus ?? 'pending' } : {}),
|
|
107
120
|
activeTask: session.processingState ?? null,
|
|
108
121
|
metadata,
|
|
109
122
|
createdAt: session.createdAt,
|
|
@@ -153,7 +166,11 @@ export function fileToSession(file) {
|
|
|
153
166
|
projectPath: file.projectPath,
|
|
154
167
|
agentSessionId: file.agentSessionId ?? undefined,
|
|
155
168
|
name: file.name ?? undefined,
|
|
156
|
-
...(file.
|
|
169
|
+
...(file.threadId && (file.label || (file.lable && file.lable !== '归档'))
|
|
170
|
+
? { label: file.label || file.lable } : {}),
|
|
171
|
+
...(file.threadId ? { topicStatus: file.topicStatus
|
|
172
|
+
? normalizeTopicStatus(file.topicStatus)
|
|
173
|
+
: file.lable === '归档' ? 'archived' : 'pending' } : {}),
|
|
157
174
|
processingState: file.activeTask ?? undefined,
|
|
158
175
|
metadata,
|
|
159
176
|
createdAt: file.createdAt,
|
|
@@ -1,25 +1,26 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
1
3
|
import { resolveEffective } from '../../config/config-manager.js';
|
|
2
4
|
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
3
5
|
import { logger } from '../../utils/logger.js';
|
|
4
6
|
import { isTransientProtocolMessage, messageLogPath } from '../message/message-log.js';
|
|
5
7
|
import { readAllJsonlLines } from './session-fs-store.js';
|
|
6
|
-
import {
|
|
7
|
-
import { registerBuiltinModes } from '../../response-system/modes/index.js';
|
|
8
|
-
import { normalizeBaseagent } from '../../agents/baseagent.js';
|
|
8
|
+
import { getPackageRoot } from '../../paths.js';
|
|
9
9
|
const DEFAULT_AFTER_HOURS = 24;
|
|
10
10
|
const DEFAULT_FALLBACK = 'continue';
|
|
11
|
-
const
|
|
11
|
+
const DEFAULT_NEW_PROBABILITY_THRESHOLD = 0.85;
|
|
12
|
+
const MAX_CONTEXT_MESSAGES = 30;
|
|
13
|
+
const PRESERVED_HEAD_MESSAGES = 5;
|
|
14
|
+
const PRESERVED_TAIL_MESSAGES = 25;
|
|
12
15
|
const MAX_CONTEXT_CHARS = 16_000;
|
|
13
|
-
const MAX_MESSAGE_CHARS =
|
|
16
|
+
const MAX_MESSAGE_CHARS = 800;
|
|
17
|
+
const MAX_MESSAGE_HEAD_CHARS = 600;
|
|
18
|
+
const MAX_MESSAGE_TAIL_CHARS = 200;
|
|
14
19
|
const MODEL_TIMEOUT_MS = 10_000;
|
|
15
20
|
const RECENT_DECISION_TTL_MS = 30_000;
|
|
16
21
|
const FRESH_EMPTY_SESSION_TTL_MS = 30_000;
|
|
17
22
|
const RENEWED_AT_METADATA_KEY = '__sessionRenewedAt';
|
|
18
23
|
const CREATED_AT_METADATA_KEY = '__sessionCreatedAt';
|
|
19
|
-
const NEW_CONFIDENCE_THRESHOLD = 0.85;
|
|
20
|
-
const AUXILIARY_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
21
|
-
const EXPLICIT_NEW_RE = /^(?:新话题|换个话题|重新开始|新开会话|新会话)[::,,;;。!!\n]/;
|
|
22
|
-
const EXPLICIT_CONTINUE_RE = /^(?:继续上次|延续上次|继续|接着)[::,,;;。!!\n]/;
|
|
23
24
|
const NON_CONVERSATION_TYPES = new Set([
|
|
24
25
|
'command',
|
|
25
26
|
'thought',
|
|
@@ -28,13 +29,59 @@ const NON_CONVERSATION_TYPES = new Set([
|
|
|
28
29
|
'tool_call',
|
|
29
30
|
'tool_result',
|
|
30
31
|
]);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
const SESSION_RENEW_TEMPLATE_PATH = path.join(getPackageRoot(), 'kits', 'templates', 'decision', 'session-renew.md');
|
|
33
|
+
const DEFAULT_SESSION_RENEW_PROMPT = {
|
|
34
|
+
stateRules: [
|
|
35
|
+
'会话边界判定规则:',
|
|
36
|
+
'1. 如果当前消息的内容主题已经转向另一件事,应判定为新会话,即使没有使用会话命令词。',
|
|
37
|
+
'2. 如果当前消息表达暂停当前主题并转向另一件事,也应判定为新会话。',
|
|
38
|
+
'3. 如果当前消息仍围绕历史主题补充、追问、修改或执行,应判定为继续当前会话。',
|
|
39
|
+
'4. 如果信息不足、无法可靠判断主题关系,应保留不确定性,不要默认新建或继续。',
|
|
40
|
+
'5. 重点判断内容主题和上下文关系,不要只匹配“新开”“重新”“换个”“继续”等关键词。',
|
|
41
|
+
].join('\n'),
|
|
42
|
+
questionInstructions: '判断当前消息相对于历史会话是否应该开启独立的新会话,重点依据内容主题和上下文关系。',
|
|
43
|
+
trueCriteria: '当前消息已经转向另一件具体事情,或表达了停止当前主题、之后开始另一件事的边界意图。',
|
|
44
|
+
falseCriteria: '当前消息仍然围绕历史主题补充、追问、修改或执行。',
|
|
45
|
+
};
|
|
46
|
+
let cachedSessionRenewPrompt;
|
|
47
|
+
function parseSessionRenewPromptTemplate(source) {
|
|
48
|
+
const sections = new Map();
|
|
49
|
+
const matches = [...source.matchAll(/^##\s+(.+)\s*$/gmu)];
|
|
50
|
+
for (const [index, match] of matches.entries()) {
|
|
51
|
+
const name = match[1].trim();
|
|
52
|
+
const start = (match.index ?? 0) + match[0].length;
|
|
53
|
+
const end = matches[index + 1]?.index ?? source.length;
|
|
54
|
+
sections.set(name, source.slice(start, end).trim());
|
|
55
|
+
}
|
|
56
|
+
const prompt = {
|
|
57
|
+
stateRules: sections.get('State Rules'),
|
|
58
|
+
questionInstructions: sections.get('Question Instructions'),
|
|
59
|
+
trueCriteria: sections.get('True Criteria'),
|
|
60
|
+
falseCriteria: sections.get('False Criteria'),
|
|
61
|
+
};
|
|
62
|
+
return Object.values(prompt).every(value => value)
|
|
63
|
+
? prompt
|
|
64
|
+
: undefined;
|
|
65
|
+
}
|
|
66
|
+
export function loadSessionRenewPrompt() {
|
|
67
|
+
if (cachedSessionRenewPrompt)
|
|
68
|
+
return cachedSessionRenewPrompt;
|
|
69
|
+
try {
|
|
70
|
+
const parsed = parseSessionRenewPromptTemplate(fs.readFileSync(SESSION_RENEW_TEMPLATE_PATH, 'utf8'));
|
|
71
|
+
if (parsed) {
|
|
72
|
+
cachedSessionRenewPrompt = parsed;
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
logger.warn(`[SessionRenewPrompt] Missing required sections: ${SESSION_RENEW_TEMPLATE_PATH}`);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
logger.warn(`[SessionRenewPrompt] Failed to load ${SESSION_RENEW_TEMPLATE_PATH}: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
|
+
}
|
|
80
|
+
cachedSessionRenewPrompt = DEFAULT_SESSION_RENEW_PROMPT;
|
|
81
|
+
return DEFAULT_SESSION_RENEW_PROMPT;
|
|
82
|
+
}
|
|
83
|
+
export function resetSessionRenewPromptForTests() {
|
|
84
|
+
cachedSessionRenewPrompt = undefined;
|
|
38
85
|
}
|
|
39
86
|
export function isSessionRenewConversationEntry(entry, sessionId) {
|
|
40
87
|
return entry.sessionId === sessionId
|
|
@@ -43,26 +90,52 @@ export function isSessionRenewConversationEntry(entry, sessionId) {
|
|
|
43
90
|
&& entry.content.trim().length > 0;
|
|
44
91
|
}
|
|
45
92
|
export function selectSessionRenewContext(entries) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
93
|
+
const selected = entries.length <= MAX_CONTEXT_MESSAGES
|
|
94
|
+
? entries
|
|
95
|
+
: [
|
|
96
|
+
...entries.slice(0, PRESERVED_HEAD_MESSAGES),
|
|
97
|
+
...entries.slice(-PRESERVED_TAIL_MESSAGES),
|
|
98
|
+
];
|
|
99
|
+
return selected.map(entry => ({
|
|
100
|
+
...entry,
|
|
101
|
+
content: truncateSessionRenewMessage(entry.content),
|
|
102
|
+
}));
|
|
50
103
|
}
|
|
51
|
-
function
|
|
52
|
-
if (
|
|
53
|
-
return
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
104
|
+
function truncateSessionRenewMessage(content) {
|
|
105
|
+
if (content.length <= MAX_MESSAGE_CHARS)
|
|
106
|
+
return content;
|
|
107
|
+
const marker = '[中间内容已省略]';
|
|
108
|
+
const available = MAX_MESSAGE_CHARS - marker.length;
|
|
109
|
+
const headBudget = Math.min(MAX_MESSAGE_HEAD_CHARS, available);
|
|
110
|
+
const tailBudget = Math.min(MAX_MESSAGE_TAIL_CHARS, available - headBudget);
|
|
111
|
+
const head = takeParagraphBoundary(content, headBudget, false);
|
|
112
|
+
const tail = takeParagraphBoundary(content, tailBudget, true);
|
|
113
|
+
return `${head}${marker}${tail}`.slice(0, MAX_MESSAGE_CHARS);
|
|
114
|
+
}
|
|
115
|
+
function takeParagraphBoundary(content, budget, fromEnd) {
|
|
116
|
+
if (budget <= 0)
|
|
117
|
+
return '';
|
|
118
|
+
if (content.length <= budget)
|
|
119
|
+
return content;
|
|
120
|
+
const paragraphs = content.split(/\n\s*\n/u).filter(Boolean);
|
|
121
|
+
if (paragraphs.length <= 1) {
|
|
122
|
+
return fromEnd ? content.slice(-budget) : content.slice(0, budget);
|
|
123
|
+
}
|
|
124
|
+
const selected = [];
|
|
125
|
+
let used = 0;
|
|
126
|
+
const ordered = fromEnd ? [...paragraphs].reverse() : paragraphs;
|
|
127
|
+
for (const paragraph of ordered) {
|
|
128
|
+
const separatorLength = selected.length > 0 ? 2 : 0;
|
|
129
|
+
if (used + separatorLength + paragraph.length > budget)
|
|
61
130
|
break;
|
|
62
|
-
|
|
63
|
-
|
|
131
|
+
selected.push(paragraph);
|
|
132
|
+
used += separatorLength + paragraph.length;
|
|
64
133
|
}
|
|
65
|
-
|
|
134
|
+
if (selected.length === 0) {
|
|
135
|
+
return fromEnd ? content.slice(-budget) : content.slice(0, budget);
|
|
136
|
+
}
|
|
137
|
+
const result = fromEnd ? selected.reverse() : selected;
|
|
138
|
+
return result.join('\n\n');
|
|
66
139
|
}
|
|
67
140
|
export class SessionRenewService {
|
|
68
141
|
sessionManager;
|
|
@@ -72,14 +145,12 @@ export class SessionRenewService {
|
|
|
72
145
|
now;
|
|
73
146
|
effectiveConfigResolver;
|
|
74
147
|
eventBus;
|
|
75
|
-
responseModeRegistry = new ResponseModeRegistry();
|
|
76
148
|
constructor(sessionManager, inferenceProviderResolver, options = {}) {
|
|
77
149
|
this.sessionManager = sessionManager;
|
|
78
150
|
this.inferenceProviderResolver = inferenceProviderResolver;
|
|
79
151
|
this.now = options.now ?? (() => Date.now());
|
|
80
152
|
this.effectiveConfigResolver = options.resolveEffectiveConfig;
|
|
81
153
|
this.eventBus = options.eventBus;
|
|
82
|
-
registerBuiltinModes(this.responseModeRegistry);
|
|
83
154
|
}
|
|
84
155
|
async resolve(request) {
|
|
85
156
|
const effectiveConfig = this.resolveEffectiveConfig(request);
|
|
@@ -107,6 +178,7 @@ export class SessionRenewService {
|
|
|
107
178
|
renewed: cached.decision === 'new',
|
|
108
179
|
decision: cached.decision,
|
|
109
180
|
source: cached.source,
|
|
181
|
+
newProbability: cached.newProbability,
|
|
110
182
|
};
|
|
111
183
|
}
|
|
112
184
|
}
|
|
@@ -144,47 +216,32 @@ export class SessionRenewService {
|
|
|
144
216
|
const idleMs = this.now() - last.ts;
|
|
145
217
|
if (idleMs <= afterHours * 60 * 60 * 1000)
|
|
146
218
|
return { session: request.session, renewed: false };
|
|
147
|
-
|
|
148
|
-
let decision
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
219
|
+
const threshold = validProbability(config.new_probability_threshold) ?? DEFAULT_NEW_PROBABILITY_THRESHOLD;
|
|
220
|
+
let decision;
|
|
221
|
+
let source = 'model';
|
|
222
|
+
let judgeMetadata = {
|
|
223
|
+
model: effectiveConfig.decision_model?.model,
|
|
224
|
+
threshold,
|
|
225
|
+
};
|
|
226
|
+
try {
|
|
227
|
+
const model = effectiveConfig.decision_model?.model?.trim();
|
|
228
|
+
if (!model)
|
|
229
|
+
throw new SessionRenewJudgeError('decision_model_missing');
|
|
230
|
+
const provider = this.inferenceProviderResolver.getDecisionInferenceProvider(request.selfAid);
|
|
231
|
+
if (!provider)
|
|
232
|
+
throw new SessionRenewJudgeError('provider_unavailable');
|
|
233
|
+
const judged = await this.judgeWithModel(request, entries, idleMs, provider, model);
|
|
234
|
+
const newProbability = judged.newProbability;
|
|
235
|
+
decision = newProbability >= threshold ? 'new' : 'continue';
|
|
236
|
+
judgeMetadata = { ...judgeMetadata, newProbability };
|
|
156
237
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
model: auxiliary.model,
|
|
163
|
-
effort: auxiliary.effort,
|
|
164
|
-
};
|
|
165
|
-
try {
|
|
166
|
-
if (!auxiliary.model)
|
|
167
|
-
throw new SessionRenewJudgeError('auxiliary_model_missing');
|
|
168
|
-
if (auxiliary.invalidEffort)
|
|
169
|
-
throw new SessionRenewJudgeError('auxiliary_effort_invalid');
|
|
170
|
-
const provider = this.inferenceProviderResolver.getTextInferenceProvider(request.channelName, request.session.baseagent, request.selfAid);
|
|
171
|
-
if (!provider)
|
|
172
|
-
throw new SessionRenewJudgeError('provider_unavailable');
|
|
173
|
-
const judged = await this.judgeWithModel(request, entries, idleMs, provider, auxiliary);
|
|
174
|
-
decision = judged.decision === 'new' && judged.confidence < NEW_CONFIDENCE_THRESHOLD
|
|
175
|
-
? 'continue'
|
|
176
|
-
: judged.decision;
|
|
177
|
-
source = 'model';
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
decision = config.fallback_action ?? DEFAULT_FALLBACK;
|
|
181
|
-
source = 'fallback';
|
|
182
|
-
judgeMetadata = { ...judgeMetadata, fallbackReason: sessionRenewFallbackReason(error) };
|
|
183
|
-
logger.warn(`[SessionRenew] judge failed, using fallback=${decision}: ${error instanceof Error ? error.message : String(error)}`);
|
|
184
|
-
}
|
|
185
|
-
return await this.finishDecision(routeKey, request, decision, source, idleMs, evaluationStartedAt, judgeMetadata);
|
|
238
|
+
catch (error) {
|
|
239
|
+
decision = config.fallback_action ?? DEFAULT_FALLBACK;
|
|
240
|
+
source = 'fallback';
|
|
241
|
+
judgeMetadata = { ...judgeMetadata, fallbackReason: sessionRenewFallbackReason(error) };
|
|
242
|
+
logger.warn(`[SessionRenew] judge failed, using fallback=${decision}: ${error instanceof Error ? error.message : String(error)}`);
|
|
186
243
|
}
|
|
187
|
-
return await this.finishDecision(routeKey, request, decision, source, idleMs, evaluationStartedAt);
|
|
244
|
+
return await this.finishDecision(routeKey, request, decision, source, idleMs, evaluationStartedAt, judgeMetadata);
|
|
188
245
|
}
|
|
189
246
|
async finishDecision(routeKey, request, decision, source, idleMs, evaluationStartedAt, judge) {
|
|
190
247
|
const selected = decision === 'new'
|
|
@@ -200,14 +257,14 @@ export class SessionRenewService {
|
|
|
200
257
|
selectedSessionId: selected.id,
|
|
201
258
|
decision,
|
|
202
259
|
source,
|
|
260
|
+
newProbability: judge?.newProbability,
|
|
203
261
|
expiresAt: this.now() + RECENT_DECISION_TTL_MS,
|
|
204
262
|
});
|
|
205
263
|
logger.info(`[SessionRenew] session=${request.session.id} selected=${selected.id} decision=${decision} source=${source}`
|
|
206
264
|
+ ` idleHours=${(idleMs / 3_600_000).toFixed(2)}`
|
|
207
|
-
+ ` responseMode=${judge?.responseMode ?? 'none'}`
|
|
208
|
-
+ ` baseagent=${judge?.baseagent ?? 'none'}`
|
|
209
265
|
+ ` model=${judge?.model ?? 'none'}`
|
|
210
|
-
+ `
|
|
266
|
+
+ ` newProbability=${judge?.newProbability ?? 'none'}`
|
|
267
|
+
+ ` threshold=${judge?.threshold ?? 'none'}`
|
|
211
268
|
+ ` fallbackReason=${judge?.fallbackReason ?? 'none'}`);
|
|
212
269
|
this.eventBus?.publish({
|
|
213
270
|
type: 'session:renew-evaluated',
|
|
@@ -221,16 +278,20 @@ export class SessionRenewService {
|
|
|
221
278
|
channelId: request.channelId,
|
|
222
279
|
selfAid: request.selfAid,
|
|
223
280
|
idleHours: Number((idleMs / 3_600_000).toFixed(2)),
|
|
224
|
-
responseMode: judge?.responseMode,
|
|
225
|
-
baseagent: judge?.baseagent,
|
|
226
281
|
model: judge?.model,
|
|
227
|
-
|
|
282
|
+
newProbability: judge?.newProbability,
|
|
228
283
|
fallbackReason: judge?.fallbackReason,
|
|
229
284
|
durationMs: Math.max(0, Date.now() - evaluationStartedAt),
|
|
230
285
|
timestamp: this.now(),
|
|
231
286
|
causation: request.causation,
|
|
232
287
|
});
|
|
233
|
-
return {
|
|
288
|
+
return {
|
|
289
|
+
session: selected,
|
|
290
|
+
renewed: decision === 'new',
|
|
291
|
+
decision,
|
|
292
|
+
source,
|
|
293
|
+
newProbability: judge?.newProbability,
|
|
294
|
+
};
|
|
234
295
|
}
|
|
235
296
|
resolveEffectiveConfig(request) {
|
|
236
297
|
if (this.effectiveConfigResolver)
|
|
@@ -246,76 +307,38 @@ export class SessionRenewService {
|
|
|
246
307
|
peerKey,
|
|
247
308
|
}, { cache: true, expand: true });
|
|
248
309
|
}
|
|
249
|
-
resolveAuxiliaryConfig(config, baseagent) {
|
|
250
|
-
const canonicalBaseagent = normalizeBaseagent(baseagent).canonical;
|
|
251
|
-
const baseagentConfig = canonicalBaseagent !== 'unknown'
|
|
252
|
-
? config.baseagents?.[canonicalBaseagent]
|
|
253
|
-
: undefined;
|
|
254
|
-
const rawBaseagentConfig = baseagentConfig && typeof baseagentConfig === 'object'
|
|
255
|
-
? baseagentConfig
|
|
256
|
-
: undefined;
|
|
257
|
-
// Auxiliary inference is owned exclusively by the selected baseagent. Old
|
|
258
|
-
// response-mode bucket values must be migrated before they can take effect.
|
|
259
|
-
const configuredMode = typeof config.responseMode === 'string' && config.responseMode.trim()
|
|
260
|
-
? config.responseMode.trim()
|
|
261
|
-
: undefined;
|
|
262
|
-
const responseMode = configuredMode && this.responseModeRegistry.has(configuredMode)
|
|
263
|
-
? configuredMode
|
|
264
|
-
: this.responseModeRegistry.getPreferredId() ?? 'single-session';
|
|
265
|
-
const rawModel = rawBaseagentConfig?.auxiliaryModel;
|
|
266
|
-
const rawEffort = rawBaseagentConfig?.auxiliaryEffort;
|
|
267
|
-
const model = typeof rawModel === 'string' && rawModel.trim()
|
|
268
|
-
? rawModel.trim()
|
|
269
|
-
: undefined;
|
|
270
|
-
const effort = typeof rawEffort === 'string' && AUXILIARY_EFFORTS.has(rawEffort)
|
|
271
|
-
? rawEffort
|
|
272
|
-
: undefined;
|
|
273
|
-
return {
|
|
274
|
-
responseMode,
|
|
275
|
-
model,
|
|
276
|
-
effort,
|
|
277
|
-
invalidEffort: rawEffort !== undefined && effort === undefined,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
310
|
readConversationEntries(session) {
|
|
281
311
|
const entries = readAllJsonlLines(messageLogPath(this.sessionManager.getChatDir(session)));
|
|
282
312
|
return entries
|
|
283
313
|
.filter(entry => isSessionRenewConversationEntry(entry, session.id))
|
|
284
314
|
.sort((left, right) => left.ts - right.ts);
|
|
285
315
|
}
|
|
286
|
-
async judgeWithModel(request, entries, idleMs, provider,
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
content:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
'You are a session continuity classifier.',
|
|
302
|
-
'Decide whether the new message continues the old conversation or starts an independent conversation.',
|
|
303
|
-
'Use NEW only when the new intent is clearly independent. When uncertain, choose CONTINUE.',
|
|
304
|
-
'Do not call tools. Do not explain your reasoning.',
|
|
305
|
-
'Return exactly one JSON object: {"decision":"continue|new","confidence":0..1,"reason_code":"short_code"}.',
|
|
306
|
-
].join('\n');
|
|
316
|
+
async judgeWithModel(request, entries, idleMs, provider, model) {
|
|
317
|
+
const prompt = loadSessionRenewPrompt();
|
|
318
|
+
const state = {
|
|
319
|
+
content: buildSessionRenewStateContent(request, entries, idleMs),
|
|
320
|
+
};
|
|
321
|
+
const questions = {
|
|
322
|
+
new_session: {
|
|
323
|
+
type: 'noul',
|
|
324
|
+
instructions: prompt.questionInstructions,
|
|
325
|
+
criteria: {
|
|
326
|
+
true: prompt.trueCriteria,
|
|
327
|
+
false: prompt.falseCriteria,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
};
|
|
307
331
|
const controller = new AbortController();
|
|
308
332
|
let timer;
|
|
309
333
|
try {
|
|
310
334
|
const operation = (async () => {
|
|
311
|
-
const text = await provider.
|
|
312
|
-
model
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
input: prompt,
|
|
335
|
+
const text = await provider.completeDecision({
|
|
336
|
+
model,
|
|
337
|
+
state,
|
|
338
|
+
questions,
|
|
316
339
|
signal: controller.signal,
|
|
317
340
|
});
|
|
318
|
-
return
|
|
341
|
+
return text;
|
|
319
342
|
})();
|
|
320
343
|
const timeout = new Promise((_resolve, reject) => {
|
|
321
344
|
timer = setTimeout(() => {
|
|
@@ -324,7 +347,7 @@ export class SessionRenewService {
|
|
|
324
347
|
}, MODEL_TIMEOUT_MS);
|
|
325
348
|
});
|
|
326
349
|
const result = await Promise.race([operation, timeout]);
|
|
327
|
-
return
|
|
350
|
+
return parseModelResult(result);
|
|
328
351
|
}
|
|
329
352
|
finally {
|
|
330
353
|
if (timer)
|
|
@@ -367,29 +390,68 @@ function sessionRenewFallbackReason(error) {
|
|
|
367
390
|
return 'timeout';
|
|
368
391
|
if (/HTTP \d+/i.test(message))
|
|
369
392
|
return 'http_error';
|
|
370
|
-
if (/no JSON|invalid
|
|
393
|
+
if (/no JSON|invalid noul|invalid response/i.test(message))
|
|
371
394
|
return 'invalid_response';
|
|
372
|
-
if (/
|
|
395
|
+
if (/empty/i.test(message))
|
|
373
396
|
return 'empty_response';
|
|
374
397
|
return 'inference_error';
|
|
375
398
|
}
|
|
376
399
|
function validPositiveNumber(value) {
|
|
377
400
|
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
378
401
|
}
|
|
402
|
+
function validProbability(value) {
|
|
403
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1 ? value : undefined;
|
|
404
|
+
}
|
|
405
|
+
function buildSessionRenewStateContent(request, entries, idleMs) {
|
|
406
|
+
const title = request.session.name?.trim() || '无标题';
|
|
407
|
+
const header = [
|
|
408
|
+
`话题标题:${title}`,
|
|
409
|
+
`当前聊天类型:${request.chatType}`,
|
|
410
|
+
`空闲时长:${Number((idleMs / 3_600_000).toFixed(2))} 小时`,
|
|
411
|
+
`是否回复旧消息:${request.replyToMessageId ? `是,消息 ID=${request.replyToMessageId}` : '否'}`,
|
|
412
|
+
loadSessionRenewPrompt().stateRules,
|
|
413
|
+
`当前新消息:${request.content}`,
|
|
414
|
+
'',
|
|
415
|
+
'历史会话上下文:',
|
|
416
|
+
].join('\n');
|
|
417
|
+
const selected = selectSessionRenewContext(entries);
|
|
418
|
+
const historyHeader = `${header}\n`;
|
|
419
|
+
if (historyHeader.length >= MAX_CONTEXT_CHARS)
|
|
420
|
+
return historyHeader.slice(0, MAX_CONTEXT_CHARS);
|
|
421
|
+
const blocks = [];
|
|
422
|
+
for (const [index, entry] of selected.entries()) {
|
|
423
|
+
const block = [
|
|
424
|
+
`历史消息 ${index + 1}(${entry.dir},${entry.msgType},${new Date(entry.ts).toISOString()},消息 ID=${entry.msgId || '无'}):`,
|
|
425
|
+
entry.content,
|
|
426
|
+
].join('\n');
|
|
427
|
+
const candidate = blocks.length > 0
|
|
428
|
+
? `${blocks.join('\n\n')}\n\n${block}`
|
|
429
|
+
: block;
|
|
430
|
+
if (historyHeader.length + candidate.length <= MAX_CONTEXT_CHARS)
|
|
431
|
+
blocks.push(block);
|
|
432
|
+
}
|
|
433
|
+
return `${historyHeader}${blocks.join('\n\n')}`.slice(0, MAX_CONTEXT_CHARS);
|
|
434
|
+
}
|
|
379
435
|
function parseModelResult(text) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const parsed = JSON.parse(match[0]);
|
|
384
|
-
if (parsed.decision !== 'continue' && parsed.decision !== 'new') {
|
|
385
|
-
throw new Error('judge returned invalid decision');
|
|
436
|
+
let parsed;
|
|
437
|
+
try {
|
|
438
|
+
parsed = JSON.parse(text);
|
|
386
439
|
}
|
|
387
|
-
|
|
388
|
-
throw new Error('judge returned invalid
|
|
440
|
+
catch {
|
|
441
|
+
throw new Error('judge returned invalid response JSON');
|
|
389
442
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
443
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
444
|
+
throw new Error('judge returned invalid response');
|
|
445
|
+
}
|
|
446
|
+
const answer = parsed.answers;
|
|
447
|
+
const newSession = answer && typeof answer === 'object' && !Array.isArray(answer)
|
|
448
|
+
? answer.new_session
|
|
449
|
+
: undefined;
|
|
450
|
+
const noul = newSession && typeof newSession === 'object' && !Array.isArray(newSession)
|
|
451
|
+
? newSession.noul
|
|
452
|
+
: undefined;
|
|
453
|
+
const newProbability = validProbability(noul);
|
|
454
|
+
if (newProbability === undefined)
|
|
455
|
+
throw new Error('judge returned invalid noul');
|
|
456
|
+
return { newProbability };
|
|
395
457
|
}
|