@gt-fe/eap-runtime 0.0.8 → 0.0.10

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 (87) hide show
  1. package/README.md +12 -17
  2. package/dist/api/handlers/invoke.handler.js +3 -3
  3. package/dist/api/handlers/messages.handler.js +0 -1
  4. package/dist/api/handlers/stream.handler.js +3 -3
  5. package/dist/api/routes/agent.routes.js +2 -2
  6. package/dist/checkpoint/checkpoint-ref-notify.d.ts +10 -0
  7. package/dist/checkpoint/checkpoint-ref-notify.js +59 -0
  8. package/dist/checkpoint/index.d.ts +1 -0
  9. package/dist/checkpoint/index.js +1 -0
  10. package/dist/checkpoint/pg-checkpoint-saver.js +25 -2
  11. package/dist/common/agent-code.d.ts +2 -4
  12. package/dist/common/agent-code.js +4 -5
  13. package/dist/contracts/trace-collector.d.ts +15 -1
  14. package/dist/debug/llm-turn-log.d.ts +100 -0
  15. package/dist/debug/llm-turn-log.js +130 -0
  16. package/dist/debug/pg-llm-turn-log.d.ts +22 -0
  17. package/dist/debug/pg-llm-turn-log.js +57 -0
  18. package/dist/graph/adapters/langgraph-checkpointer.js +3 -1
  19. package/dist/graph/adapters/langgraph.adapter.d.ts +1 -0
  20. package/dist/graph/adapters/langgraph.adapter.js +147 -91
  21. package/dist/graph/adapters/skill-read.d.ts +12 -0
  22. package/dist/graph/adapters/skill-read.js +63 -0
  23. package/dist/graph/adapters/subgraph-compiler.js +5 -0
  24. package/dist/graph/contracts/executable-graph.d.ts +3 -3
  25. package/dist/graph/hitl/hitl-handler.d.ts +7 -0
  26. package/dist/graph/hitl/hitl-handler.js +47 -6
  27. package/dist/graph/nodes/llm-node.d.ts +14 -0
  28. package/dist/graph/nodes/skill-bind-node.js +1 -2
  29. package/dist/graph/nodes/tool-node.d.ts +2 -0
  30. package/dist/graph/nodes/tool-node.js +24 -9
  31. package/dist/graph/trace-span.d.ts +22 -0
  32. package/dist/graph/trace-span.js +69 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.js +2 -0
  35. package/dist/memory/compression/compression.repository.d.ts +1 -15
  36. package/dist/providers/echo-llm-provider.d.ts +1 -1
  37. package/dist/providers/echo-llm-provider.js +2 -1
  38. package/dist/providers/http-llm-provider.d.ts +4 -0
  39. package/dist/providers/http-llm-provider.js +129 -4
  40. package/dist/providers/local-llm-provider.d.ts +5 -1
  41. package/dist/providers/local-llm-provider.js +14 -6
  42. package/dist/providers/routing-llm-provider.d.ts +4 -0
  43. package/dist/providers/routing-llm-provider.js +12 -2
  44. package/dist/registry/agent-models.d.ts +1 -1
  45. package/dist/registry/agent-models.js +1 -1
  46. package/dist/registry/agent-registry-client.d.ts +2 -2
  47. package/dist/registry/deployment-snapshot.d.ts +4 -4
  48. package/dist/registry/deployment-snapshot.js +50 -28
  49. package/dist/registry/graph-loader.d.ts +1 -1
  50. package/dist/registry/graph-loader.js +39 -76
  51. package/dist/registry/index.d.ts +1 -1
  52. package/dist/registry/index.js +1 -1
  53. package/dist/registry/skill-tree.d.ts +3 -0
  54. package/dist/registry/skill-tree.js +41 -3
  55. package/dist/runtime/agent-runtime.d.ts +42 -3
  56. package/dist/runtime/agent-runtime.js +110 -34
  57. package/dist/runtime/dev-runtime.d.ts +6 -3
  58. package/dist/runtime/dev-runtime.js +21 -14
  59. package/dist/runtime/index.d.ts +3 -3
  60. package/dist/runtime/index.js +1 -1
  61. package/dist/runtime/storage-profile.d.ts +19 -10
  62. package/dist/runtime/storage-profile.js +42 -29
  63. package/dist/runtime/types.d.ts +15 -17
  64. package/dist/server.js +1 -1
  65. package/dist/session/session.service.d.ts +5 -0
  66. package/dist/session/session.service.js +12 -0
  67. package/dist/sse/events/index.d.ts +5 -3
  68. package/dist/sse/events/index.js +2 -0
  69. package/dist/sse/events/skill-events.d.ts +11 -0
  70. package/dist/sse/trace-collector.d.ts +10 -0
  71. package/dist/sse/trace-collector.js +1 -0
  72. package/dist/token/token-budget.service.d.ts +2 -2
  73. package/dist/token/token-budget.service.js +1 -1
  74. package/dist/tool/create-tool-executor.js +3 -2
  75. package/dist/tool/harness-primitives.d.ts +32 -0
  76. package/dist/tool/harness-primitives.js +385 -0
  77. package/dist/tool/http-tool-executor.js +1 -1
  78. package/dist/tool/index.d.ts +2 -1
  79. package/dist/tool/index.js +2 -1
  80. package/dist/tool/local-tool-executor.d.ts +3 -3
  81. package/dist/tool/local-tool-executor.js +5 -7
  82. package/dist/tool/mcp-tool-executor.js +9 -1
  83. package/dist/tool/system-prompt-builder.d.ts +61 -25
  84. package/dist/tool/system-prompt-builder.js +195 -92
  85. package/dist/tool/tool-execution-router.js +1 -1
  86. package/dist/tool/tool-executor.interface.d.ts +12 -6
  87. package/package.json +2 -2
