@gt-fe/eap-runtime 0.0.21 → 0.0.22
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 +1 -1
- package/dist/api/handlers/invoke.handler.js +6 -3
- package/dist/api/handlers/stream.handler.js +6 -2
- package/dist/checkpoint/file-checkpoint-saver.d.ts +4 -0
- package/dist/checkpoint/file-checkpoint-saver.js +51 -22
- package/dist/checkpoint/index.d.ts +1 -1
- package/dist/checkpoint/index.js +1 -1
- package/dist/graph/adapters/langgraph-checkpointer.d.ts +11 -0
- package/dist/graph/adapters/langgraph-checkpointer.js +48 -9
- package/dist/graph/adapters/langgraph.adapter.d.ts +8 -0
- package/dist/graph/adapters/langgraph.adapter.js +58 -16
- package/dist/graph/adapters/skill-isolate.d.ts +1 -5
- package/dist/graph/adapters/skill-isolate.js +1 -12
- package/dist/graph/adapters/subgraph-compiler.js +12 -15
- package/dist/graph/contracts/executable-graph.d.ts +6 -0
- package/dist/graph/hitl/hitl-handler.d.ts +13 -4
- package/dist/graph/hitl/hitl-handler.js +78 -7
- package/dist/graph/nodes/tool-node.js +1 -5
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/providers/local-llm-provider.d.ts +6 -0
- package/dist/providers/local-llm-provider.js +29 -5
- package/dist/providers/routing-llm-provider.d.ts +2 -2
- package/dist/providers/routing-llm-provider.js +5 -15
- package/dist/registry/deployment-snapshot.js +18 -4
- package/dist/registry/graph-loader.d.ts +2 -0
- package/dist/registry/graph-loader.js +39 -2
- package/dist/registry/preload-flags.d.ts +1 -1
- package/dist/registry/preload-flags.js +1 -1
- package/dist/runtime/agent-runtime.js +27 -10
- package/dist/runtime/dev-runtime.d.ts +2 -1
- package/dist/runtime/dev-runtime.js +9 -3
- package/dist/session/apply-session-compression.d.ts +4 -0
- package/dist/session/apply-session-compression.js +22 -3
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/persist-turn-messages.d.ts +6 -1
- package/dist/session/persist-turn-messages.js +23 -5
- package/dist/token/record-task-token-usage.d.ts +23 -0
- package/dist/token/record-task-token-usage.js +20 -0
- package/dist/tool/tool-confirmation.js +8 -5
- package/package.json +1 -1
|
@@ -7,20 +7,20 @@
|
|
|
7
7
|
* @see 04-runtime-core/design.md §1.5 / §1.7
|
|
8
8
|
*/
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
|
-
import { HITL_TIMEOUT_ERROR_CODE, HITL_TIMEOUT_MESSAGE, HitlAlreadyTimedOutError, HitlNotWaitingError, buildHitlWaitedPayload, isHitlDeadlinePassed, readPersistedHitlWaiting, unwrapHitlPaused, } from '../graph/hitl/hitl-handler.js';
|
|
10
|
+
import { HITL_TIMEOUT_ERROR_CODE, HITL_TIMEOUT_MESSAGE, HitlAlreadyTimedOutError, HitlNotWaitingError, buildHitlWaitedPayload, isHitlDeadlinePassed, loadHitlCheckpoint, readPersistedHitlWaiting, unwrapHitlPaused, } from '../graph/hitl/hitl-handler.js';
|
|
11
11
|
import { resolveAgentSystemPromptFromLoader } from '../registry/graph-loader.js';
|
|
12
12
|
import { toLoadedSkillMap } from '../tool/system-prompt-builder.js';
|
|
13
13
|
import { assembleLoadedTools } from '../tool/harness-primitives.js';
|
|
14
14
|
import { createFileLlmTurnLogFromEnv, } from '../debug/llm-turn-log.js';
|
|
15
15
|
import { buildHitlWaitingTaskOutput, buildTurnGraphMessages, buildTurnTranscriptFromStreamEvents, extractHitlThreadId, extractStreamReasoning, applyStreamTextDelta, createStreamAssistantBuckets, visibleStreamAssistantText, streamAssistantTexts, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, resolveCheckpointThreadId, appendTerminalToolResult, } from '../session/persist-turn-messages.js';
|
|
16
|
-
import { iterSessionCompression, } from '../session/apply-session-compression.js';
|
|
16
|
+
import { iterSessionCompression, withCompressionSummaryModel, } from '../session/apply-session-compression.js';
|
|
17
17
|
import { applySessionMemory } from '../session/apply-session-memory.js';
|
|
18
18
|
import { resolveTokenDetail, shouldEmitTokenSse } from '../token/token-detail.js';
|
|
19
19
|
import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
|
|
20
20
|
import { resolveExecutionContext } from '../governance/governance-client.js';
|
|
21
21
|
import { resolveAgentCode } from '../common/agent-code.js';
|
|
22
22
|
import { resolveTraceId } from '../common/trace-id.js';
|
|
23
|
-
import { collectStreamNodeTokenUsage, recordTaskTokenUsages, tokenUsagesFromLlmChannel, } from '../token/record-task-token-usage.js';
|
|
23
|
+
import { collectStreamNodeTokenUsage, recordTaskTokenUsages, summarizeTokenUsages, tokenUsagesFromLlmChannel, } from '../token/record-task-token-usage.js';
|
|
24
24
|
import { resolveSkillInputFromInvokeInput } from '../registry/skill-io-guard.js';
|
|
25
25
|
const HITL_TERMINAL_STATUSES = [
|
|
26
26
|
'success',
|
|
@@ -280,15 +280,18 @@ export class AgentRuntime {
|
|
|
280
280
|
: undefined;
|
|
281
281
|
const agentSystemPrompt = await resolveAgentSystemPromptFromLoader(this.deps.graphLoader, agentCode, params.agentVersion);
|
|
282
282
|
let history = messageRecordsToGraphMessages(sessionContext.messages);
|
|
283
|
+
const compressionOptions = await withCompressionSummaryModel({
|
|
284
|
+
...this.deps.sessionCompression,
|
|
285
|
+
turnLog: this.deps.sessionCompression?.turnLog ?? this.deps.turnLog,
|
|
286
|
+
}, () => this.deps.graphLoader
|
|
287
|
+
? this.deps.graphLoader.resolveDefaultModel(agentCode, params.agentVersion)
|
|
288
|
+
: Promise.resolve(undefined));
|
|
283
289
|
for await (const step of iterSessionCompression({
|
|
284
290
|
history,
|
|
285
291
|
sessionId,
|
|
286
292
|
userId,
|
|
287
293
|
taskId: task.id,
|
|
288
|
-
options:
|
|
289
|
-
...this.deps.sessionCompression,
|
|
290
|
-
turnLog: this.deps.sessionCompression?.turnLog ?? this.deps.turnLog,
|
|
291
|
-
},
|
|
294
|
+
options: compressionOptions,
|
|
292
295
|
})) {
|
|
293
296
|
if (step.kind === 'history') {
|
|
294
297
|
history = step.messages;
|
|
@@ -367,6 +370,7 @@ export class AgentRuntime {
|
|
|
367
370
|
}
|
|
368
371
|
await recordTaskTokenUsages(this.deps.taskService, task.id, nodeTokenUsages);
|
|
369
372
|
await this.deps.taskService.updateStatus(task.id, 'success');
|
|
373
|
+
const tokenSummary = summarizeTokenUsages(nodeTokenUsages);
|
|
370
374
|
await persistTurnTranscript(this.deps.messageService, {
|
|
371
375
|
sessionId,
|
|
372
376
|
taskId: task.id,
|
|
@@ -380,7 +384,12 @@ export class AgentRuntime {
|
|
|
380
384
|
toolCallReasoningContent,
|
|
381
385
|
assistantReasoningContent,
|
|
382
386
|
}),
|
|
387
|
+
assistantTokenCount: tokenSummary.total.output,
|
|
388
|
+
assistantDurationMs: Date.now() - startTime,
|
|
383
389
|
});
|
|
390
|
+
if (shouldEmitTokenSse(tokenDetail, 'stats.token_usage')) {
|
|
391
|
+
yield this.toSSE('stats.token_usage', correlationId, task.id, tokenSummary);
|
|
392
|
+
}
|
|
384
393
|
yield this.toSSE('task.completed', correlationId, task.id, {
|
|
385
394
|
taskId: task.id,
|
|
386
395
|
duration: Date.now() - startTime,
|
|
@@ -559,6 +568,7 @@ export class AgentRuntime {
|
|
|
559
568
|
}
|
|
560
569
|
await recordTaskTokenUsages(this.deps.taskService, taskId, nodeTokenUsages);
|
|
561
570
|
await this.deps.taskService.updateStatus(taskId, 'success', visibleStreamAssistantText(assistantBuckets) || undefined);
|
|
571
|
+
const tokenSummary = summarizeTokenUsages(nodeTokenUsages);
|
|
562
572
|
if (sessionId) {
|
|
563
573
|
await persistTurnTranscript(this.deps.messageService, {
|
|
564
574
|
sessionId,
|
|
@@ -573,8 +583,13 @@ export class AgentRuntime {
|
|
|
573
583
|
toolCallReasoningContent,
|
|
574
584
|
assistantReasoningContent,
|
|
575
585
|
}),
|
|
586
|
+
assistantTokenCount: tokenSummary.total.output,
|
|
587
|
+
assistantDurationMs: Date.now() - startTime,
|
|
576
588
|
});
|
|
577
589
|
}
|
|
590
|
+
if (shouldEmitTokenSse(tokenDetail, 'stats.token_usage')) {
|
|
591
|
+
yield this.toSSE('stats.token_usage', correlationId, taskId, tokenSummary);
|
|
592
|
+
}
|
|
578
593
|
yield this.toSSE('task.completed', correlationId, taskId, {
|
|
579
594
|
taskId,
|
|
580
595
|
duration: Date.now() - startTime,
|
|
@@ -778,16 +793,18 @@ export class AgentRuntime {
|
|
|
778
793
|
const sessionId = typeof task.sessionId === 'string' && task.sessionId.trim()
|
|
779
794
|
? task.sessionId.trim()
|
|
780
795
|
: undefined;
|
|
781
|
-
const
|
|
796
|
+
const hinted = waiting?.threadId ??
|
|
782
797
|
(typeof response.threadId === 'string' && response.threadId.trim()
|
|
783
798
|
? response.threadId.trim()
|
|
784
799
|
: undefined) ??
|
|
785
800
|
extractHitlThreadId(task.output) ??
|
|
786
801
|
sessionId;
|
|
787
|
-
if (!
|
|
802
|
+
if (!hinted) {
|
|
788
803
|
throw new AppError(ErrorCodes.TASK_HITL_INVALID_STATE, `Task ${taskId} 缺少 sessionId/hitlThreadId,无法冷恢复 HITL Checkpoint`, { exposeMessage: true });
|
|
789
804
|
}
|
|
790
|
-
const
|
|
805
|
+
const resolved = await loadHitlCheckpoint(this.deps.checkpointSaver, hinted);
|
|
806
|
+
const threadId = resolved.threadId;
|
|
807
|
+
const checkpoint = resolved.checkpoint;
|
|
791
808
|
const persisted = readPersistedHitlWaiting(checkpoint?.state);
|
|
792
809
|
const behavior = persisted?.config.timeoutBehavior ?? 'error';
|
|
793
810
|
if (persisted &&
|
|
@@ -84,6 +84,7 @@ export interface DevRuntime {
|
|
|
84
84
|
* 装配本地 dev 模式所需的全部 Runtime 依赖
|
|
85
85
|
*
|
|
86
86
|
* @returns DevRuntime(含 invokeOrchestrator / streamOrchestrator / 诊断摘要)
|
|
87
|
-
* @throws EAP_LLM_MODE=local
|
|
87
|
+
* @throws `EAP_LLM_MODE=local` 且解不出密钥
|
|
88
|
+
* @throws 已配 `FOUNDATION_SERVICE_URL` 且 `EAP_LLM_CONNECTIONS` 不是合法 JSON 对象
|
|
88
89
|
*/
|
|
89
90
|
export declare function createDevRuntime(options?: CreateDevRuntimeOptions): DevRuntime;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { createDefaultLLMProvider } from '../providers/echo-llm-provider.js';
|
|
27
27
|
import { HttpLLMProvider } from '../providers/http-llm-provider.js';
|
|
28
|
-
import { createLocalLlmProvider, requireLocalLlmConnections } from '../providers/local-llm-provider.js';
|
|
28
|
+
import { createLocalLlmProvider, createOptionalLocalLlmProvider, requireLocalLlmConnections, } from '../providers/local-llm-provider.js';
|
|
29
29
|
import { RoutingLLMProvider } from '../providers/routing-llm-provider.js';
|
|
30
30
|
import { createToolExecutor } from '../tool/create-tool-executor.js';
|
|
31
31
|
import { DevToolExecutor } from '../tool/dev-tool-executor.js';
|
|
@@ -44,7 +44,8 @@ import { isRuntimeMemoryEnabled } from '../session/apply-session-memory.js';
|
|
|
44
44
|
* 装配本地 dev 模式所需的全部 Runtime 依赖
|
|
45
45
|
*
|
|
46
46
|
* @returns DevRuntime(含 invokeOrchestrator / streamOrchestrator / 诊断摘要)
|
|
47
|
-
* @throws EAP_LLM_MODE=local
|
|
47
|
+
* @throws `EAP_LLM_MODE=local` 且解不出密钥
|
|
48
|
+
* @throws 已配 `FOUNDATION_SERVICE_URL` 且 `EAP_LLM_CONNECTIONS` 不是合法 JSON 对象
|
|
48
49
|
*/
|
|
49
50
|
export function createDevRuntime(options) {
|
|
50
51
|
const foundationUrl = process.env.FOUNDATION_SERVICE_URL ?? 'http://localhost:3100';
|
|
@@ -72,7 +73,12 @@ export function createDevRuntime(options) {
|
|
|
72
73
|
llmProviderKind = 'openai-http';
|
|
73
74
|
}
|
|
74
75
|
else if (foundationConfigured) {
|
|
75
|
-
|
|
76
|
+
const fallbackProvider = createOptionalLocalLlmProvider(turnLog);
|
|
77
|
+
llmProvider = new RoutingLLMProvider({
|
|
78
|
+
foundationUrl,
|
|
79
|
+
turnLog,
|
|
80
|
+
...(fallbackProvider ? { fallbackProvider } : {}),
|
|
81
|
+
});
|
|
76
82
|
llmProviderKind = 'routing';
|
|
77
83
|
}
|
|
78
84
|
else {
|
|
@@ -23,6 +23,8 @@ export interface SessionCompressionOptions {
|
|
|
23
23
|
compressionRepository?: ICompressionRepository;
|
|
24
24
|
maxRounds?: number;
|
|
25
25
|
preserveRecentRounds?: number;
|
|
26
|
+
/** 摘要模型;与图节点同一套:Agent `models.default`。不要传字符串 `default`。 */
|
|
27
|
+
summaryModel?: string;
|
|
26
28
|
}
|
|
27
29
|
export interface RunSessionCompressionParams {
|
|
28
30
|
history: GraphMessage[];
|
|
@@ -72,6 +74,8 @@ export declare function formatCompressionCompletedMessage(input: {
|
|
|
72
74
|
error?: string;
|
|
73
75
|
}): string;
|
|
74
76
|
export declare function applySessionCompression(params: RunSessionCompressionParams): Promise<GraphMessage[]>;
|
|
77
|
+
/** 未显式 summaryModel 时写入 Agent `models.default`。压缩未开则不解析。 */
|
|
78
|
+
export declare function withCompressionSummaryModel(options: SessionCompressionOptions | undefined, resolveDefaultModel: () => Promise<string | undefined>, env?: NodeJS.ProcessEnv): Promise<SessionCompressionOptions | undefined>;
|
|
75
79
|
/**
|
|
76
80
|
* 流式入口用:先产出 started,再跑摘要,再产出 completed,最后给出本轮历史。
|
|
77
81
|
* SDK `stream` 可在 started 之后才继续 next(),HTTP SSE 可立刻 emit。
|
|
@@ -49,6 +49,19 @@ export async function applySessionCompression(params) {
|
|
|
49
49
|
const result = await runSessionCompression(params);
|
|
50
50
|
return result.messages;
|
|
51
51
|
}
|
|
52
|
+
/** 未显式 summaryModel 时写入 Agent `models.default`。压缩未开则不解析。 */
|
|
53
|
+
export async function withCompressionSummaryModel(options, resolveDefaultModel, env = process.env) {
|
|
54
|
+
if (!options)
|
|
55
|
+
return options;
|
|
56
|
+
if (!isRuntimeCompressionEnabled(options.enabled, env))
|
|
57
|
+
return options;
|
|
58
|
+
if (options.summaryModel?.trim())
|
|
59
|
+
return options;
|
|
60
|
+
const summaryModel = (await resolveDefaultModel())?.trim();
|
|
61
|
+
if (!summaryModel || summaryModel.toLowerCase() === 'default')
|
|
62
|
+
return options;
|
|
63
|
+
return { ...options, summaryModel };
|
|
64
|
+
}
|
|
52
65
|
/**
|
|
53
66
|
* 流式入口用:先产出 started,再跑摘要,再产出 completed,最后给出本轮历史。
|
|
54
67
|
* SDK `stream` 可在 started 之后才继续 next(),HTTP SSE 可立刻 emit。
|
|
@@ -151,7 +164,7 @@ async function runDecidedSessionCompression(params, decision) {
|
|
|
151
164
|
maxRounds,
|
|
152
165
|
preserveRecentRounds: options.preserveRecentRounds ?? DEFAULT_COMPRESSION_CONFIG.preserveRecentRounds,
|
|
153
166
|
...(decision.window ? { maxContextTokens: decision.window } : {}),
|
|
154
|
-
}, async (msgs) => summarizeWithProvider(provider, msgs));
|
|
167
|
+
}, async (msgs) => summarizeWithProvider(provider, msgs, options.summaryModel));
|
|
155
168
|
const compressed = await compression.compress(history);
|
|
156
169
|
if (compressed === history) {
|
|
157
170
|
return {
|
|
@@ -242,18 +255,24 @@ async function lastTurnUsage(turnLog, sessionId) {
|
|
|
242
255
|
return undefined;
|
|
243
256
|
return { used: last.used, window: last.window };
|
|
244
257
|
}
|
|
245
|
-
async function summarizeWithProvider(provider, messages) {
|
|
258
|
+
async function summarizeWithProvider(provider, messages, summaryModel) {
|
|
246
259
|
const body = messages
|
|
247
260
|
.map((m) => `${m.role}: ${m.content}`)
|
|
248
261
|
.join('\n')
|
|
249
262
|
.slice(0, 12000);
|
|
263
|
+
const raw = summaryModel?.trim();
|
|
264
|
+
const model = raw && raw.toLowerCase() !== 'default' ? raw : '';
|
|
250
265
|
const response = await provider.invoke([
|
|
251
266
|
{
|
|
252
267
|
role: 'system',
|
|
253
268
|
content: '将对话历史压成短摘要,保留用户需求、结论和未完成事项。',
|
|
254
269
|
},
|
|
255
270
|
{ role: 'user', content: body },
|
|
256
|
-
], {
|
|
271
|
+
], {
|
|
272
|
+
model,
|
|
273
|
+
temperature: 0.2,
|
|
274
|
+
maxTokens: 1024,
|
|
275
|
+
});
|
|
257
276
|
const text = response.content?.trim();
|
|
258
277
|
if (!text) {
|
|
259
278
|
throw new Error('empty compression summary');
|
package/dist/session/index.d.ts
CHANGED
|
@@ -10,6 +10,6 @@ export type { FileMessageRepositoryOptions } from './file-message.repository.js'
|
|
|
10
10
|
export { SessionContextService } from './session-context.service.js';
|
|
11
11
|
export type { ResolveSessionParams, SessionContext, } from './session-context.service.js';
|
|
12
12
|
export { CompressionMiddleware, createCompressionMiddleware } from './compression-middleware.js';
|
|
13
|
-
export { applySessionCompression, runSessionCompression, decideSessionCompression, iterSessionCompression, formatCompressionStartedMessage, formatCompressionCompletedMessage, isRuntimeCompressionEnabled, isRuntimeCompressionPersist, type SessionCompressionOptions, type RunSessionCompressionParams, type RunSessionCompressionResult, type SessionCompressionDecision, type SessionCompressionStep, } from './apply-session-compression.js';
|
|
13
|
+
export { applySessionCompression, 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';
|
package/dist/session/index.js
CHANGED
|
@@ -10,5 +10,5 @@ export { FileMessageRepository } from './file-message.repository.js';
|
|
|
10
10
|
export { SessionContextService } from './session-context.service.js';
|
|
11
11
|
// 压缩中间件
|
|
12
12
|
export { CompressionMiddleware, createCompressionMiddleware } from './compression-middleware.js';
|
|
13
|
-
export { applySessionCompression, runSessionCompression, decideSessionCompression, iterSessionCompression, formatCompressionStartedMessage, formatCompressionCompletedMessage, isRuntimeCompressionEnabled, isRuntimeCompressionPersist, } from './apply-session-compression.js';
|
|
13
|
+
export { applySessionCompression, withCompressionSummaryModel, runSessionCompression, decideSessionCompression, iterSessionCompression, formatCompressionStartedMessage, formatCompressionCompletedMessage, isRuntimeCompressionEnabled, isRuntimeCompressionPersist, } from './apply-session-compression.js';
|
|
14
14
|
export { applySessionMemory, isRuntimeMemoryEnabled, } from './apply-session-memory.js';
|
|
@@ -21,6 +21,7 @@ export declare function persistAssistantTurnMessage(messageService: MessageServi
|
|
|
21
21
|
toolCalls?: unknown[];
|
|
22
22
|
/** DeepSeek thinking:多轮回传用 */
|
|
23
23
|
reasoningContent?: string;
|
|
24
|
+
durationMs?: number | null;
|
|
24
25
|
}): Promise<void>;
|
|
25
26
|
/**
|
|
26
27
|
* HITL 等待态占位消息:避免早退时本轮只有 user、历史偏薄
|
|
@@ -80,6 +81,7 @@ export declare function sessionVisibleAssistantTexts(texts: Array<{
|
|
|
80
81
|
* invoke 路径图增量。tool_calls / tool 行原样保留,纯文本助手收成一条放在末尾(与 stream 落库顺序一致)。
|
|
81
82
|
* GraphMessage 无 nodeId:`finishPoints` 只含一个实际节点 id 时取最后一条纯文本,不是按 id 筛选(官方 DAG 的 `merge` 在末尾)。
|
|
82
83
|
* 没有实际节点 id 或仅为 `__end__` 时把多段正文用空行拼进这一条。
|
|
84
|
+
* 无 toolCallId 的 tool 行不进 Session(code 节点结果)。
|
|
83
85
|
*/
|
|
84
86
|
export declare function sessionVisibleGraphMessages(messages: GraphMessage[], finishPoints?: string[]): GraphMessage[];
|
|
85
87
|
/**
|
|
@@ -138,8 +140,10 @@ export declare function buildHitlWaitingTaskOutput(params: {
|
|
|
138
140
|
/** 从 Task.output 解析 Checkpoint thread_id(含嵌套 hitl.threadId) */
|
|
139
141
|
export declare function extractHitlThreadId(output: unknown): string | undefined;
|
|
140
142
|
/**
|
|
141
|
-
* Checkpoint thread_id
|
|
143
|
+
* Checkpoint 父图 thread_id:HITL/多轮优先且应唯一等于 sessionId;
|
|
142
144
|
* 无 session 时退到 `task-${taskId}`。勿用 correlationId 作为 Checkpoint key。
|
|
145
|
+
* 子图槽位再经 `checkpointStorageThreadId`(`thread_id#父节点id`),
|
|
146
|
+
* 与 LangGraph put / 子图内 HITL interrupt 同一把钥匙。
|
|
143
147
|
*
|
|
144
148
|
* 注意:session 级 thread + 全量 Session 历史同时注入会导致 messages 双重 append。
|
|
145
149
|
* 组装本轮 Graph 输入请用 {@link buildTurnGraphMessages}(有 Checkpoint 历史则只追加 user)。
|
|
@@ -193,4 +197,5 @@ export declare function persistTurnTranscript(messageService: MessageService | u
|
|
|
193
197
|
/** 无 assistant 文本消息时的兜底(invoke 的 graphResult.output) */
|
|
194
198
|
fallbackAssistantContent?: string;
|
|
195
199
|
assistantTokenCount?: number;
|
|
200
|
+
assistantDurationMs?: number | null;
|
|
196
201
|
}): Promise<void>;
|
|
@@ -32,6 +32,7 @@ export async function persistAssistantTurnMessage(messageService, params) {
|
|
|
32
32
|
reasoning: params.reasoningContent
|
|
33
33
|
? { reasoningContent: params.reasoningContent }
|
|
34
34
|
: null,
|
|
35
|
+
durationMs: params.durationMs ?? null,
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
@@ -163,21 +164,32 @@ function isTextAssistant(message) {
|
|
|
163
164
|
return false;
|
|
164
165
|
return Boolean(message.content?.trim());
|
|
165
166
|
}
|
|
167
|
+
/** Session 只收成对的 tool 结果;code 节点没有 toolCallId,留在 Checkpoint。 */
|
|
168
|
+
function isSessionToolMessage(message) {
|
|
169
|
+
if (message.role !== 'tool')
|
|
170
|
+
return false;
|
|
171
|
+
return Boolean(message.toolCallId?.trim());
|
|
172
|
+
}
|
|
173
|
+
function sessionGraphMessages(messages) {
|
|
174
|
+
return messages.filter((message) => message.role !== 'tool' || isSessionToolMessage(message));
|
|
175
|
+
}
|
|
166
176
|
/**
|
|
167
177
|
* invoke 路径图增量。tool_calls / tool 行原样保留,纯文本助手收成一条放在末尾(与 stream 落库顺序一致)。
|
|
168
178
|
* GraphMessage 无 nodeId:`finishPoints` 只含一个实际节点 id 时取最后一条纯文本,不是按 id 筛选(官方 DAG 的 `merge` 在末尾)。
|
|
169
179
|
* 没有实际节点 id 或仅为 `__end__` 时把多段正文用空行拼进这一条。
|
|
180
|
+
* 无 toolCallId 的 tool 行不进 Session(code 节点结果)。
|
|
170
181
|
*/
|
|
171
182
|
export function sessionVisibleGraphMessages(messages, finishPoints) {
|
|
172
|
-
const
|
|
183
|
+
const eligible = sessionGraphMessages(messages);
|
|
184
|
+
const texts = eligible.filter(isTextAssistant);
|
|
173
185
|
if (texts.length === 0)
|
|
174
|
-
return
|
|
186
|
+
return eligible;
|
|
175
187
|
const named = namedFinishPoints(finishPoints);
|
|
176
188
|
const visible = named.length === 1 ? [texts.at(-1)] : texts;
|
|
177
189
|
const keep = visible.at(-1);
|
|
178
190
|
const joined = visible.map((message) => message.content).join('\n\n');
|
|
179
191
|
const out = [];
|
|
180
|
-
for (const message of
|
|
192
|
+
for (const message of eligible) {
|
|
181
193
|
if (isTextAssistant(message))
|
|
182
194
|
continue;
|
|
183
195
|
out.push(message);
|
|
@@ -344,8 +356,10 @@ export function extractHitlThreadId(output) {
|
|
|
344
356
|
return undefined;
|
|
345
357
|
}
|
|
346
358
|
/**
|
|
347
|
-
* Checkpoint thread_id
|
|
359
|
+
* Checkpoint 父图 thread_id:HITL/多轮优先且应唯一等于 sessionId;
|
|
348
360
|
* 无 session 时退到 `task-${taskId}`。勿用 correlationId 作为 Checkpoint key。
|
|
361
|
+
* 子图槽位再经 `checkpointStorageThreadId`(`thread_id#父节点id`),
|
|
362
|
+
* 与 LangGraph put / 子图内 HITL interrupt 同一把钥匙。
|
|
349
363
|
*
|
|
350
364
|
* 注意:session 级 thread + 全量 Session 历史同时注入会导致 messages 双重 append。
|
|
351
365
|
* 组装本轮 Graph 输入请用 {@link buildTurnGraphMessages}(有 Checkpoint 历史则只追加 user)。
|
|
@@ -436,14 +450,17 @@ export async function persistTurnTranscript(messageService, params) {
|
|
|
436
450
|
toolCalls: m.toolCalls,
|
|
437
451
|
tokenCount: isToolCallOnly ? 0 : params.assistantTokenCount,
|
|
438
452
|
reasoningContent: m.reasoningContent,
|
|
453
|
+
durationMs: isToolCallOnly ? null : params.assistantDurationMs,
|
|
439
454
|
});
|
|
440
455
|
}
|
|
441
456
|
else if (m.role === 'tool') {
|
|
457
|
+
if (!m.toolCallId?.trim())
|
|
458
|
+
continue;
|
|
442
459
|
await persistToolTurnMessage(messageService, {
|
|
443
460
|
sessionId: params.sessionId,
|
|
444
461
|
taskId: params.taskId,
|
|
445
462
|
correlationId: params.correlationId,
|
|
446
|
-
toolCallId: m.toolCallId
|
|
463
|
+
toolCallId: m.toolCallId,
|
|
447
464
|
toolName: m.toolName ?? 'unknown',
|
|
448
465
|
content: m.content,
|
|
449
466
|
success: m.success !== false,
|
|
@@ -457,6 +474,7 @@ export async function persistTurnTranscript(messageService, params) {
|
|
|
457
474
|
correlationId: params.correlationId,
|
|
458
475
|
content: params.fallbackAssistantContent,
|
|
459
476
|
tokenCount: params.assistantTokenCount,
|
|
477
|
+
durationMs: params.assistantDurationMs,
|
|
460
478
|
});
|
|
461
479
|
}
|
|
462
480
|
}
|
|
@@ -14,6 +14,29 @@ export declare function tokenUsageFromNodePayload(payload: unknown): TokenUsageR
|
|
|
14
14
|
export declare function collectStreamNodeTokenUsage(event: Pick<RuntimeEvent, 'type' | 'payload'>, bucket: TokenUsageRecord[]): void;
|
|
15
15
|
/** Graph `__llmTokenUsage` channel → Task.token_usage 行(invoke / fork 抛错后从 Checkpoint 读)。 */
|
|
16
16
|
export declare function tokenUsagesFromLlmChannel(state: unknown): TokenUsageRecord[];
|
|
17
|
+
/** 本轮 LLM 用量汇总,供 stats.token_usage 与 assistantTokenCount。 */
|
|
18
|
+
export declare function summarizeTokenUsages(usages: Array<{
|
|
19
|
+
nodeId: string;
|
|
20
|
+
model: string;
|
|
21
|
+
inputTokens: number;
|
|
22
|
+
outputTokens: number;
|
|
23
|
+
costEstimate?: number;
|
|
24
|
+
}>): {
|
|
25
|
+
total: {
|
|
26
|
+
input: number;
|
|
27
|
+
output: number;
|
|
28
|
+
total: number;
|
|
29
|
+
};
|
|
30
|
+
byNode: Record<string, {
|
|
31
|
+
input: number;
|
|
32
|
+
output: number;
|
|
33
|
+
}>;
|
|
34
|
+
byModel: Record<string, {
|
|
35
|
+
input: number;
|
|
36
|
+
output: number;
|
|
37
|
+
}>;
|
|
38
|
+
costEstimate: number;
|
|
39
|
+
};
|
|
17
40
|
export declare function recordTaskTokenUsages(taskService: TaskTokenUsageWriter, taskId: string, usages: Array<{
|
|
18
41
|
nodeId: string;
|
|
19
42
|
model: string;
|
|
@@ -53,6 +53,26 @@ export function tokenUsagesFromLlmChannel(state) {
|
|
|
53
53
|
}
|
|
54
54
|
return rows;
|
|
55
55
|
}
|
|
56
|
+
/** 本轮 LLM 用量汇总,供 stats.token_usage 与 assistantTokenCount。 */
|
|
57
|
+
export function summarizeTokenUsages(usages) {
|
|
58
|
+
const byNode = {};
|
|
59
|
+
const byModel = {};
|
|
60
|
+
let input = 0;
|
|
61
|
+
let output = 0;
|
|
62
|
+
let costEstimate = 0;
|
|
63
|
+
for (const usage of usages) {
|
|
64
|
+
input += usage.inputTokens;
|
|
65
|
+
output += usage.outputTokens;
|
|
66
|
+
costEstimate += usage.costEstimate ?? 0;
|
|
67
|
+
const node = byNode[usage.nodeId] ?? (byNode[usage.nodeId] = { input: 0, output: 0 });
|
|
68
|
+
node.input += usage.inputTokens;
|
|
69
|
+
node.output += usage.outputTokens;
|
|
70
|
+
const model = byModel[usage.model] ?? (byModel[usage.model] = { input: 0, output: 0 });
|
|
71
|
+
model.input += usage.inputTokens;
|
|
72
|
+
model.output += usage.outputTokens;
|
|
73
|
+
}
|
|
74
|
+
return { total: { input, output, total: input + output }, byNode, byModel, costEstimate };
|
|
75
|
+
}
|
|
56
76
|
export async function recordTaskTokenUsages(taskService, taskId, usages) {
|
|
57
77
|
for (const usage of usages) {
|
|
58
78
|
try {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* @see tasks.md Task 17
|
|
10
10
|
*/
|
|
11
11
|
import { DEFAULT_HITL_TIMEOUT_MS, HitlPausedError, } from '../graph/hitl/hitl-handler.js';
|
|
12
|
+
import { checkpointStorageThreadId } from '../graph/adapters/langgraph-checkpointer.js';
|
|
12
13
|
import { resolveCheckpointThreadId } from '../session/persist-turn-messages.js';
|
|
13
14
|
export class ToolConfirmationInterceptor {
|
|
14
15
|
confirmationTools;
|
|
@@ -54,11 +55,13 @@ export class ToolConfirmationInterceptor {
|
|
|
54
55
|
errorCode: 'hitl_rejected',
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
const sessionId = typeof state.sessionId === 'string' && state.sessionId.trim()
|
|
59
|
+
? state.sessionId.trim()
|
|
60
|
+
: undefined;
|
|
61
|
+
const parentThreadId = sessionId ??
|
|
62
|
+
threadId ??
|
|
63
|
+
resolveCheckpointThreadId({ taskId });
|
|
64
|
+
const resolvedThreadId = checkpointStorageThreadId(parentThreadId, typeof state.checkpointNs === 'string' ? state.checkpointNs : undefined);
|
|
62
65
|
const question = `确认执行 ${request.toolName}?\n参数: ${JSON.stringify(request.arguments)}`;
|
|
63
66
|
const options = ['confirm', 'reject'];
|
|
64
67
|
const timeoutMs = DEFAULT_HITL_TIMEOUT_MS;
|