@gt-fe/eap-runtime 0.0.28 → 0.0.30

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.
@@ -36,7 +36,7 @@ import { pickContextFields } from '../../token/context-window.js';
36
36
  import { isHitlDecision, lastAssistantHasUnansweredToolCalls } from '../hitl/hitl-handler.js';
37
37
  import { classifySkillRead } from './skill-read.js';
38
38
  import { skillEntryInvocationFromTool } from './skill-entry-span.js';
39
- import { withGraphSpan } from '../trace-span.js';
39
+ import { llmSpanInput, llmSpanOutputFromResult, withGraphSpan } from '../trace-span.js';
40
40
  import { executionTraceFromState, getExecutionStream, getExecutionTrace, runWithExecutionTrace, } from '../execution-trace-context.js';
41
41
  import { isRetryableToolFailure, resolveToolRetryConfig, sleepToolRetry, toolRetryDelayMs, } from '../../tool/tool-retry.js';
42
42
  /**
@@ -788,17 +788,30 @@ export class LangGraphAdapter {
788
788
  case 'tool':
789
789
  return this.createToolNodeFunction(node);
790
790
  case 'condition':
791
- return async (state) => withGraphSpan(state, { name: node.id, type: 'condition' }, async () => {
791
+ return async (state) => {
792
792
  const conditionExpr = node.config?.['conditionExpr']
793
793
  ?? node.config?.['conditionFn'];
794
- if (conditionExpr) {
795
- const branchValues = node.config?.['branchValues']
796
- ?? this.extractBranchValuesFromDsl(node.id);
797
- const result = await this.conditionEvaluator.evaluate(conditionExpr, state, branchValues, node.config?.['fallback'] ?? '__default__');
798
- return { currentNode: node.id, __conditionResult: result };
799
- }
800
- return { currentNode: node.id };
801
- });
794
+ const branchValues = node.config?.['branchValues']
795
+ ?? this.extractBranchValuesFromDsl(node.id);
796
+ const fallback = node.config?.['fallback'] ?? '__default__';
797
+ return withGraphSpan(state, {
798
+ name: node.id,
799
+ type: 'condition',
800
+ extra: {
801
+ input: {
802
+ conditionExpr: conditionExpr ?? null,
803
+ branchValues,
804
+ fallback,
805
+ },
806
+ },
807
+ }, async () => {
808
+ if (conditionExpr) {
809
+ const result = await this.conditionEvaluator.evaluate(conditionExpr, state, branchValues, fallback);
810
+ return { currentNode: node.id, __conditionResult: result };
811
+ }
812
+ return { currentNode: node.id };
813
+ }, { outputFrom: (result) => result['__conditionResult'] });
814
+ };
802
815
  case 'subgraph':
803
816
  return this.createSubGraphNodeFunction(node);
804
817
  case 'human':
@@ -1019,136 +1032,155 @@ export class LangGraphAdapter {
1019
1032
  lastAssistantHasUnansweredToolCalls(rawMessages)) {
1020
1033
  return { currentNode: node.id };
1021
1034
  }
1022
- return withGraphSpan(state, { name: node.id, type: 'llm' }, async () => {
1023
- const promptRoots = Array.isArray(state['promptRoots']) ? state['promptRoots'] : [];
1024
- const resolvedPrompt = resolveGraphPrompt(baseConfig.systemPrompt, promptRoots);
1025
- const loadedToolsBase = state['loadedTools'];
1026
- const agentCode = typeof state['agentCode'] === 'string' ? state['agentCode'] : undefined;
1027
- const skillDefs = state['skillDefinitions'];
1028
- const execCtx = state['executionContext'];
1029
- const userId = (typeof execCtx?.userId === 'string' && execCtx.userId) || state['userId'];
1030
- const loadedTools = disableTools
1031
- ? loadedToolsBase
1032
- : await hydrateMcpTools(loadedToolsBase, this.toolExecutor, {
1033
- authContext: {
1034
- userId: String(userId ?? ''),
1035
- delegationToken: String(execCtx?.delegationToken ?? state['delegationToken'] ?? ''),
1036
- agentCode: String(agentCode ?? ''),
1037
- },
1038
- correlationId: typeof state['correlationId'] === 'string' ? state['correlationId'] : undefined,
1039
- taskId: typeof state['taskId'] === 'string' ? state['taskId'] : undefined,
1040
- onListFailure: ({ toolset, error }) => {
1041
- this.emitToolExecutionEvents({
1042
- toolCallId: `mcp-list:${toolset}`,
1043
- toolName: MCP_LIST_TOOL_NAME,
1044
- toolset,
1045
- status: 'error',
1046
- error,
1047
- });
1048
- },
1049
- });
1050
- if (loadedTools && loadedTools !== loadedToolsBase) {
1051
- state['loadedTools'] = loadedTools;
1052
- }
1053
- // [DEBUG] 打印 loadedTools 实际状态
1054
- if (process.env.EAP_LLM_DEBUG === '1') {
1055
- const toolNames = loadedTools ? Object.keys(loadedTools) : [];
1056
- console.log(`[LLM-DEBUG] node=${node.id} loadedTools=${toolNames.length}:`, toolNames);
1057
- }
1058
- const catalog = includeSkillCatalog ? this.buildSkillCatalog(skillDefs) : undefined;
1059
- const toolCatalog = disableTools ? undefined : buildToolCatalog(loadedTools);
1060
- const jail = resolveJailFromState({
1061
- agentDir: state['agentDir'],
1062
- userId,
1063
- executionContext: execCtx,
1064
- loadedTools,
1065
- skillDefinitions: skillDefs,
1066
- });
1067
- if (this.overlayExtraRoots.length > 0) {
1068
- jail.extraRoots = [...(jail.extraRoots ?? []), ...this.overlayExtraRoots];
1069
- }
1070
- const workingDirectory = jail.workdir;
1071
- const jailRoots = resolveJailRoots(jail);
1072
- const skillDirs = Object.values(skillDefs ?? {})
1073
- .filter((skill) => Boolean(skill.bundlePath))
1074
- .map((skill) => ({
1075
- skillId: skill.skillId,
1076
- bundlePath: skill.bundlePath,
1077
- entryFile: skill.entryFile,
1078
- }));
1079
- const promptInputs = resolveLlmPromptInputs({
1080
- silent,
1081
- agentSystemPrompt: state['agentSystemPrompt'],
1082
- nodePrompt: resolvedPrompt,
1035
+ const promptRoots = Array.isArray(state['promptRoots']) ? state['promptRoots'] : [];
1036
+ const resolvedPrompt = resolveGraphPrompt(baseConfig.systemPrompt, promptRoots);
1037
+ const loadedToolsBase = state['loadedTools'];
1038
+ const agentCode = typeof state['agentCode'] === 'string' ? state['agentCode'] : undefined;
1039
+ const skillDefs = state['skillDefinitions'];
1040
+ const execCtx = state['executionContext'];
1041
+ const userId = (typeof execCtx?.userId === 'string' && execCtx.userId) || state['userId'];
1042
+ const loadedTools = disableTools
1043
+ ? loadedToolsBase
1044
+ : await hydrateMcpTools(loadedToolsBase, this.toolExecutor, {
1045
+ authContext: {
1046
+ userId: String(userId ?? ''),
1047
+ delegationToken: String(execCtx?.delegationToken ?? state['delegationToken'] ?? ''),
1048
+ agentCode: String(agentCode ?? ''),
1049
+ },
1050
+ correlationId: typeof state['correlationId'] === 'string' ? state['correlationId'] : undefined,
1051
+ taskId: typeof state['taskId'] === 'string' ? state['taskId'] : undefined,
1052
+ onListFailure: ({ toolset, error }) => {
1053
+ this.emitToolExecutionEvents({
1054
+ toolCallId: `mcp-list:${toolset}`,
1055
+ toolName: MCP_LIST_TOOL_NAME,
1056
+ toolset,
1057
+ status: 'error',
1058
+ error,
1059
+ });
1060
+ },
1083
1061
  });
1084
- const assembled = this.systemPromptBuilder.build({
1085
- agentPrompt: promptInputs.agentPrompt,
1086
- nodePrompt: promptInputs.nodePrompt,
1087
- skillCatalog: catalog,
1088
- toolCatalog,
1062
+ if (loadedTools && loadedTools !== loadedToolsBase) {
1063
+ state['loadedTools'] = loadedTools;
1064
+ }
1065
+ // [DEBUG] 打印 loadedTools 实际状态
1066
+ if (process.env.EAP_LLM_DEBUG === '1') {
1067
+ const toolNames = loadedTools ? Object.keys(loadedTools) : [];
1068
+ console.log(`[LLM-DEBUG] node=${node.id} loadedTools=${toolNames.length}:`, toolNames);
1069
+ }
1070
+ const catalog = includeSkillCatalog ? this.buildSkillCatalog(skillDefs) : undefined;
1071
+ const toolCatalog = disableTools ? undefined : buildToolCatalog(loadedTools);
1072
+ const jail = resolveJailFromState({
1073
+ agentDir: state['agentDir'],
1074
+ userId,
1075
+ executionContext: execCtx,
1076
+ loadedTools,
1077
+ skillDefinitions: skillDefs,
1078
+ });
1079
+ if (this.overlayExtraRoots.length > 0) {
1080
+ jail.extraRoots = [...(jail.extraRoots ?? []), ...this.overlayExtraRoots];
1081
+ }
1082
+ const workingDirectory = jail.workdir;
1083
+ const jailRoots = resolveJailRoots(jail);
1084
+ const skillDirs = Object.values(skillDefs ?? {})
1085
+ .filter((skill) => Boolean(skill.bundlePath))
1086
+ .map((skill) => ({
1087
+ skillId: skill.skillId,
1088
+ bundlePath: skill.bundlePath,
1089
+ entryFile: skill.entryFile,
1090
+ }));
1091
+ const promptInputs = resolveLlmPromptInputs({
1092
+ silent,
1093
+ agentSystemPrompt: state['agentSystemPrompt'],
1094
+ nodePrompt: resolvedPrompt,
1095
+ });
1096
+ const assembled = this.systemPromptBuilder.build({
1097
+ agentPrompt: promptInputs.agentPrompt,
1098
+ nodePrompt: promptInputs.nodePrompt,
1099
+ skillCatalog: catalog,
1100
+ toolCatalog,
1101
+ agentCode,
1102
+ userId,
1103
+ workingDirectory,
1104
+ os: { platform: os.platform(), release: os.release() },
1105
+ jailRoots,
1106
+ skillDirs,
1107
+ });
1108
+ const apiTools = disableTools ? [] : loadedToolsToApiTools(loadedTools);
1109
+ const finalConfig = {
1110
+ ...baseConfig,
1111
+ systemPrompt: assembled.text,
1112
+ systemSections: assembled.sections,
1113
+ turn: {
1114
+ sessionId: typeof state['sessionId'] === 'string' ? state['sessionId'] : undefined,
1115
+ taskId: typeof state['taskId'] === 'string' ? state['taskId'] : undefined,
1116
+ nodeId: node.id,
1089
1117
  agentCode,
1090
- userId,
1091
- workingDirectory,
1092
- os: { platform: os.platform(), release: os.release() },
1093
- jailRoots,
1094
- skillDirs,
1095
- });
1096
- const apiTools = disableTools ? [] : loadedToolsToApiTools(loadedTools);
1097
- const finalConfig = {
1098
- ...baseConfig,
1099
- systemPrompt: assembled.text,
1100
- systemSections: assembled.sections,
1101
- turn: {
1102
- sessionId: typeof state['sessionId'] === 'string' ? state['sessionId'] : undefined,
1103
- taskId: typeof state['taskId'] === 'string' ? state['taskId'] : undefined,
1104
- nodeId: node.id,
1105
- agentCode,
1106
- correlationId: typeof state['correlationId'] === 'string' ? state['correlationId'] : undefined,
1107
- },
1108
- tools: disableTools ? undefined : (apiTools.length > 0 ? apiTools : baseConfig.tools),
1109
- toolChoice: disableTools ? 'none' : baseConfig.toolChoice,
1110
- };
1111
- // 流式旁路:有本次 stream 袋时启用 LLM token 推送;silent 节点不推给用户
1112
- const stream = getExecutionStream();
1113
- const enableStream = Boolean(stream) && !silent;
1114
- if (stream) {
1115
- stream.reasoningStartedThisTurn = false;
1116
- stream.streamedTextThisTurn = false;
1117
- }
1118
- const llmNode = new LLMNode(node.id, finalConfig, this.llmProvider, enableStream, enableStream
1119
- ? (text) => {
1120
- const live = getExecutionStream();
1121
- if (!live || !text)
1122
- return;
1123
- const timestamp = new Date().toISOString();
1124
- if (!live.reasoningStartedThisTurn) {
1125
- live.reasoningStartedThisTurn = true;
1126
- live.iteration += 1;
1127
- live.sink({
1128
- type: 'reasoning.started',
1129
- taskId: undefined,
1130
- correlationId: undefined,
1131
- timestamp,
1132
- payload: {
1133
- iterationCount: live.iteration,
1134
- model: finalConfig.model,
1135
- },
1136
- });
1137
- }
1138
- live.streamedTextThisTurn = true;
1118
+ correlationId: typeof state['correlationId'] === 'string' ? state['correlationId'] : undefined,
1119
+ },
1120
+ tools: disableTools ? undefined : (apiTools.length > 0 ? apiTools : baseConfig.tools),
1121
+ toolChoice: disableTools ? 'none' : baseConfig.toolChoice,
1122
+ };
1123
+ // 流式旁路:有本次 stream 袋时启用 LLM token 推送;silent 节点不推给用户
1124
+ const stream = getExecutionStream();
1125
+ const enableStream = Boolean(stream) && !silent;
1126
+ if (stream) {
1127
+ stream.reasoningStartedThisTurn = false;
1128
+ stream.streamedTextThisTurn = false;
1129
+ }
1130
+ const llmNode = new LLMNode(node.id, finalConfig, this.llmProvider, enableStream, enableStream
1131
+ ? (text) => {
1132
+ const live = getExecutionStream();
1133
+ if (!live || !text)
1134
+ return;
1135
+ const timestamp = new Date().toISOString();
1136
+ if (!live.reasoningStartedThisTurn) {
1137
+ live.reasoningStartedThisTurn = true;
1138
+ live.iteration += 1;
1139
1139
  live.sink({
1140
- type: 'response.chunk',
1140
+ type: 'reasoning.started',
1141
1141
  taskId: undefined,
1142
1142
  correlationId: undefined,
1143
1143
  timestamp,
1144
1144
  payload: {
1145
- content: text,
1146
- index: live.chunkIndex++,
1147
- node: node.id,
1145
+ iterationCount: live.iteration,
1146
+ model: finalConfig.model,
1148
1147
  },
1149
1148
  });
1150
1149
  }
1151
- : undefined, this.interceptors);
1150
+ live.streamedTextThisTurn = true;
1151
+ live.sink({
1152
+ type: 'response.chunk',
1153
+ taskId: undefined,
1154
+ correlationId: undefined,
1155
+ timestamp,
1156
+ payload: {
1157
+ content: text,
1158
+ index: live.chunkIndex++,
1159
+ node: node.id,
1160
+ },
1161
+ });
1162
+ }
1163
+ : undefined, this.interceptors);
1164
+ const toolsForSpan = disableTools || !loadedTools
1165
+ ? undefined
1166
+ : Object.values(loadedTools).map((t) => ({
1167
+ name: t.name,
1168
+ ...(t.manifest.description?.trim()
1169
+ ? { description: t.manifest.description.trim() }
1170
+ : {}),
1171
+ }));
1172
+ // 组装后再 createNode:input 必须是发给模型的 system/tools/messages;completeNode 不能补 input。
1173
+ return withGraphSpan(state, {
1174
+ name: node.id,
1175
+ type: 'llm',
1176
+ extra: {
1177
+ input: llmSpanInput({
1178
+ system: assembled.text,
1179
+ tools: toolsForSpan,
1180
+ messages: state['messages'],
1181
+ }),
1182
+ },
1183
+ }, async () => {
1152
1184
  const next = await llmNode.execute(state);
1153
1185
  const hydratedUpdate = loadedTools && loadedTools !== loadedToolsBase ? { loadedTools } : {};
1154
1186
  if (!silent)
@@ -1163,7 +1195,7 @@ export class LangGraphAdapter {
1163
1195
  silentOutput: raw,
1164
1196
  ...hydratedUpdate,
1165
1197
  };
1166
- });
1198
+ }, { outputFrom: llmSpanOutputFromResult });
1167
1199
  };
1168
1200
  }
1169
1201
  /**
@@ -289,6 +289,12 @@ export class HitlHandler {
289
289
  name: extras.toolName ?? 'hitl',
290
290
  type: 'hitl',
291
291
  parentSpanId: extras.toolSpanId ?? skillSpanId,
292
+ extra: {
293
+ input: {
294
+ question: config.question,
295
+ options: config.options,
296
+ },
297
+ },
292
298
  });
293
299
  const persisted = {
294
300
  taskId,
@@ -12,4 +12,18 @@ export declare function completeGraphSpan(state: Record<string, unknown>, spanId
12
12
  export declare function withGraphSpan(state: Record<string, unknown>, spec: {
13
13
  name: string;
14
14
  type: ExecutionNodeEvent['type'];
15
- }, run: () => Promise<Record<string, unknown>>): Promise<Record<string, unknown>>;
15
+ extra?: Partial<Omit<ExecutionNodeEvent, 'traceId' | 'name' | 'type' | 'parentNodeId'>>;
16
+ }, run: () => Promise<Record<string, unknown>>, complete?: {
17
+ outputFrom?: (result: Record<string, unknown>) => unknown;
18
+ }): Promise<Record<string, unknown>>;
19
+ /** llm Span create 时的 input:组装后的 system、工具短目录、本回合 messages。 */
20
+ export declare function llmSpanInput(input: {
21
+ system?: string;
22
+ tools?: Array<{
23
+ name: string;
24
+ description?: string;
25
+ }>;
26
+ messages: unknown;
27
+ }): Record<string, unknown>;
28
+ /** llm Span complete 的 output:本回合新增 assistant(含 toolCalls)或 silentOutput。 */
29
+ export declare function llmSpanOutputFromResult(result: Record<string, unknown>): unknown;
@@ -43,15 +43,17 @@ export async function completeGraphSpan(state, spanId, params, collectorOverride
43
43
  warnTraceSpanFailure('completeNode', { spanId, traceId: resolveExecutionTrace(state).traceId }, err);
44
44
  }
