@gt-fe/eap-runtime 0.0.21 → 0.0.23

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.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/dist/api/handlers/invoke.handler.js +6 -3
  3. package/dist/api/handlers/stream.handler.js +6 -2
  4. package/dist/checkpoint/file-checkpoint-saver.d.ts +4 -0
  5. package/dist/checkpoint/file-checkpoint-saver.js +51 -22
  6. package/dist/checkpoint/index.d.ts +1 -1
  7. package/dist/checkpoint/index.js +1 -1
  8. package/dist/graph/adapters/langgraph-checkpointer.d.ts +11 -0
  9. package/dist/graph/adapters/langgraph-checkpointer.js +48 -9
  10. package/dist/graph/adapters/langgraph.adapter.d.ts +8 -0
  11. package/dist/graph/adapters/langgraph.adapter.js +58 -16
  12. package/dist/graph/adapters/skill-isolate.d.ts +1 -5
  13. package/dist/graph/adapters/skill-isolate.js +1 -12
  14. package/dist/graph/adapters/subgraph-compiler.js +12 -15
  15. package/dist/graph/contracts/executable-graph.d.ts +6 -0
  16. package/dist/graph/hitl/hitl-handler.d.ts +13 -4
  17. package/dist/graph/hitl/hitl-handler.js +78 -7
  18. package/dist/graph/nodes/tool-node.js +11 -6
  19. package/dist/providers/index.d.ts +1 -1
  20. package/dist/providers/index.js +1 -1
  21. package/dist/providers/local-llm-provider.d.ts +6 -0
  22. package/dist/providers/local-llm-provider.js +29 -5
  23. package/dist/providers/routing-llm-provider.d.ts +2 -2
  24. package/dist/providers/routing-llm-provider.js +5 -15
  25. package/dist/registry/deployment-snapshot.js +18 -4
  26. package/dist/registry/graph-loader.d.ts +2 -0
  27. package/dist/registry/graph-loader.js +99 -7
  28. package/dist/registry/preload-flags.d.ts +1 -1
  29. package/dist/registry/preload-flags.js +1 -1
  30. package/dist/runtime/agent-runtime.js +27 -10
  31. package/dist/runtime/dev-runtime.d.ts +2 -1
  32. package/dist/runtime/dev-runtime.js +9 -3
  33. package/dist/session/apply-session-compression.d.ts +4 -0
  34. package/dist/session/apply-session-compression.js +22 -3
  35. package/dist/session/index.d.ts +1 -1
  36. package/dist/session/index.js +1 -1
  37. package/dist/session/persist-turn-messages.d.ts +6 -1
  38. package/dist/session/persist-turn-messages.js +23 -5
  39. package/dist/token/record-task-token-usage.d.ts +23 -0
  40. package/dist/token/record-task-token-usage.js +20 -0
  41. package/dist/tool/http-tool-executor.js +7 -3
  42. package/dist/tool/index.d.ts +2 -1
  43. package/dist/tool/index.js +2 -1
  44. package/dist/tool/loaded-tool-definition.d.ts +1 -1
  45. package/dist/tool/local-mcp-client.d.ts +1 -1
  46. package/dist/tool/local-mcp-client.js +2 -39
  47. package/dist/tool/local-tool-executor.js +32 -5
  48. package/dist/tool/mcp-list.d.ts +21 -0
  49. package/dist/tool/mcp-list.js +49 -3
  50. package/dist/tool/mcp-tool-executor.d.ts +1 -1
  51. package/dist/tool/mcp-tool-executor.js +58 -22
  52. package/dist/tool/model-visible-tool-content.d.ts +16 -0
  53. package/dist/tool/model-visible-tool-content.js +266 -0
  54. package/dist/tool/timeout.d.ts +5 -12
  55. package/dist/tool/timeout.js +12 -34
  56. package/dist/tool/tool-confirmation.js +8 -5
  57. package/dist/tool/tool-executor.interface.d.ts +7 -1
  58. package/dist/tool/tool-orchestrator.js +6 -1
  59. package/dist/tool/tool-result-injector.js +2 -1
  60. package/package.json +2 -2