@@ -8,6 +8,7 @@
8
8
  * @see tasks.md Task 11.5
9
9
  */
10
10
  import { BaseCheckpointSaver, } from '@langchain/langgraph';
11
+ import { notifyCheckpointRef, stripCheckpointCollectors, } from '../../checkpoint/checkpoint-ref-notify.js';
11
12
  /**
12
13
  * LangGraphCheckpointer — 桥接 LangGraph BaseCheckpointSaver → ICheckpointSaver
13
14
  *
@@ -42,7 +43,8 @@ export class LangGraphCheckpointer extends BaseCheckpointSaver {
42
43
  async put(config, checkpoint, metadata, _newVersions) {
43
44
  const threadId = this.extractThreadId(config) ?? `thread-${Date.now()}`;
44
45
  const data = this.checkpointToData(threadId, checkpoint, metadata);
45
- await this.saver.save(threadId, data);
46
+ await this.saver.save(threadId, stripCheckpointCollectors(data));
47
+ await notifyCheckpointRef(data);
46
48
  return {
47
49
  ...config,
48
50
  configurable: {
@@ -64,6 +64,7 @@ export declare class LangGraphAdapter implements IStateGraphEngine {
64
64
  /**
65
65
  * Tool 节点执行旁路:started → tool.called + tool.progress;
66
66
  * completed/error → 对应终态 + tool.progress。同一 toolCallId。
67
+ * 读 SKILL.md / 技能包内文件:成功后再发 skill.loaded / skill.resource_read,同时保留 tool.*。
67
68
  */
68
69
  private emitToolExecutionEvents;
69
70
  /**
@@ -18,13 +18,17 @@ import { ToolResultInjector } from '../../tool/tool-result-injector.js';
18
18
  import { fileArtifactFromToolOutput } from '../../tool/file-artifact.js';
19
19
  import { EchoLLMProvider } from '../../providers/echo-llm-provider.js';
20
20
  import { ConditionEvaluator } from '../condition-evaluator.js';
21
- import { SystemPromptBuilder, loadedToolsToApiTools, filterLoadedToolsForSkill } from '../../tool/system-prompt-builder.js';
21
+ import { SystemPromptBuilder, buildToolCatalog, loadedToolsToApiTools } from '../../tool/system-prompt-builder.js';
22
+ import { resolveJailRoots } from '../../tool/harness-primitives.js';
23
+ import os from 'node:os';
22
24
  import { bindSelectedSkill } from '../nodes/skill-bind-node.js';
23
25
  import { resolveGraphPrompt } from '../resolve-graph-prompt.js';
24
26
  import { SkillAuditRecorder } from '../skill-audit-recorder.js';
25
27
  import { preloadSkillNotFound } from '../../registry/preload-errors.js';
26
28
  import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
27
29
  import { isHitlDecision, lastAssistantHasUnansweredToolCalls } from '../hitl/hitl-handler.js';
30
+ import { classifySkillRead } from './skill-read.js';
31
+ import { withGraphSpan } from '../trace-span.js';
28
32
  // ─────────────────────────────────────────────
29
33
  // LangGraphAdapter
30
34
  // ─────────────────────────────────────────────
@@ -148,6 +152,7 @@ export class LangGraphAdapter {
148
152
  /**
149
153
  * Tool 节点执行旁路:started → tool.called + tool.progress;
150
154
  * completed/error → 对应终态 + tool.progress。同一 toolCallId。
155
+ * 读 SKILL.md / 技能包内文件:成功后再发 skill.loaded / skill.resource_read,同时保留 tool.*。
151
156
  */