45
45
  }
46
- export async function withGraphSpan(state, spec, run) {
46
+ export async function withGraphSpan(state, spec, run, complete) {
47
47
  const spanId = await startGraphSpan(state, spec);
48
48
  try {
49
49
  const result = await run();
50
50
  const usage = Array.isArray(result['__llmTokenUsage'])
51
51
  ? result['__llmTokenUsage'].at(-1)
52
52
  : undefined;
53
+ const output = complete?.outputFrom?.(result);
53
54
  await completeGraphSpan(state, spanId, {
54
55
  status: 'completed',
56
+ ...(output !== undefined ? { output } : {}),
55
57
  inputTokens: usage?.inputTokens,
56
58
  outputTokens: usage?.outputTokens,
57
59
  totalTokens: usage?.inputTokens != null || usage?.outputTokens != null
@@ -69,3 +71,69 @@ export async function withGraphSpan(state, spec, run) {
69
71
  throw err;
70
72
  }
71
73
  }
74
+ function spanGraphMessages(raw) {
75
+ if (!Array.isArray(raw))
76
+ return [];
77
+ return raw.map((msg) => {
78
+ if (typeof msg !== 'object' || msg === null) {
79
+ return { role: 'user', content: String(msg) };
80
+ }
81
+ const m = msg;
82
+ const out = {
83
+ role: typeof m['role'] === 'string' ? m['role'] : 'user',
84
+ content: typeof m['content'] === 'string' ? m['content'] : '',
85
+ };
86
+ const toolCalls = m['toolCalls'] ?? m['tool_calls'];
87
+ if (toolCalls !== undefined)
88
+ out.toolCalls = toolCalls;
89
+ const toolCallId = typeof m['toolCallId'] === 'string'
90
+ ? m['toolCallId']
91
+ : typeof m['tool_call_id'] === 'string'
92
+ ? m['tool_call_id']
93
+ : undefined;
94
+ if (toolCallId)
95
+ out.toolCallId = toolCallId;
96
+ if (typeof m['toolName'] === 'string')
97
+ out.toolName = m['toolName'];
98
+ if (typeof m['success'] === 'boolean')
99
+ out.success = m['success'];
100
+ const reasoning = typeof m['reasoningContent'] === 'string'
101
+ ? m['reasoningContent']
102
+ : typeof m['reasoning_content'] === 'string'
103
+ ? m['reasoning_content']
104
+ : undefined;
105
+ if (reasoning)
106
+ out.reasoningContent = reasoning;
107
+ return out;
108
+ });
109
+ }
110
+ /** llm Span create 时的 input:组装后的 system、工具短目录、本回合 messages。 */
111
+ export function llmSpanInput(input) {
112
+ const tools = input.tools?.filter((t) => t.name);
113
+ return {
114
+ ...(input.system ? { system: input.system } : {}),
115
+ ...(tools && tools.length > 0 ? { tools } : {}),
116
+ messages: spanGraphMessages(input.messages),
117
+ };
118
+ }
119
+ /** llm Span complete 的 output:本回合新增 assistant(含 toolCalls)或 silentOutput。 */
120
+ export function llmSpanOutputFromResult(result) {
121
+ if (typeof result['silentOutput'] === 'string') {
122
+ return { content: result['silentOutput'] };
123
+ }
124
+ const messages = Array.isArray(result['messages']) ? result['messages'] : [];
125
+ const added = messages[0];
126
+ if (!added || typeof added !== 'object')
127
+ return undefined;
128
+ const m = added;
129
+ const out = {
130
+ content: typeof m['content'] === 'string' ? m['content'] : '',
131
+ };
132
+ const toolCalls = m['toolCalls'] ?? m['tool_calls'];
133
+ if (Array.isArray(toolCalls) && toolCalls.length > 0)
134
+ out.toolCalls = toolCalls;
135
+ if (typeof m['reasoningContent'] === 'string' && m['reasoningContent']) {
136
+ out.reasoningContent = m['reasoningContent'];
137
+ }
138
+ return out;
139
+ }
@@ -18,9 +18,10 @@ import type { MessageService } from '../session/message.service.js';
18
18
  import type { SessionService, SessionRecord, MessageRecord } from '../session/session.service.js';
19
19
  import { type ILlmTurnLog, type LlmTurnIndexEntry, type LlmTurnLogKind, type LlmTurnRecord } from '../debug/llm-turn-log.js';
20
20
  import { type SessionCompressionOptions } from '../session/apply-session-compression.js';
21
+ import type { ILLMProvider } from '../graph/nodes/llm-node.js';
21
22
  import type { SSEEvent } from '../sse/contracts/sse-emitter.contract.js';
22
23
  import type { TaskService } from '../task/task.service.js';
23
- import type { ExecuteParams, ExecuteResult, ForkParams, ForkOverrides, ForkResult, HitlResponse, ResumeHitlResult, SeedSessionMessagesParams, SeedSessionMessagesResult } from './types.js';
24
+ import type { ExecuteParams, ExecuteResult, ForkParams, ForkOverrides, ForkResult, HitlResponse, ResumeHitlResult, SeedSessionMessagesParams, SeedSessionMessagesResult, GenerateSessionTitleParams, GenerateSessionTitleResult } from './types.js';
24
25
  export interface ForkRelationRecord {
25
26
  originalTraceId: string;
26
27
  forkedTraceId: string;
@@ -50,6 +51,11 @@ export interface AgentRuntimeDeps {
50
51
  recordForkRelation?: RecordForkRelation;
51
52
  /** 历史压缩;默认开。EAP_RUNTIME_COMPRESSION=0 关闭 */
52
53
  sessionCompression?: SessionCompressionOptions;
54
+ /**
55
+ * 会话标题 completion。与图引擎共用同一实例。
56
+ * 未注入时回退 `sessionCompression.provider`。
57
+ */
58
+ llmProvider?: ILLMProvider;
53
59
  /** 长期记忆注入;默认关。见 EAP_RUNTIME_MEMORY */
54
60
  sessionMemory?: import('../session/apply-session-memory.js').SessionMemoryOptions;
55
61
  /** 未注入时不启动整图超时(测试桩) */
@@ -114,6 +120,11 @@ export declare class AgentRuntime {
114
120
  listConversations(options?: {
115
121
  userId?: string;
116
122
  }): Promise<SessionRecord[]>;
123
+ /**
124
+ * 根据该 sessionId 的 Message 生成会话标题。不写库、不进 execute。
125
+ * 失败返回 `{ title: null }`。
126
+ */
127
+ generateSessionTitle(params: GenerateSessionTitleParams): Promise<GenerateSessionTitleResult>;
117
128
  private resolveTurnLog;
118
129
  /** 同步执行 Graph(经 InvokeOrchestrator,内含 loadedTools 注入) */
119
130
  execute(params: ExecuteParams): Promise<ExecuteResult>;
@@ -18,6 +18,8 @@ import { createFileLlmTurnLogFromEnv, } from '../debug/llm-turn-log.js';
18
18
  import { buildHitlWaitingTaskOutput, buildTurnTranscriptFromStreamEvents, loadModelFacingHistory, turnGraphMessagesAfterCompression, extractHitlThreadId, extractStreamReasoning, applyStreamTextDelta, createStreamAssistantBuckets, visibleStreamAssistantText, streamAssistantTexts, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, resolveCheckpointThreadId, appendTerminalToolResult, } from '../session/persist-turn-messages.js';
19
19
  import { iterSessionCompression, withCompressionSummaryModel, } from '../session/apply-session-compression.js';
20
20
  import { applySessionMemory } from '../session/apply-session-memory.js';
21
+ import { buildSessionTitleUserPayload, invokeSessionTitle, } from '../session/session-title.js';
22
+ import { EchoLLMProvider } from '../providers/echo-llm-provider.js';
21
23
  import { resolveTokenDetail, shouldEmitTokenSse } from '../token/token-detail.js';
22
24
  import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
23
25
  import { resolveExecutionContext } from '../governance/governance-client.js';
@@ -192,6 +194,39 @@ export class AgentRuntime {
192
194
  }
193
195
  return [];
194
196
  }
197
+ /**
198
+ * 根据该 sessionId 的 Message 生成会话标题。不写库、不进 execute。
199
+ * 失败返回 `{ title: null }`。
200
+ */
201
+ async generateSessionTitle(params) {
202
+ try {
203
+ const provider = this.deps.llmProvider ?? this.deps.sessionCompression?.provider;
204
+ if (!provider || provider instanceof EchoLLMProvider) {
205
+ return { title: null };
206
+ }
207
+ if (!this.deps.messageService) {
208
+ return { title: null };
209
+ }
210
+ const messages = await this.deps.messageService.getMessages(params.sessionId);
211
+ const payload = buildSessionTitleUserPayload(messages);
212
+ if (!payload) {
213
+ return { title: null };
214
+ }
215
+ let model = params.model?.trim() ?? '';
216
+ if (!model && params.agentCode && this.deps.graphLoader) {
217
+ const resolved = await this.deps.graphLoader.resolveDefaultModel(params.agentCode, params.agentVersion);
218
+ model = resolved?.trim() ?? '';
219
+ }
220
+ const title = await invokeSessionTitle(provider, payload, {
221
+ model,
222
+ sessionId: params.sessionId,
223
+ });
224
+ return { title };
225
+ }
226
+ catch {
227
+ return { title: null };
228
+ }
229
+ }
195
230
  resolveTurnLog() {
196
231
  return this.deps.turnLog ?? createFileLlmTurnLogFromEnv();
197
232
  }
@@ -235,6 +235,7 @@ export function createDevRuntime(options) {
235
235
  sessionCompression,
236
236
  sessionMemory,
237
237
  timeoutManager,
238
+ llmProvider,
238
239
  });
239
240
  return {
240
241
  invokeOrchestrator,
@@ -1,4 +1,4 @@
1
1
  export { AgentRuntime, createAgentRuntime, type AgentRuntimeDeps, type ForkRelationRecord, type RecordForkRelation, type ConversationDetail, } from './agent-runtime.js';
2
- export type { ExecuteParams, ExecuteResult, ForkParams, ForkOverrides, ForkResult, HitlResponse, ResumeHitlResult, SSEEvent, } from './types.js';
2
+ export type { ExecuteParams, ExecuteResult, ForkParams, ForkOverrides, ForkResult, HitlResponse, ResumeHitlResult, SSEEvent, GenerateSessionTitleParams, GenerateSessionTitleResult, } from './types.js';
3
3
  export { createDevRuntime, type DevRuntime, type CreateDevRuntimeOptions, } from './dev-runtime.js';
4
4
  export { assembleRuntimeStorage, resolveRuntimeStorageProfile, resolveDevStorageDirs, type RuntimeStorageProfile, type AssembleRuntimeStorageOptions, type AssembledRuntimeStorage, } from './storage-profile.js';
@@ -38,6 +38,19 @@ export interface ExecuteParams {
38
38
  }
39
39
  /** 同步执行结果 */
40
40
  export type ExecuteResult = InvokeResponseData;
41
+ /** 会话标题生成入参。不启动图执行。 */
42
+ export interface GenerateSessionTitleParams {
43
+ sessionId: string;
44
+ /** 覆盖标题模型;未设则用 Agent `models.default` */
45
+ model?: string;
46
+ /** 用于解析 `models.default`;与 execute 的 agentCode 相同 */
47
+ agentCode?: string;
48
+ agentVersion?: string;
49
+ }
50
+ /** 已规范到 ≤50 字;失败为 null */
51
+ export interface GenerateSessionTitleResult {
52
+ title: string | null;
53
+ }
41
54
  /** HITL 恢复结果(与 @gt-fe/eap-contracts ResumeHitlResult 对齐) */
42
55
  export type { ResumeHitlResult };
43
56
  /** HITL 用户响应(与 @gt-fe/eap-contracts ResumeHitlRequest 对齐;userId 必填) */
@@ -13,6 +13,7 @@ import { EchoLLMProvider } from '../providers/echo-llm-provider.js';
13
13
  import { COMPRESSION_WINDOW_FACTOR, countRounds, DEFAULT_COMPRESSION_CONFIG, } from '../memory/message-compression.service.js';
14
14
  import { SlidingWindowCompression } from '../memory/compression/sliding-window.compression.js';
15
15
  import { emitRuntimeLog } from '../common/logger.js';
16
+ import { SESSION_TITLE_TURN_NODE_ID } from './session-title.js';
16
17
  /** 摘要 LLM 写入 llm_turns 的 nodeId。lastTurnUsage 跳过,避免用摘要占用触发下一轮压缩。 */
17
18
  export const COMPRESSION_TURN_NODE_ID = 'session-compression';
18
19
  export function isRuntimeCompressionEnabled(explicit, env = process.env) {
@@ -256,8 +257,11 @@ async function lastTurnUsage(turnLog, sessionId) {
256
257
  const index = await turnLog.list(sessionId);
257
258
  for (let i = index.length - 1; i >= 0; i--) {
258
259
  const entry = index[i];
259
- if (!entry || entry.nodeId === COMPRESSION_TURN_NODE_ID)
260
+ if (!entry ||
261
+ entry.nodeId === COMPRESSION_TURN_NODE_ID ||
262
+ entry.nodeId === SESSION_TITLE_TURN_NODE_ID) {
260
263
  continue;
264
+ }
261
265
  if (typeof entry.used !== 'number')
262
266
  continue;
263
267
  return { used: entry.used, window: entry.window };
@@ -13,3 +13,4 @@ export { CompressionMiddleware, createCompressionMiddleware } from './compressio
13
13
  export { applySessionCompression, COMPRESSION_TURN_NODE_ID, withCompressionSummaryModel, runSessionCompression, decideSessionCompression, iterSessionCompression, formatCompressionStartedMessage, formatCompressionCompletedMessage, isRuntimeCompressionEnabled, isRuntimeCompressionPersist, type SessionCompressionOptions, type RunSessionCompressionParams, type RunSessionCompressionResult, type SessionCompressionDecision, type SessionCompressionStep, } from './apply-session-compression.js';
14
14
  export type { CompressionMiddlewareConfig } from './compression-middleware.js';
15
15
  export { applySessionMemory, isRuntimeMemoryEnabled, type SessionMemoryOptions, } from './apply-session-memory.js';
16
+ export { SESSION_TITLE_MAX_CHARS, SESSION_TITLE_ASSISTANT_CHAR_BUDGET, SESSION_TITLE_TURN_NODE_ID, SESSION_TITLE_USER_CHAR_BUDGET, provisionalSessionTitle, normalizeGeneratedTitle, fitUserTexts, buildSessionTitleUserPayload, invokeSessionTitle, } from './session-title.js';
@@ -12,3 +12,4 @@ export { SessionContextService } from './session-context.service.js';
12
12
  export { CompressionMiddleware, createCompressionMiddleware } from './compression-middleware.js';
13
13
  export { applySessionCompression, COMPRESSION_TURN_NODE_ID, withCompressionSummaryModel, runSessionCompression, decideSessionCompression, iterSessionCompression, formatCompressionStartedMessage, formatCompressionCompletedMessage, isRuntimeCompressionEnabled, isRuntimeCompressionPersist, } from './apply-session-compression.js';
14
14
  export { applySessionMemory, isRuntimeMemoryEnabled, } from './apply-session-memory.js';
15
+ export { SESSION_TITLE_MAX_CHARS, SESSION_TITLE_ASSISTANT_CHAR_BUDGET, SESSION_TITLE_TURN_NODE_ID, SESSION_TITLE_USER_CHAR_BUDGET, provisionalSessionTitle, normalizeGeneratedTitle, fitUserTexts, buildSessionTitleUserPayload, invokeSessionTitle, } from './session-title.js';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 会话标题:从 Message 生成短标题字符串。
3
+ * 不写库、不进 execute。Hosting 负责落 sessions.title。
4
+ *
5
+ * @see docs/architecture/session-title-generation.md
6
+ */
7
+ import type { GraphMessage } from '../graph/contracts/executable-graph.js';
8
+ import type { ILLMProvider } from '../graph/nodes/llm-node.js';
9
+ import type { MessageRecord } from './session.service.js';
10
+ /** 与 `@gt-fe/eap-contracts` `SESSION_TITLE_MAX_CHARS` 同值。 */
11
+ export declare const SESSION_TITLE_MAX_CHARS = 50;
12
+ export declare const SESSION_TITLE_USER_CHAR_BUDGET = 2000;
13
+ export declare const SESSION_TITLE_ASSISTANT_CHAR_BUDGET = 500;
14
+ /** 标题 LLM 写入 llm_turns 的 nodeId。压缩 lastTurnUsage 跳过。 */
15
+ export declare const SESSION_TITLE_TURN_NODE_ID = "session-title";
16
+ export declare function clipChars(text: string, max: number): string;
17
+ /** 临时标题:去换行后截到 50 字。 */
18
+ export declare function provisionalSessionTitle(userText: string): string;
19
+ export declare function normalizeGeneratedTitle(raw: string): string | null;
20
+ /** 超预算时丢掉最早的 user;只剩一条仍超则保留开头。 */
21
+ export declare function fitUserTexts(texts: string[], budget: number): string[];
22
+ export declare function buildSessionTitleUserPayload(messages: MessageRecord[]): string | null;
23
+ export declare function buildSessionTitlePromptMessages(payload: string): GraphMessage[];
24
+ export declare function invokeSessionTitle(provider: ILLMProvider, payload: string, options: {
25
+ model?: string;
26
+ sessionId: string;
27
+ }): Promise<string | null>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * 会话标题:从 Message 生成短标题字符串。
3
+ * 不写库、不进 execute。Hosting 负责落 sessions.title。
4
+ *
5
+ * @see docs/architecture/session-title-generation.md
6
+ */
7
+ import { EchoLLMProvider } from '../providers/echo-llm-provider.js';
8
+ /** 与 `@gt-fe/eap-contracts` `SESSION_TITLE_MAX_CHARS` 同值。 */
9
+ export const SESSION_TITLE_MAX_CHARS = 50;
10
+ export const SESSION_TITLE_USER_CHAR_BUDGET = 2000;
11
+ export const SESSION_TITLE_ASSISTANT_CHAR_BUDGET = 500;
12
+ /** 标题 LLM 写入 llm_turns 的 nodeId。压缩 lastTurnUsage 跳过。 */
13
+ export const SESSION_TITLE_TURN_NODE_ID = 'session-title';
14
+ const TITLE_SYSTEM_PROMPT = '根据对话写一个不超过50个字的会话标题。跟随用户主要语言。只输出标题,不要引号、不要解释。';
15
+ export function clipChars(text, max) {
16
+ const chars = Array.from(text);
17
+ return chars.length <= max ? text : chars.slice(0, max).join('');
18
+ }
19
+ /** 临时标题:去换行后截到 50 字。 */
20
+ export function provisionalSessionTitle(userText) {
21
+ const collapsed = userText.replace(/\s+/g, ' ').trim();
22
+ return clipChars(collapsed, SESSION_TITLE_MAX_CHARS);
23
+ }
24
+ export function normalizeGeneratedTitle(raw) {
25
+ const stripped = raw
26
+ .replace(/^[\s"'「」『』《》]+|[\s"'「」『』《》]+$/g, '')
27
+ .replace(/\s+/g, ' ')
28
+ .trim();
29
+ if (!stripped)
30
+ return null;
31
+ return clipChars(stripped, SESSION_TITLE_MAX_CHARS);
32
+ }
33
+ function charLength(text) {
34
+ return Array.from(text).length;
35
+ }
36
+ /** 超预算时丢掉最早的 user;只剩一条仍超则保留开头。 */
37
+ export function fitUserTexts(texts, budget) {
38
+ const out = texts.filter((t) => t.length > 0);
39
+ while (out.length > 1 && out.reduce((n, t) => n + charLength(t), 0) > budget) {
40
+ out.shift();
41
+ }
42
+ if (out.length === 1 && charLength(out[0]) > budget) {
43
+ out[0] = clipChars(out[0], budget);
44
+ }
45
+ return out;
46
+ }
47
+ export function buildSessionTitleUserPayload(messages) {
48
+ const users = messages
49
+ .filter((m) => m.role === 'user')
50
+ .map((m) => m.content.trim())
51
+ .filter(Boolean);
52
+ if (users.length === 0)
53
+ return null;
54
+ const fitted = fitUserTexts(users, SESSION_TITLE_USER_CHAR_BUDGET);
55
+ const parts = [...fitted];
56
+ const lastAssistant = [...messages]
57
+ .reverse()
58
+ .find((m) => m.role === 'assistant' && m.content.trim());
59
+ if (lastAssistant) {
60
+ parts.push(clipChars(lastAssistant.content.trim(), SESSION_TITLE_ASSISTANT_CHAR_BUDGET));
61
+ }
62
+ return parts.join('\n');
63
+ }
64
+ export function buildSessionTitlePromptMessages(payload) {
65
+ return [
66
+ { role: 'system', content: TITLE_SYSTEM_PROMPT },
67
+ { role: 'user', content: payload },
68
+ ];
69
+ }
70
+ export async function invokeSessionTitle(provider, payload, options) {
71
+ if (provider instanceof EchoLLMProvider)
72
+ return null;
73
+ const rawModel = options.model?.trim() ?? '';
74
+ const model = rawModel.toLowerCase() === 'default' ? '' : rawModel;
75
+ const response = await provider.invoke(buildSessionTitlePromptMessages(payload), {
76
+ model,
77
+ temperature: 0.2,
78
+ maxTokens: 64,
79
+ toolChoice: 'none',
80
+ turn: {
81
+ sessionId: options.sessionId,
82
+ nodeId: SESSION_TITLE_TURN_NODE_ID,
83
+ },
84
+ });
85
+ return normalizeGeneratedTitle(response.content ?? '');
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gt-fe/eap-runtime",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "description": "Enterprise Agent Platform — Runtime核心:Graph执行引擎、Task生命周期、Session管理、Checkpoint持久化",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -20,7 +20,7 @@
20
20
  "ioredis": "^5.4.0",
21
21
  "pg": "^8.13.0",
22
22
  "yaml": "^2.6.1",
23
- "@gt-fe/eap-contracts": "0.0.15"
23
+ "@gt-fe/eap-contracts": "0.0.16"
24
24
  },
25
25
  "publishConfig": {
26
26
  "registry": "https://registry.npmjs.org/",