@@ -4,7 +4,7 @@
4
4
  * 生产(server + EAP_PREINSTALL_ROOT):以预装为准,缺失则失败(04 Req 8.1)。
5
5
  * 开发态:Registry + 可选 ./tools 本地 manifest。
6
6
  */
7
- import { readFile } from 'node:fs/promises';
7
+ import { readdir, readFile } from 'node:fs/promises';
8
8
  import path from 'node:path';
9
9
  import { resolveAgentModelAllowlist, resolveAgentDefaultModel } from './agent-models.js';
10
10
  import { DEFAULT_HTTP_TOOL_TIMEOUT_SEC, DEFAULT_TOOL_TIMEOUT_SEC } from '../tool/timeout.js';
@@ -144,6 +144,26 @@ const DEFAULT_GRAPH_DSL = {
144
144
  finishPoints: ['__end__'],
145
145
  config: { timeout: 60000 },
146
146
  };
147
+ /** 源图有非空字符串则保留;缺省或空数组为 `['__end__']`。预装 graph.json 与清单内嵌图同一收口。 */
148
+ function finishPointsFromGraphDocument(graph) {
149
+ const raw = graph.finishPoints;
150
+ if (!Array.isArray(raw))
151
+ return ['__end__'];
152
+ const ids = raw
153
+ .filter((id) => typeof id === 'string')
154
+ .map((id) => id.trim())
155
+ .filter((id) => id.length > 0);
156
+ return ids.length > 0 ? ids : ['__end__'];
157
+ }
158
+ const DEFAULT_LOADED_GRAPH_CONFIG = { timeout: 60000 };
159
+ /** 默认 timeout 60000;源图 `config` 覆盖。缺 `maxIterations` 时由引擎用 50。 */
160
+ function configFromGraphDocument(graph) {
161
+ const raw = graph.config;
162
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
163
+ return { ...DEFAULT_LOADED_GRAPH_CONFIG };
164
+ }
165
+ return { ...DEFAULT_LOADED_GRAPH_CONFIG, ...raw };
166
+ }
147
167
  function yamlGraphToDsl(agentCode, graph) {
148
168
  const nodes = graph.nodes ?? [];
149
169
  const edges = graph.edges ?? [];
@@ -155,8 +175,8 @@ function yamlGraphToDsl(agentCode, graph) {
155
175
  edges,
156
176
  conditionalEdges: graph.conditionalEdges,
157
177
  entryPoint: graph.entryPoint ?? nodes[0]?.id ?? 'llm',
158
- finishPoints: ['__end__'],
159
- config: { timeout: 60000 },
178
+ finishPoints: finishPointsFromGraphDocument(graph),
179
+ config: configFromGraphDocument(graph),
160
180
  };
161
181
  }
162
182
  function collectToolNames(agent, graphDsl) {
@@ -443,16 +463,71 @@ function fromInlineToolDeclaration(tool) {
443
463
  inputSchema: tool.input,
444
464
  };
445
465
  }