152
157
  emitToolExecutionEvents(progress) {
153
158
  if (!this.streamSink)
@@ -158,6 +163,30 @@ export class LangGraphAdapter {
158
163
  ...(progress.node ? { node: progress.node } : {}),
159
164
  ...(progress.skillId ? { skillId: progress.skillId } : {}),
160
165
  };
166
+ const skillRead = classifySkillRead(progress.toolName, progress.arguments, progress.skillDirs);
167
+ if (skillRead && progress.status === 'completed') {
168
+ if (skillRead.kind === 'loaded') {
169
+ this.streamSink({
170
+ type: 'skill.loaded',
171
+ taskId,
172
+ correlationId,
173
+ timestamp,
174
+ payload: {
175
+ skillId: skillRead.skillId || loc.skillId || skillRead.path,
176
+ path: skillRead.path,
177
+ },
178
+ });
179
+ }
180
+ else {
181
+ this.streamSink({
182
+ type: 'skill.resource_read',
183
+ taskId,
184
+ correlationId,
185
+ timestamp,
186
+ payload: { skillId: skillRead.skillId || loc.skillId, path: skillRead.path },
187
+ });
188
+ }
189
+ }
161
190
  if (progress.status === 'started') {
162
191
  this.streamSink({
163
192
  type: 'tool.called',
@@ -274,7 +303,6 @@ export class LangGraphAdapter {
274
303
  activeSkillPrompt: ephemeral(undefined),
275
304
  activatedSkill: ephemeral(undefined),
276
305
  skillRoute: ephemeral(undefined),
277
- skillSelectRaw: ephemeral(undefined),
278
306
  silentOutput: ephemeral(undefined),
279
307
  promptRoots: ephemeral(undefined),
280
308
  executionContext: ephemeral(undefined),
@@ -282,6 +310,7 @@ export class LangGraphAdapter {
282
310
  traceCollector: ephemeral(undefined),
283
311
  traceId: ephemeral(undefined),
284
312
  skillSpanId: ephemeral(undefined),
313
+ lastSpanId: ephemeral(undefined),
285
314
  hitlChoice: ephemeral(undefined),
286
315
  // ── LLM Token 使用量:数组累加器 reducer,每次 LLM 节点追加一条使用明细
287
316
  __llmTokenUsage: {
@@ -546,25 +575,22 @@ export class LangGraphAdapter {
546
575
  case 'tool':
547
576
  return this.createToolNodeFunction(node);
548
577
  case 'condition':
549
- return async (state) => {
550
- // 条件节点:根据 config 中的 conditionExpr 评估并写入 __conditionResult
578
+ return async (state) => withGraphSpan(state, { name: node.id, type: 'condition' }, async () => {
551
579
  const conditionExpr = node.config?.['conditionExpr']
552
580
  ?? node.config?.['conditionFn'];
553
581
  if (conditionExpr) {
554
582
  const branchValues = node.config?.['branchValues']
555
583
  ?? this.extractBranchValuesFromDsl(node.id);
556
584
  const result = await this.conditionEvaluator.evaluate(conditionExpr, state, branchValues, node.config?.['fallback'] ?? '__default__');
557
- return { ...state, currentNode: node.id, __conditionResult: result };
585
+ return { currentNode: node.id, __conditionResult: result };
558
586
  }
559
- // 无表达式时透传(由 addConditionalEdge 自行处理)
560
- return { ...state, currentNode: node.id };
561
- };
587
+ return { currentNode: node.id };
588
+ });
562
589
  case 'subgraph':
563
590
  return this.createSubGraphNodeFunction(node);
564
591
  case 'human':
565
592
  return async (state) => {
566
- // HITL 节点:暂停等待人工介入(Task 14)
567
- return { ...state, currentNode: node.id };
593
+ return { currentNode: node.id };
568
594
  };
569
595
  case 'parallel':
570
596
  return this.createParallelNodeFunction(node);
@@ -639,11 +665,14 @@ export class LangGraphAdapter {
639
665
  return undefined;
640
666
  const rows = Object.values(defs).map((skill) => {
641
667
  const desc = (skill.description ?? '').replace(/\s+/g, ' ').trim();
642
- return `- ${skill.skillId}${skill.name && skill.name !== skill.skillId ? `(${skill.name})` : ''}${desc ? `:${desc}` : ''}`;
668
+ const entry = skill.entryFile ?? (skill.bundlePath ? `${skill.bundlePath}/SKILL.md` : '');
669
+ const title = skill.name && skill.name !== skill.skillId ? `${skill.skillId}(${skill.name})` : skill.skillId;
670
+ const pathLine = entry ? `\n SKILL.md: ${entry}` : '';
671
+ return `- **${title}**${desc ? `:${desc}` : ''}${pathLine}`;
643
672
  });
644
673
  if (rows.length === 0)
645
674
  return undefined;
646
- return `# 可用技能\n下列技能已装配,引用时使用 skillId 或 name。\n${rows.join('\n')}`;
675
+ return `# 可用技能\n要用某个技能时,调用 read_file 读取其 SKILL.md。脚本和参考文档默认在同一目录。\n${rows.join('\n')}`;
647
676
  }
648
677
  createToolNodeFunction(node) {
649
678
  const toolName = node.config?.['tool'] ?? node.id;
@@ -672,6 +701,10 @@ export class LangGraphAdapter {
672
701
  arguments: defaultArgs,
673
702
  node: node.id,
674
703
  });
704
+ const skillDefs = state['skillDefinitions'];
705
+ const jailExtraRoots = Object.values(skillDefs ?? {})
706
+ .map((skill) => skill.bundlePath)
707
+ .filter((p) => Boolean(p));
675
708
  const result = await this.toolExecutor.execute({
676
709
  toolCallId,
677
710
  toolName,
@@ -679,16 +712,22 @@ export class LangGraphAdapter {
679
712
  authContext: { userId, delegationToken, agentCode },
680
713
  correlationId,
681
714
  taskId,
715
+ ...(jailExtraRoots.length > 0 ? { jailExtraRoots } : {}),
682
716
  });
683
717
  const executionTime = Date.now() - startedAt;
718
+ const skillDirs = Object.values(skillDefs ?? {})
719
+ .filter((skill) => Boolean(skill.bundlePath))
720
+ .map((skill) => ({ skillId: skill.skillId, bundlePath: skill.bundlePath }));
684
721
  this.emitToolExecutionEvents({
685
722
  toolCallId: result.toolCallId,
686
723
  toolName: result.toolName,
687
724
  status: result.success ? 'completed' : 'error',
725
+ arguments: defaultArgs,
688
726
  content: result.content,
689
727
  error: result.error,
690
728
  executionTime,
691
729
  node: node.id,
730
+ ...(skillDirs.length > 0 ? { skillDirs } : {}),
692
731
  });
693
732
  // 重要:messages channel 的 reducer 是 append 语义,只返回新增消息
694
733
  return {
@@ -733,8 +772,8 @@ export class LangGraphAdapter {
733
772
  toolChoice: node.config?.['toolChoice'],
734
773
  };
735
774
  const disableTools = node.config?.['disableTools'] === true;
736
- const includeSkillCatalog = node.config?.['includeSkillCatalog'] === true;
737
775
  const silent = node.config?.['silent'] === true;
776
+ const includeSkillCatalog = node.config?.['includeSkillCatalog'] !== false;
738
777
  return async (state) => {
739
778
  // HITL 续跑:已有未闭合 toolCalls + hitlChoice 时不要再调 LLM(否则会换新 toolCallId)
740
779
  const hitlChoice = state['hitlChoice'];
@@ -745,92 +784,109 @@ export class LangGraphAdapter {
745
784
  lastAssistantHasUnansweredToolCalls(rawMessages)) {
746
785
  return { currentNode: node.id };
747
786
  }
748
- const promptRoots = Array.isArray(state['promptRoots']) ? state['promptRoots'] : [];
749
- const resolvedPrompt = resolveGraphPrompt(baseConfig.systemPrompt, promptRoots);
750
- const loadedTools = state['loadedTools'];
751
- // 旧 Checkpoint 兼容:agentId 仅作读兜底
752
- const agentCode = (state['agentCode'] ?? state['agentId']);
753
- const activeSkillPrompt = state['activeSkillPrompt'];
754
- const skillDefs = state['skillDefinitions'];
755
- const activatedSkillId = state['activatedSkill'];
756
- // [DEBUG] 打印 loadedTools 实际状态
757
- if (process.env.EAP_LLM_DEBUG === '1') {
758
- const toolNames = loadedTools ? Object.keys(loadedTools) : [];
759
- console.log(`[LLM-DEBUG] node=${node.id} loadedTools=${toolNames.length}:`, toolNames);
760
- }
761
- const catalog = includeSkillCatalog ? this.buildSkillCatalog(skillDefs) : undefined;
762
- const developerPrompt = [catalog, activeSkillPrompt, resolvedPrompt]
763
- .filter((s) => Boolean(s && s.trim()))
764
- .join('\n\n');
765
- const execCtx = state['executionContext'];
766
- const userId = (typeof execCtx?.userId === 'string' && execCtx.userId) || state['userId'];
767
- // 1. 构建 system prompt(平台规则 + 技能提示 + 开发者提示词)
768
- const builtPrompt = this.systemPromptBuilder.build({
769
- developerPrompt: developerPrompt || undefined,
770
- agentCode,
771
- userId,
772
- });
773
- // 2. 工具走 API 的 tools 参数,不塞进 system prompt
774
- const scopedTools = activatedSkillId && skillDefs
775
- ? filterLoadedToolsForSkill(loadedTools, skillDefs[activatedSkillId])
776
- : loadedTools;
777
- const apiTools = disableTools ? [] : loadedToolsToApiTools(scopedTools);
778
- const finalConfig = {
779
- ...baseConfig,
780
- systemPrompt: builtPrompt,
781
- tools: disableTools ? undefined : (apiTools.length > 0 ? apiTools : baseConfig.tools),
782
- toolChoice: disableTools ? 'none' : baseConfig.toolChoice,
783
- };
784
- // 流式旁路:有 streamSink 时启用 LLM token 推送;silent 节点不推给用户
785
- const enableStream = Boolean(this.streamSink) && !silent;
786
- this.reasoningStartedThisTurn = false;
787
- this.streamedTextThisTurn = false;
788
- const llmNode = new LLMNode(node.id, finalConfig, this.llmProvider, enableStream, enableStream
789
- ? (text) => {
790
- if (!this.streamSink || !text)
791
- return;
792
- const timestamp = new Date().toISOString();
793
- if (!this.reasoningStartedThisTurn) {
794
- this.reasoningStartedThisTurn = true;
795
- this.streamIteration += 1;
787
+ return withGraphSpan(state, { name: node.id, type: 'llm' }, async () => {
788
+ const promptRoots = Array.isArray(state['promptRoots']) ? state['promptRoots'] : [];
789
+ const resolvedPrompt = resolveGraphPrompt(baseConfig.systemPrompt, promptRoots);
790
+ const loadedTools = state['loadedTools'];
791
+ const agentCode = typeof state['agentCode'] === 'string' ? state['agentCode'] : undefined;
792
+ const skillDefs = state['skillDefinitions'];
793
+ // [DEBUG] 打印 loadedTools 实际状态
794
+ if (process.env.EAP_LLM_DEBUG === '1') {
795
+ const toolNames = loadedTools ? Object.keys(loadedTools) : [];
796
+ console.log(`[LLM-DEBUG] node=${node.id} loadedTools=${toolNames.length}:`, toolNames);
797
+ }
798
+ const catalog = includeSkillCatalog ? this.buildSkillCatalog(skillDefs) : undefined;
799
+ const toolCatalog = disableTools ? undefined : buildToolCatalog(loadedTools);
800
+ const execCtx = state['executionContext'];
801
+ const userId = (typeof execCtx?.userId === 'string' && execCtx.userId) || state['userId'];
802
+ const workingDirectory = typeof process.cwd === 'function' ? process.cwd() : undefined;
803
+ const skillDirs = Object.values(skillDefs ?? {})
804
+ .filter((skill) => Boolean(skill.bundlePath))
805
+ .map((skill) => ({
806
+ skillId: skill.skillId,
807
+ bundlePath: skill.bundlePath,
808
+ entryFile: skill.entryFile,
809
+ }));
810
+ const jailRoots = resolveJailRoots({
811
+ workdir: workingDirectory,
812
+ extraRoots: skillDirs.map((s) => s.bundlePath),
813
+ });
814
+ const assembled = this.systemPromptBuilder.build({
815
+ developerPrompt: resolvedPrompt || undefined,
816
+ skillCatalog: catalog,
817
+ toolCatalog,
818
+ agentCode,
819
+ userId,
820
+ workingDirectory,
821
+ os: { platform: os.platform(), release: os.release() },
822
+ jailRoots,
823
+ skillDirs,
824
+ });
825
+ const apiTools = disableTools ? [] : loadedToolsToApiTools(loadedTools);
826
+ const finalConfig = {
827
+ ...baseConfig,
828
+ systemPrompt: assembled.text,
829
+ systemSections: assembled.sections,
830
+ turn: {
831
+ sessionId: typeof state['sessionId'] === 'string' ? state['sessionId'] : undefined,
832
+ taskId: typeof state['taskId'] === 'string' ? state['taskId'] : undefined,
833
+ nodeId: node.id,
834
+ agentCode,
835
+ correlationId: typeof state['correlationId'] === 'string' ? state['correlationId'] : undefined,
836
+ },
837
+ tools: disableTools ? undefined : (apiTools.length > 0 ? apiTools : baseConfig.tools),
838
+ toolChoice: disableTools ? 'none' : baseConfig.toolChoice,
839
+ };
840
+ // 流式旁路:有 streamSink 时启用 LLM token 推送;silent 节点不推给用户
841
+ const enableStream = Boolean(this.streamSink) && !silent;
842
+ this.reasoningStartedThisTurn = false;
843
+ this.streamedTextThisTurn = false;
844
+ const llmNode = new LLMNode(node.id, finalConfig, this.llmProvider, enableStream, enableStream
845
+ ? (text) => {
846
+ if (!this.streamSink || !text)
847
+ return;
848
+ const timestamp = new Date().toISOString();
849
+ if (!this.reasoningStartedThisTurn) {
850
+ this.reasoningStartedThisTurn = true;
851
+ this.streamIteration += 1;
852
+ this.streamSink({
853
+ type: 'reasoning.started',
854
+ taskId: undefined,
855
+ correlationId: undefined,
856
+ timestamp,
857
+ payload: {
858
+ iterationCount: this.streamIteration,
859
+ model: finalConfig.model,
860
+ },
861
+ });
862
+ }
863
+ this.streamedTextThisTurn = true;
796
864
  this.streamSink({
797
- type: 'reasoning.started',
865
+ type: 'response.chunk',
798
866
  taskId: undefined,
799
867
  correlationId: undefined,
800
868
  timestamp,
801
869
  payload: {
802
- iterationCount: this.streamIteration,
803
- model: finalConfig.model,
870
+ content: text,
871
+ index: this.streamChunkIndex++,
872
+ node: node.id,
804
873
  },
805
874
  });
806
875
  }
807
- this.streamedTextThisTurn = true;
808
- this.streamSink({
809
- type: 'response.chunk',
810
- taskId: undefined,
811
- correlationId: undefined,
812
- timestamp,
813
- payload: {
814
- content: text,
815
- index: this.streamChunkIndex++,
816
- node: node.id,
817
- },
818
- });
819
- }
820
- : undefined);
821
- const next = await llmNode.execute(state);
822
- if (!silent)
823
- return next;
824
- const added = Array.isArray(next.messages) ? next.messages[0] : undefined;
825
- const raw = added && typeof added === 'object' && typeof added.content === 'string'
826
- ? added.content
827
- : '';
828
- return {
829
- ...next,
830
- messages: [],
831
- silentOutput: raw,
832
- skillSelectRaw: raw,
833
- };
876
+ : undefined);
877
+ const next = await llmNode.execute(state);
878
+ if (!silent)
879
+ return next;
880
+ const added = Array.isArray(next.messages) ? next.messages[0] : undefined;
881
+ const raw = added && typeof added === 'object' && typeof added.content === 'string'
882
+ ? added.content
883
+ : '';
884
+ return {
885
+ ...next,
886
+ messages: [],
887
+ silentOutput: raw,
888
+ };
889
+ });
834
890
  };
835
891
  }
836
892
  /**
@@ -0,0 +1,12 @@
1
+ export interface SkillDirRef {
2
+ skillId: string;
3
+ bundlePath: string;
4
+ }
5
+ /**
6
+ * 判断 harness 读操作是否命中技能包,并用 bundlePath 最长前缀解析 skillId。
7
+ */
8
+ export declare function classifySkillRead(toolName: string, args: Record<string, unknown> | undefined, skillDirs?: SkillDirRef[]): {
9
+ kind: 'loaded' | 'resource';
10
+ path: string;
11
+ skillId: string;
12
+ } | null;
@@ -0,0 +1,63 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ function tryRealpath(p) {
4
+ try {
5
+ return realpathSync(p);
6
+ }
7
+ catch {
8
+ return undefined;
9
+ }
10
+ }
11
+ function normalizeForMatch(p) {
12
+ const resolved = path.resolve(p);
13
+ return (tryRealpath(resolved) ?? resolved).replace(/\\/g, '/').replace(/\/+$/, '');
14
+ }
15
+ function longestSkillMatch(filePath, skillDirs) {
16
+ if (!skillDirs?.length)
17
+ return undefined;
18
+ const target = normalizeForMatch(filePath);
19
+ let best;
20
+ for (const dir of skillDirs) {
21
+ if (!dir.bundlePath)
22
+ continue;
23
+ const root = normalizeForMatch(dir.bundlePath);
24
+ if (target === root || target.startsWith(`${root}/`)) {
25
+ if (!best || root.length > best.len) {
26
+ best = { skillId: dir.skillId, bundlePath: root, len: root.length };
27
+ }
28
+ }
29
+ }
30
+ return best;
31
+ }
32
+ function skillIdFromSkillsSegment(norm) {
33
+ const match = norm.match(/(?:^|\/)(?:preinstall\/)?skills\/([^/]+)(?:\/|$)/i);
34
+ return match?.[1] ?? '';
35
+ }
36
+ /**
37
+ * 判断 harness 读操作是否命中技能包,并用 bundlePath 最长前缀解析 skillId。
38
+ */
39
+ export function classifySkillRead(toolName, args, skillDirs) {
40
+ const n = toolName.toLowerCase().replace(/-/g, '_');
41
+ if (n !== 'read_file' && n !== 'read_skill_file' && n !== 'readfile')
42
+ return null;
43
+ const filePath = String(args?.path ?? args?.file ?? '');
44
+ if (!filePath)
45
+ return null;
46
+ const norm = filePath.replace(/\\/g, '/');
47
+ const matched = longestSkillMatch(filePath, skillDirs);
48
+ const isEntry = /(^|\/)(SKILL\.md|skill\.md)$/i.test(norm);
49
+ if (matched) {
50
+ return {
51
+ kind: isEntry ? 'loaded' : 'resource',
52
+ path: filePath,
53
+ skillId: matched.skillId,
54
+ };
55
+ }
56
+ if (isEntry) {
57
+ return { kind: 'loaded', path: filePath, skillId: skillIdFromSkillsSegment(norm) };
58
+ }
59
+ if (/(^|\/)(?:preinstall\/)?skills\//i.test(norm)) {
60
+ return { kind: 'resource', path: filePath, skillId: skillIdFromSkillsSegment(norm) };
61
+ }
62
+ return null;
63
+ }
@@ -138,6 +138,7 @@ export class SubGraphCompiler {
138
138
  traceId,
139
139
  traceCollector,
140
140
  skillSpanId: skillSpanId ?? undefined,
141
+ lastSpanId: skillSpanId ?? state['lastSpanId'],
141
142
  hitlChoice: state['hitlChoice'],
142
143
  hitlRespondedBy: state['hitlRespondedBy'],
143
144
  };
@@ -185,9 +186,13 @@ export class SubGraphCompiler {
185
186
  ? [{ role: 'assistant', content: result.output }]
186
187
  : [];
187
188
  const perNode = result.tokenUsage?.perNode;
189
+ const nestedLastSpanId = (typeof result.finalState?.lastSpanId === 'string' && result.finalState.lastSpanId) ||
190
+ skillSpanId ||
191
+ undefined;
188
192
  return {
189
193
  messages: outputMessages,
190
194
  currentNode: `subgraph:${dsl.name}`,
195
+ ...(nestedLastSpanId ? { lastSpanId: nestedLastSpanId } : {}),
191
196
  ...(Array.isArray(perNode) && perNode.length > 0
192
197
  ? {
193
198
  __llmTokenUsage: perNode.map((u) => ({
@@ -185,7 +185,7 @@ export interface GraphState {
185
185
  forceToolServer?: string[];
186
186
  /** GraphLoader 注入:toolName → 运行期 manifest 引用 */
187
187
  loadedTools?: import('../../tool/loaded-tool-definition.js').LoadedToolMap;
188
- /** GraphLoader 注入:已装配 Skill 列表(兼容) */
188
+ /** GraphLoader 注入:已装配 Skill 列表 */
189
189
  loadedSkills?: import('../../tool/system-prompt-builder.js').LoadedSkillInfo[];
190
190
  /** GraphLoader 注入:skillId → 完整定义(子图解析 / prompt) */
191
191
  skillDefinitions?: import('../../tool/system-prompt-builder.js').LoadedSkillMap;
@@ -193,8 +193,6 @@ export interface GraphState {
193
193
  activeSkillPrompt?: string;
194
194
  /** silent LLM 的原始输出;不入对话 messages */
195
195
  silentOutput?: string;
196
- /** @deprecated 同 silentOutput,兼容旧图 */
197
- skillSelectRaw?: string;
198
196
  /** 解析 graph 内 ./prompts/*.md 的根目录(agentDir + 工程根) */
199
197
  promptRoots?: string[];
200
198
  activatedSkill?: string | null;
@@ -207,6 +205,8 @@ export interface GraphState {
207
205
  traceCollector?: import('../../contracts/trace-collector.js').ITraceCollector;
208
206
  /** 当前 Skill 子图 createNode 返回的 span id;子图内 Tool span 的 parentNodeId */
209
207
  skillSpanId?: string;
208
+ /** 最近一次 createNode 的 span id;Checkpoint 引用挂在此 Span 上 */
209
+ lastSpanId?: string;
210
210
  /**
211
211
  * HITL 续跑:入口 messages 以 __set 覆盖 checkpoint 历史,避免 append 重复。
212
212
  * 仅 stream/invoke 入口使用,不入持久化语义。
@@ -11,6 +11,7 @@
11
11
  * @see tasks.md Task 14
12
12
  */
13
13
  import type { ICheckpointSaver } from '../../checkpoint/contracts/checkpoint-saver.contract.js';
14
+ import type { ITraceCollector } from '../../contracts/trace-collector.js';
14
15
  import type { ISSEEmitter } from '../../sse/contracts/sse-emitter.contract.js';
15
16
  import type { GraphState } from '../contracts/executable-graph.js';
16
17
  /** HITL 默认等待时长(毫秒)= 300 秒。SSE `hitl.waited.timeout` 对外用秒。 */
@@ -51,6 +52,7 @@ export interface HitlWaitingState {
51
52
  toolName?: string;
52
53
  toolSpanId?: string;
53
54
  skillSpanId?: string;
55
+ hitlSpanId?: string;
54
56
  }
55
57
  /** HITL 超时错误(保留类型;timer 不再 throw,resume 逾期改走 AppError) */
56
58
  export declare class HitlTimeoutError extends Error {
@@ -112,6 +114,7 @@ export interface PersistedHitlWaiting {
112
114
  toolName?: string;
113
115
  toolSpanId?: string;
114
116
  skillSpanId?: string;
117
+ hitlSpanId?: string;
115
118
  /** error/cancel 超时已占用此等待;resume 必须拒绝 */
116
119
  timedOut?: boolean;
117
120
  }
@@ -162,8 +165,12 @@ export declare class HitlHandler {
162
165
  private timeoutListener?;
163
166
  /** 同一 taskId 上串行化 resume 与 timeout,避免双边占用 */
164
167
  private taskLocks;
168
+ private traceCollector?;
169
+ /** 进程内 interrupt 的 collector,不进 Checkpoint */
170
+ private readonly waitingCollectors;
165
171
  constructor(checkpointSaver: ICheckpointSaver, sseEmitter: ISSEEmitter, timeoutListener?: HitlTimeoutListener);
166
172
  setTimeoutListener(listener: HitlTimeoutListener | undefined): void;
173
+ setTraceCollector(collector: ITraceCollector | undefined): void;
167
174
  /**
168
175
  * 中断 Graph 执行,等待人工确认
169
176
  *