466
+ const DEV_LOCAL_MANIFEST_FILES = [
467
+ 'toolset.manifest.json',
468
+ 'tool.manifest.json',
469
+ 'manifest.json',
470
+ ];
471
+ async function readJsonObject(filePath) {
472
+ try {
473
+ const raw = await readFile(filePath, 'utf-8');
474
+ const parsed = JSON.parse(raw);
475
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
476
+ return null;
477
+ return parsed;
478
+ }
479
+ catch {
480
+ return null;
481
+ }
482
+ }
483
+ function loadedToolFromDevDir(dir, toolName, doc, toolsetOnly) {
484
+ const tools = doc.tools;
485
+ if (Array.isArray(tools) && tools.length > 0 && typeof tools[0] === 'object') {
486
+ const pre = {
487
+ toolId: String(doc.code ?? path.basename(dir)),
488
+ name: String(doc.name ?? path.basename(dir)),
489
+ version: String(doc.version ?? '0.0.0'),
490
+ manifest: doc,
491
+ bundlePath: dir,
492
+ };
493
+ const expanded = expandPreinstallToolset(pre);
494
+ return expanded.find((d) => d.name === toolName) ?? null;
495
+ }
496
+ if (toolsetOnly)
497
+ return null;
498
+ return fromLocalManifestFile(toolName, doc);
499
+ }
500
+ async function loadDevLocalManifestFromDir(dir, toolName, toolsetOnly) {
501
+ const files = toolsetOnly ? ['toolset.manifest.json'] : DEV_LOCAL_MANIFEST_FILES;
502
+ for (const file of files) {
503
+ const doc = await readJsonObject(path.join(dir, file));
504
+ if (!doc)
505
+ continue;
506
+ const hit = loadedToolFromDevDir(dir, toolName, doc, toolsetOnly);
507
+ if (hit)
508
+ return hit;
509
+ }
510
+ return null;
511
+ }
446
512
  async function loadDevLocalManifest(toolName, toolsDir) {
447
- const manifestPath = path.join(toolsDir, toolName, 'tool.manifest.json');
513
+ const direct = await loadDevLocalManifestFromDir(path.join(toolsDir, toolName), toolName, false);
514
+ if (direct)
515
+ return direct;
516
+ let entries;
448
517
  try {
449
- const raw = await readFile(manifestPath, 'utf-8');
450
- const manifest = JSON.parse(raw);
451
- return fromLocalManifestFile(toolName, manifest);
518
+ entries = await readdir(toolsDir, { withFileTypes: true });
452
519
  }
453
520
  catch {
454
521
  return null;
455
522
  }
523
+ for (const ent of entries) {
524
+ if (!ent.isDirectory() || ent.name === toolName)
525
+ continue;
526
+ const hit = await loadDevLocalManifestFromDir(path.join(toolsDir, ent.name), toolName, true);
527
+ if (hit)
528
+ return hit;
529
+ }
530
+ return null;
456
531
  }
457
532
  function fromLocalInstall(install) {
458
533
  return {
@@ -620,6 +695,23 @@ export class GraphLoader {
620
695
  }
621
696
  return this.cloneCached(graph);
622
697
  }
698
+ /** Agent `models.default`;无清单或未声明则 undefined。 */
699
+ async resolveDefaultModel(agentCode, version) {
700
+ const requested = this.resolveVersion({ agentVersion: version });
701
+ if (this.deploymentSnapshot) {
702
+ try {
703
+ const snap = await this.deploymentSnapshot.loadAgentSnapshot(agentCode, requested);
704
+ const model = resolveAgentDefaultModel((snap?.manifest ?? {}));
705
+ const trimmed = model?.trim();
706
+ if (trimmed && trimmed.toLowerCase() !== 'default')
707
+ return trimmed;
708
+ }
709
+ catch {
710
+ return undefined;
711
+ }
712
+ }
713
+ return undefined;
714
+ }
623
715
  async loadGraphFromSource(agentCode, version) {
624
716
  if (this.deploymentSnapshot) {
625
717
  const snap = await this.deploymentSnapshot.loadAgentSnapshot(agentCode, version);
@@ -5,7 +5,7 @@
5
5
  * |------|------|------|
6
6
  * | EAP_PRELOAD_REQUIRE_READY | 关 | 强制校验 .ready |
7
7
  * | EAP_PRELOAD_VALIDATE_SKILL_IO | **开** | 执行子图前校验 skill inputDef.required;`0`/`false` 关闭 |
8
- * | EAP_PRELOAD_REQUIRE_CHECKSUM | 关 | snapshot 必须带 sha256 或 artifacts[](字段存在性)。加载期摘要不匹配为 warn |
8
+ * | EAP_PRELOAD_REQUIRE_CHECKSUM | 关 | snapshot 必须带 sha256 或 artifacts[]。开启时摘要不匹配抛 RUNTIME_PRELOAD_PACKAGE_CORRUPT;未开仍 warn |
9
9
  * | EAP_PRELOAD_DEFINITION_CACHE | 有预装时开 | 进程内定义缓存;`0`/`false` 关闭 |
10
10
  * | EAP_SKILL_HOT_RELOAD | **dev 开 / server 关** | 已装配 Skill 的目录 mtime 变化则丢缓存;未写入装配名单的目录不进清单 |
11
11
  * | EAP_LOCAL_SKILLS_DIR | 关 | 开发态作者源 Skill 根;查找优先于 preinstall,低于 users/ |
@@ -5,7 +5,7 @@
5
5
  * |------|------|------|
6
6
  * | EAP_PRELOAD_REQUIRE_READY | 关 | 强制校验 .ready |
7
7
  * | EAP_PRELOAD_VALIDATE_SKILL_IO | **开** | 执行子图前校验 skill inputDef.required;`0`/`false` 关闭 |
8
- * | EAP_PRELOAD_REQUIRE_CHECKSUM | 关 | snapshot 必须带 sha256 或 artifacts[](字段存在性)。加载期摘要不匹配为 warn |
8
+ * | EAP_PRELOAD_REQUIRE_CHECKSUM | 关 | snapshot 必须带 sha256 或 artifacts[]。开启时摘要不匹配抛 RUNTIME_PRELOAD_PACKAGE_CORRUPT;未开仍 warn |
9
9
  * | EAP_PRELOAD_DEFINITION_CACHE | 有预装时开 | 进程内定义缓存;`0`/`false` 关闭 |
10
10
  * | EAP_SKILL_HOT_RELOAD | **dev 开 / server 关** | 已装配 Skill 的目录 mtime 变化则丢缓存;未写入装配名单的目录不进清单 |
11
11
  * | EAP_LOCAL_SKILLS_DIR | 关 | 开发态作者源 Skill 根;查找优先于 preinstall,低于 users/ |
@@ -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 threadId = waiting?.threadId ??
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 (!threadId) {
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 checkpoint = await this.deps.checkpointSaver.load(threadId);
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 但解不出任何密钥时抛错,避免静默 Echo
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 但解不出任何密钥时抛错,避免静默 Echo
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
- llmProvider = new RoutingLLMProvider({ foundationUrl, turnLog });
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
- ], { model: 'default', temperature: 0.2, maxTokens: 1024 });
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');
@@ -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';
@@ -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 契约:HITL/多轮优先且应唯一等于 sessionId;
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 texts = messages.filter(isTextAssistant);
183
+ const eligible = sessionGraphMessages(messages);
184
+ const texts = eligible.filter(isTextAssistant);
173
185
  if (texts.length === 0)
174
- return messages;
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 messages) {
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 契约:HITL/多轮优先且应唯一等于 sessionId;
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 ?? 'unknown',
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 {
@@ -5,7 +5,7 @@
5
5
  * @see docs/contracts/cross-service-api.md §4.6
6
6
  */
7
7
  import { ErrorCodes } from '@gt-fe/eap-contracts';
8
- import { abortOrTimeoutMessage, isAbortError } from './timeout.js';
8
+ import { abortOrTimeoutMessage, isAbortError, timeoutMsFromExecution } from './timeout.js';
9
9
  function mapGatewayHttpError(status, body, statusText) {
10
10
  const code = body?.error?.code;
11
11
  const bodyMessage = [body?.error?.message, body?.message].find((s) => typeof s === 'string' && s.trim()) ?? '';
@@ -81,13 +81,17 @@ export class HttpToolExecutor {
81
81
  Authorization: `Bearer ${request.authContext.delegationToken}`,
82
82
  'X-Agent-Id': agentCode,
83
83
  'X-Agent-Code': agentCode,
84
+ ...(request.authContext.agentVersion
85
+ ? { 'X-Agent-Version': request.authContext.agentVersion }
86
+ : {}),
84
87
  'X-User-Id': request.authContext.userId,
85
88
  userId: request.authContext.userId,
86
89
  'X-Correlation-Id': correlationId,
87
90
  'X-Task-Id': taskId,
88
91
  };
89
92
  const controller = new AbortController();
90
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
93
+ const timeoutMs = timeoutMsFromExecution(request.execution, this.timeout);
94
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
91
95
  try {
92
96
  const response = await fetch(url, {
93
97
  method: 'POST',
@@ -137,7 +141,7 @@ export class HttpToolExecutor {
137
141
  }
138
142
  catch (error) {
139
143
  clearTimeout(timeoutId);
140
- const timeoutSec = Math.max(1, Math.round(this.timeout / 1000));
144
+ const timeoutSec = Math.max(1, Math.round(timeoutMsFromExecution(request.execution, this.timeout) / 1000));
141
145
  const aborted = isAbortError(error);
142
146
  return {
143
147
  toolCallId: request.toolCallId,
@@ -1,5 +1,5 @@
1
1
  export type { AuthContext, ToolCallRequest, ToolCallResult, LLMToolCall, LLMToolCallResponse, IToolExecutor, McpListedTool, } from './tool-executor.interface.js';
2
- export { hydrateMcpTools, isMcpDiscoverStub, needsMcpHydration, parseListedTools, toolsetCodeFromLoaded, toolTraceIdentity, } from './mcp-list.js';
2
+ export { assertScopedListEcho, hydrateMcpTools, isMcpDiscoverStub, mcpToolsetCode, needsMcpHydration, parseListedTools, toolsetCodeFromLoaded, toolTraceIdentity, } from './mcp-list.js';
3
3
  export { toLoadedToolMap, type LoadedToolDefinition, type LoadedToolMap, type LoadedToolManifestRef, } from './loaded-tool-definition.js';
4
4
  export { ToolOrchestrator } from './tool-orchestrator.js';
5
5
  export { isRetryableToolFailure, resolveToolRetryConfig, type ToolFailureShape, } from './tool-retry.js';
@@ -11,6 +11,7 @@ export { DevHttpExecutor, buildHttpToolUrl, resolveHttpExecutionFromRequest, typ
11
11
  export { DevToolExecutor, type DevToolExecutorOptions } from './dev-tool-executor.js';
12
12
  export { LocalMcpClient, type ILocalMcpClient, type LocalMcpInvokeContext, type LocalMcpInvokeResult, } from './local-mcp-client.js';
13
13
  export { fileArtifactFromToolOutput, extractFileReference, resolveFileArtifact, type FileArtifact, } from './file-artifact.js';
14
+ export { formatMcpOutput, toModelVisibleToolContent, MAX_MODEL_RESULT_ROWS, MAX_MODEL_CONTENT_CHARS, MAX_MODEL_RESULT_CHARS, } from './model-visible-tool-content.js';
14
15
  export { ToolExecutionRouter, type DeploymentContext, type ToolExecutionRouterOptions, } from './tool-execution-router.js';
15
16
  export { createToolExecutor, EmptyInstallStore, type CreateToolExecutorOptions, } from './create-tool-executor.js';
16
17
  export { assembleLoadedTools, mergeHarnessPrimitives as mergeBuiltinTools, withHarnessPrimitives as withBuiltinTools, HarnessPrimitiveExecutor as BuiltinToolExecutor, resolveUserDir, resolveJailFromState, captureOverlayJailOptions, collectToolBundlePaths, type HarnessJailOptions as BuiltinJailOptions, } from './harness-primitives.js';