@gt-fe/eap-runtime 0.0.29 → 0.0.31

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 (39) hide show
  1. package/dist/api/handlers/invoke.handler.js +9 -5
  2. package/dist/api/handlers/stream.handler.js +15 -4
  3. package/dist/common/index.d.ts +1 -0
  4. package/dist/common/index.js +1 -0
  5. package/dist/common/user-root.d.ts +8 -0
  6. package/dist/common/user-root.js +19 -0
  7. package/dist/graph/adapters/langgraph.adapter.js +193 -134
  8. package/dist/graph/adapters/skill-read.js +13 -4
  9. package/dist/graph/contracts/executable-graph.d.ts +7 -1
  10. package/dist/graph/hitl/hitl-handler.js +6 -0
  11. package/dist/graph/nodes/tool-node.d.ts +4 -1
  12. package/dist/graph/nodes/tool-node.js +15 -1
  13. package/dist/graph/trace-span.d.ts +15 -1
  14. package/dist/graph/trace-span.js +69 -1
  15. package/dist/registry/deployment-snapshot.d.ts +1 -1
  16. package/dist/registry/deployment-snapshot.js +6 -3
  17. package/dist/registry/skill-tree.d.ts +1 -1
  18. package/dist/registry/skill-tree.js +11 -6
  19. package/dist/runtime/agent-runtime.js +36 -5
  20. package/dist/session/persist-turn-messages.d.ts +13 -6
  21. package/dist/session/persist-turn-messages.js +4 -0
  22. package/dist/sse/events/tool-events.d.ts +29 -0
  23. package/dist/tool/harness-command.d.ts +1 -1
  24. package/dist/tool/harness-command.js +8 -7
  25. package/dist/tool/harness-primitives.d.ts +3 -3
  26. package/dist/tool/harness-primitives.js +185 -60
  27. package/dist/tool/index.d.ts +2 -0
  28. package/dist/tool/index.js +2 -0
  29. package/dist/tool/inject-public-artifact-links.d.ts +32 -0
  30. package/dist/tool/inject-public-artifact-links.js +142 -0
  31. package/dist/tool/static-artifact-url.d.ts +66 -0
  32. package/dist/tool/static-artifact-url.js +561 -0
  33. package/dist/tool/system-prompt-builder.js +8 -4
  34. package/dist/tool/tool-event-emitter.d.ts +2 -1
  35. package/dist/tool/tool-event-emitter.js +30 -18
  36. package/dist/tool/tool-executor.interface.d.ts +8 -1
  37. package/dist/tool/tool-result-injector.d.ts +3 -3
  38. package/dist/tool/tool-result-injector.js +7 -3
  39. package/package.json +2 -2
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { randomUUID } from 'node:crypto';
18
18
  import { buildHitlWaitingTaskOutput, loadModelFacingHistory, messageRecordsToGraphMessages, persistGraphMessageBaseline, turnGraphMessagesAfterCompression, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, resolveCheckpointThreadId, sessionVisibleGraphMessages, } from '../../session/persist-turn-messages.js';
19
+ import { applyInjectedAssistant, collectPublicArtifactLinks, injectPublicArtifactLinks, } from '../../tool/inject-public-artifact-links.js';
19
20
  import { SessionBudgetExhaustedError } from '../../token/session-budget-checker.js';
20
21
  import { TokenBudgetExhaustedError } from '../../token/token-budget-guard.js';
21
22
  import { recordTaskTokenUsages, tokenUsagesFromLlmChannel, } from '../../token/record-task-token-usage.js';
@@ -380,21 +381,24 @@ export class InvokeOrchestrator {
380
381
  },
381
382
  ]);
382
383
  }
383
- // ─── Step 7: 完成 Task (running → success) ───
384
- await this.taskService.updateStatus(task.id, 'success', graphResult.output);
385
384
  // ─── Step 7b: 持久化本轮 transcript(统一顺序:assistant tool_calls → tool → assistant 文本)───
386
385
  // 切点是入图 messages 基线(压缩 `__set` 后 Graph 短于 Session 全文)。user 已在 4b 写入。
387
386
  const allMessages = Array.isArray(graphResult.finalState?.messages)
388
387
  ? graphResult.finalState.messages
389
388
  : [];
390
389
  const newMessages = sessionVisibleGraphMessages(allMessages.slice(persistBaseline), finishPoints);
390
+ const links = collectPublicArtifactLinks(allMessages);
391
+ const { injected } = injectPublicArtifactLinks(graphResult.output ?? '', links);
392
+ const persisted = applyInjectedAssistant(newMessages, injected, links);
393
+ // ─── Step 7: 完成 Task (running → success) ───
394
+ await this.taskService.updateStatus(task.id, 'success', injected);
391
395
  const duration = Date.now() - startTime;
392
396
  await persistTurnTranscript(this.messageService, {
393
397
  sessionId,
394
398
  taskId: task.id,
395
399
  correlationId,
396
- messages: newMessages,
397
- fallbackAssistantContent: graphResult.output ?? '',
400
+ messages: persisted,
401
+ fallbackAssistantContent: injected,
398
402
  assistantTokenCount: tokenUsage.output,
399
403
  assistantDurationMs: duration,
400
404
  });
@@ -404,7 +408,7 @@ export class InvokeOrchestrator {
404
408
  return this.buildResponse(traceId, {
405
409
  taskId: task.id,
406
410
  sessionId,
407
- output: graphResult.output,
411
+ output: injected,
408
412
  tokenUsage,
409
413
  duration,
410
414
  status: 'success',
@@ -20,6 +20,7 @@
20
20
  * @see requirements.md Requirement 11 AC2
21
21
  */
22
22
  import { buildHitlWaitingTaskOutput, buildTurnTranscriptFromStreamEvents, loadModelFacingHistory, turnGraphMessagesAfterCompression, extractStreamReasoning, applyStreamTextDelta, createStreamAssistantBuckets, visibleStreamAssistantText, streamAssistantTexts, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, appendTerminalToolResult, } from '../../session/persist-turn-messages.js';
23
+ import { collectPublicArtifactLinks, collectPublicArtifactLinksFromEvent, injectPublicArtifactLinks, mergeArtifactLinks, } from '../../tool/inject-public-artifact-links.js';
23
24
  import { SessionBudgetExhaustedError } from '../../token/session-budget-checker.js';
24
25
  import { TokenBudgetExhaustedError } from '../../token/token-budget-guard.js';
25
26
  import { recordTaskTokenUsages } from '../../token/record-task-token-usage.js';
@@ -268,6 +269,7 @@ export class StreamOrchestrator {
268
269
  let assistantReasoningContent;
269
270
  const pendingToolCalls = [];
270
271
  const toolResults = [];
272
+ const artifactLinks = [];
271
273
  // ─── Step 6: Graph.stream() 流式执行 ───
272
274
  try {
273
275
  const eventStream = runWithExecutionTrace({ abortSignal }, () => this.graphEngine.stream(graph, {
@@ -326,6 +328,7 @@ export class StreamOrchestrator {
326
328
  }
327
329
  }
328
330
  appendTerminalToolResult(runtimeEvent.type, runtimeEvent.payload, toolResults);
331
+ collectPublicArtifactLinksFromEvent(runtimeEvent.type, runtimeEvent.payload, artifactLinks);
329
332
  const streamedReasoning = extractStreamReasoning(runtimeEvent.type, runtimeEvent.payload);
330
333
  if (streamedReasoning)
331
334
  assistantReasoningContent = streamedReasoning;
@@ -387,10 +390,18 @@ export class StreamOrchestrator {
387
390
  this.timeoutManager?.clearTimer(taskId);
388
391
  }
389
392
  // ─── Step 7: 发送结束事件序列 ───
390
- // Emit: response.final(带完整 assistant 文本,供客户端兜底)
391
393
  const tokenStats = this.tokenStatsService.getTaskStats(taskId);
394
+ const visible = visibleStreamAssistantText(assistantBuckets);
395
+ const links = mergeArtifactLinks(artifactLinks, collectPublicArtifactLinks(toolResults));
396
+ const { injected, suffix } = injectPublicArtifactLinks(visible, links);
397
+ if (suffix) {
398
+ emitter.emit(this.buildSSEEvent('response.chunk', correlationId, taskId, {
399
+ content: suffix,
400
+ format: 'text',
401
+ }));
402
+ }
392
403
  emitter.emit(this.buildSSEEvent('response.final', correlationId, taskId, {
393
- content: visibleStreamAssistantText(assistantBuckets),
404
+ content: injected,
394
405
  format: 'text',
395
406
  }));
396
407
  const totalTokens = (tokenStats.total.input ?? 0) + (tokenStats.total.output ?? 0);
@@ -424,8 +435,8 @@ export class StreamOrchestrator {
424
435
  messages: buildTurnTranscriptFromStreamEvents({
425
436
  pendingToolCalls,
426
437
  toolResults,
427
- assistantTexts: streamAssistantTexts(assistantBuckets),
428
- assistantText: visibleStreamAssistantText(assistantBuckets),
438
+ assistantTexts: suffix ? undefined : streamAssistantTexts(assistantBuckets),
439
+ assistantText: injected,
429
440
  finishPoints: graph.finishPoints,
430
441
  toolCallReasoningContent,
431
442
  assistantReasoningContent,
@@ -4,3 +4,4 @@ export * from './health.js';
4
4
  export * from './agent-code.js';
5
5
  export * from './trace-id.js';
6
6
  export { RUNTIME_PACKAGE_VERSION } from './runtime-package-version.js';
7
+ export { userRoot, USERS_DIR_NAME, MISSING_USER_DIR } from './user-root.js';
@@ -4,3 +4,4 @@ export * from './health.js';
4
4
  export * from './agent-code.js';
5
5
  export * from './trace-id.js';
6
6
  export { RUNTIME_PACKAGE_VERSION } from './runtime-package-version.js';
7
+ export { userRoot, USERS_DIR_NAME, MISSING_USER_DIR } from './user-root.js';
@@ -0,0 +1,8 @@
1
+ export declare const MISSING_USER_DIR = "\u7F3A\u5C11\u7528\u6237\u76EE\u5F55\uFF0C\u62D2\u7EDD\u8BE5\u64CD\u4F5C";
2
+ /** `agents/<code>/users/`:与 `<ver>` 平级,不是版本目录。 */
3
+ export declare const USERS_DIR_NAME = "users";
4
+ /**
5
+ * jail 写根:`dirname(agentDir)/users/<userId>`。
6
+ * `agentDir` 仍是版本目录 `agents/<code>/<ver>`。
7
+ */
8
+ export declare function userRoot(agentDir: string, userId: string): string;
@@ -0,0 +1,19 @@
1
+ import path from 'node:path';
2
+ export const MISSING_USER_DIR = '缺少用户目录,拒绝该操作';
3
+ /** `agents/<code>/users/`:与 `<ver>` 平级,不是版本目录。 */
4
+ export const USERS_DIR_NAME = 'users';
5
+ /**
6
+ * jail 写根:`dirname(agentDir)/users/<userId>`。
7
+ * `agentDir` 仍是版本目录 `agents/<code>/<ver>`。
8
+ */
9
+ export function userRoot(agentDir, userId) {
10
+ const dir = agentDir.trim();
11
+ const uid = userId.trim();
12
+ if (!dir || !uid) {
13
+ throw new Error(MISSING_USER_DIR);
14
+ }
15
+ if (uid === '.' || uid === '..' || /[\\/]/.test(uid)) {
16
+ throw new Error('非法用户标识');
17
+ }
18
+ return path.resolve(path.dirname(dir), USERS_DIR_NAME, uid);
19
+ }
@@ -36,7 +36,7 @@ import { pickContextFields } from '../../token/context-window.js';
36
36
  import { isHitlDecision, lastAssistantHasUnansweredToolCalls } from '../hitl/hitl-handler.js';
37
37
  import { classifySkillRead } from './skill-read.js';
38
38
  import { skillEntryInvocationFromTool } from './skill-entry-span.js';
39
- import { withGraphSpan } from '../trace-span.js';
39
+ import { llmSpanInput, llmSpanOutputFromResult, withGraphSpan } from '../trace-span.js';
40
40
  import { executionTraceFromState, getExecutionStream, getExecutionTrace, runWithExecutionTrace, } from '../execution-trace-context.js';
41
41
  import { isRetryableToolFailure, resolveToolRetryConfig, sleepToolRetry, toolRetryDelayMs, } from '../../tool/tool-retry.js';
42
42
  /**
@@ -374,6 +374,8 @@ export class LangGraphAdapter {
374
374
  ...(progress.toolset ? { toolset: progress.toolset } : {}),
375
375
  content: progress.content,
376
376
  success: true,
377
+ ...(progress.file ? { file: progress.file } : {}),
378
+ ...(progress.files?.length ? { files: progress.files } : {}),
377
379
  ...loc,
378
380
  },
379
381
  });
@@ -390,6 +392,9 @@ export class LangGraphAdapter {
390
392
  ...(progress.toolset ? { toolset: progress.toolset } : {}),
391
393
  error: progress.error ?? 'Unknown error',
392
394
  willRetry: progress.willRetry === true,
395
+ ...(progress.content ? { content: progress.content } : {}),
396
+ ...(progress.file ? { file: progress.file } : {}),
397
+ ...(progress.files?.length ? { files: progress.files } : {}),
393
398
  ...loc,
394
399
  },
395
400
  });
@@ -788,17 +793,30 @@ export class LangGraphAdapter {
788
793
  case 'tool':
789
794
  return this.createToolNodeFunction(node);
790
795
  case 'condition':
791
- return async (state) => withGraphSpan(state, { name: node.id, type: 'condition' }, async () => {
796
+ return async (state) => {
792
797
  const conditionExpr = node.config?.['conditionExpr']
793
798
  ?? node.config?.['conditionFn'];
794
- if (conditionExpr) {
795
- const branchValues = node.config?.['branchValues']
796
- ?? this.extractBranchValuesFromDsl(node.id);
797
- const result = await this.conditionEvaluator.evaluate(conditionExpr, state, branchValues, node.config?.['fallback'] ?? '__default__');
798
- return { currentNode: node.id, __conditionResult: result };
799
- }
800
- return { currentNode: node.id };
801
- });
799
+ const branchValues = node.config?.['branchValues']
800
+ ?? this.extractBranchValuesFromDsl(node.id);
801
+ const fallback = node.config?.['fallback'] ?? '__default__';
802
+ return withGraphSpan(state, {
803
+ name: node.id,
804
+ type: 'condition',
805
+ extra: {
806
+ input: {
807
+ conditionExpr: conditionExpr ?? null,
808
+ branchValues,
809
+ fallback,
810
+ },
811
+ },
812
+ }, async () => {
813
+ if (conditionExpr) {
814
+ const result = await this.conditionEvaluator.evaluate(conditionExpr, state, branchValues, fallback);
815
+ return { currentNode: node.id, __conditionResult: result };
816
+ }
817
+ return { currentNode: node.id };
818
+ }, { outputFrom: (result) => result['__conditionResult'] });
819
+ };
802
820
  case 'subgraph':
803
821
  return this.createSubGraphNodeFunction(node);
804
822
  case 'human':
@@ -1019,136 +1037,155 @@ export class LangGraphAdapter {
1019
1037
  lastAssistantHasUnansweredToolCalls(rawMessages)) {
1020
1038
  return { currentNode: node.id };
1021
1039
  }
1022
- return withGraphSpan(state, { name: node.id, type: 'llm' }, async () => {
1023
- const promptRoots = Array.isArray(state['promptRoots']) ? state['promptRoots'] : [];
1024
- const resolvedPrompt = resolveGraphPrompt(baseConfig.systemPrompt, promptRoots);
1025
- const loadedToolsBase = state['loadedTools'];
1026
- const agentCode = typeof state['agentCode'] === 'string' ? state['agentCode'] : undefined;
1027
- const skillDefs = state['skillDefinitions'];
1028
- const execCtx = state['executionContext'];
1029
- const userId = (typeof execCtx?.userId === 'string' && execCtx.userId) || state['userId'];
1030
- const loadedTools = disableTools
1031
- ? loadedToolsBase
1032
- : await hydrateMcpTools(loadedToolsBase, this.toolExecutor, {
1033
- authContext: {
1034
- userId: String(userId ?? ''),
1035
- delegationToken: String(execCtx?.delegationToken ?? state['delegationToken'] ?? ''),
1036
- agentCode: String(agentCode ?? ''),
1037
- },
1038
- correlationId: typeof state['correlationId'] === 'string' ? state['correlationId'] : undefined,
1039
- taskId: typeof state['taskId'] === 'string' ? state['taskId'] : undefined,
1040
- onListFailure: ({ toolset, error }) => {
1041
- this.emitToolExecutionEvents({
1042
- toolCallId: `mcp-list:${toolset}`,
1043
- toolName: MCP_LIST_TOOL_NAME,
1044
- toolset,
1045
- status: 'error',
1046
- error,
1047
- });
1048
- },
1049
- });
1050
- if (loadedTools && loadedTools !== loadedToolsBase) {
1051
- state['loadedTools'] = loadedTools;
1052
- }
1053
- // [DEBUG] 打印 loadedTools 实际状态
1054
- if (process.env.EAP_LLM_DEBUG === '1') {
1055
- const toolNames = loadedTools ? Object.keys(loadedTools) : [];
1056
- console.log(`[LLM-DEBUG] node=${node.id} loadedTools=${toolNames.length}:`, toolNames);
1057
- }
1058
- const catalog = includeSkillCatalog ? this.buildSkillCatalog(skillDefs) : undefined;
1059
- const toolCatalog = disableTools ? undefined : buildToolCatalog(loadedTools);
1060
- const jail = resolveJailFromState({
1061
- agentDir: state['agentDir'],
1062
- userId,
1063
- executionContext: execCtx,
1064
- loadedTools,
1065
- skillDefinitions: skillDefs,
1066
- });
1067
- if (this.overlayExtraRoots.length > 0) {
1068
- jail.extraRoots = [...(jail.extraRoots ?? []), ...this.overlayExtraRoots];
1069
- }
1070
- const workingDirectory = jail.workdir;
1071
- const jailRoots = resolveJailRoots(jail);
1072
- const skillDirs = Object.values(skillDefs ?? {})
1073
- .filter((skill) => Boolean(skill.bundlePath))
1074
- .map((skill) => ({
1075
- skillId: skill.skillId,
1076
- bundlePath: skill.bundlePath,
1077
- entryFile: skill.entryFile,
1078
- }));
1079
- const promptInputs = resolveLlmPromptInputs({
1080
- silent,
1081
- agentSystemPrompt: state['agentSystemPrompt'],
1082
- nodePrompt: resolvedPrompt,
1040
+ const promptRoots = Array.isArray(state['promptRoots']) ? state['promptRoots'] : [];
1041
+ const resolvedPrompt = resolveGraphPrompt(baseConfig.systemPrompt, promptRoots);
1042
+ const loadedToolsBase = state['loadedTools'];
1043
+ const agentCode = typeof state['agentCode'] === 'string' ? state['agentCode'] : undefined;
1044
+ const skillDefs = state['skillDefinitions'];
1045
+ const execCtx = state['executionContext'];
1046
+ const userId = (typeof execCtx?.userId === 'string' && execCtx.userId) || state['userId'];
1047
+ const loadedTools = disableTools
1048
+ ? loadedToolsBase
1049
+ : await hydrateMcpTools(loadedToolsBase, this.toolExecutor, {
1050
+ authContext: {
1051
+ userId: String(userId ?? ''),
1052
+ delegationToken: String(execCtx?.delegationToken ?? state['delegationToken'] ?? ''),
1053
+ agentCode: String(agentCode ?? ''),
1054
+ },
1055
+ correlationId: typeof state['correlationId'] === 'string' ? state['correlationId'] : undefined,
1056
+ taskId: typeof state['taskId'] === 'string' ? state['taskId'] : undefined,
1057
+ onListFailure: ({ toolset, error }) => {
1058
+ this.emitToolExecutionEvents({
1059
+ toolCallId: `mcp-list:${toolset}`,
1060
+ toolName: MCP_LIST_TOOL_NAME,
1061
+ toolset,
1062
+ status: 'error',
1063
+ error,
1064
+ });
1065
+ },
1083
1066
  });
1084
- const assembled = this.systemPromptBuilder.build({
1085
- agentPrompt: promptInputs.agentPrompt,
1086
- nodePrompt: promptInputs.nodePrompt,
1087
- skillCatalog: catalog,
1088
- toolCatalog,
1067
+ if (loadedTools && loadedTools !== loadedToolsBase) {
1068
+ state['loadedTools'] = loadedTools;
1069
+ }
1070
+ // [DEBUG] 打印 loadedTools 实际状态
1071
+ if (process.env.EAP_LLM_DEBUG === '1') {
1072
+ const toolNames = loadedTools ? Object.keys(loadedTools) : [];
1073
+ console.log(`[LLM-DEBUG] node=${node.id} loadedTools=${toolNames.length}:`, toolNames);
1074
+ }
1075
+ const catalog = includeSkillCatalog ? this.buildSkillCatalog(skillDefs) : undefined;
1076
+ const toolCatalog = disableTools ? undefined : buildToolCatalog(loadedTools);
1077
+ const jail = resolveJailFromState({
1078
+ agentDir: state['agentDir'],
1079
+ userId,
1080
+ executionContext: execCtx,
1081
+ loadedTools,
1082
+ skillDefinitions: skillDefs,
1083
+ });
1084
+ if (this.overlayExtraRoots.length > 0) {
1085
+ jail.extraRoots = [...(jail.extraRoots ?? []), ...this.overlayExtraRoots];
1086
+ }
1087
+ const workingDirectory = jail.workdir;
1088
+ const jailRoots = resolveJailRoots(jail);
1089
+ const skillDirs = Object.values(skillDefs ?? {})
1090
+ .filter((skill) => Boolean(skill.bundlePath))
1091
+ .map((skill) => ({
1092
+ skillId: skill.skillId,
1093
+ bundlePath: skill.bundlePath,
1094
+ entryFile: skill.entryFile,
1095
+ }));
1096
+ const promptInputs = resolveLlmPromptInputs({
1097
+ silent,
1098
+ agentSystemPrompt: state['agentSystemPrompt'],
1099
+ nodePrompt: resolvedPrompt,
1100
+ });
1101
+ const assembled = this.systemPromptBuilder.build({
1102
+ agentPrompt: promptInputs.agentPrompt,
1103
+ nodePrompt: promptInputs.nodePrompt,
1104
+ skillCatalog: catalog,
1105
+ toolCatalog,
1106
+ agentCode,
1107
+ userId,
1108
+ workingDirectory,
1109
+ os: { platform: os.platform(), release: os.release() },
1110
+ jailRoots,
1111
+ skillDirs,
1112
+ });
1113
+ const apiTools = disableTools ? [] : loadedToolsToApiTools(loadedTools);
1114
+ const finalConfig = {
1115
+ ...baseConfig,
1116
+ systemPrompt: assembled.text,
1117
+ systemSections: assembled.sections,
1118
+ turn: {
1119
+ sessionId: typeof state['sessionId'] === 'string' ? state['sessionId'] : undefined,
1120
+ taskId: typeof state['taskId'] === 'string' ? state['taskId'] : undefined,
1121
+ nodeId: node.id,
1089
1122
  agentCode,
1090
- userId,
1091
- workingDirectory,
1092
- os: { platform: os.platform(), release: os.release() },
1093
- jailRoots,
1094
- skillDirs,
1095
- });
1096
- const apiTools = disableTools ? [] : loadedToolsToApiTools(loadedTools);
1097
- const finalConfig = {
1098
- ...baseConfig,
1099
- systemPrompt: assembled.text,
1100
- systemSections: assembled.sections,
1101
- turn: {
1102
- sessionId: typeof state['sessionId'] === 'string' ? state['sessionId'] : undefined,
1103
- taskId: typeof state['taskId'] === 'string' ? state['taskId'] : undefined,
1104
- nodeId: node.id,
1105
- agentCode,
1106
- correlationId: typeof state['correlationId'] === 'string' ? state['correlationId'] : undefined,
1107
- },
1108
- tools: disableTools ? undefined : (apiTools.length > 0 ? apiTools : baseConfig.tools),
1109
- toolChoice: disableTools ? 'none' : baseConfig.toolChoice,
1110
- };
1111
- // 流式旁路:有本次 stream 袋时启用 LLM token 推送;silent 节点不推给用户
1112
- const stream = getExecutionStream();
1113
- const enableStream = Boolean(stream) && !silent;
1114
- if (stream) {
1115
- stream.reasoningStartedThisTurn = false;
1116
- stream.streamedTextThisTurn = false;
1117
- }
1118
- const llmNode = new LLMNode(node.id, finalConfig, this.llmProvider, enableStream, enableStream
1119
- ? (text) => {
1120
- const live = getExecutionStream();
1121
- if (!live || !text)
1122
- return;
1123
- const timestamp = new Date().toISOString();
1124
- if (!live.reasoningStartedThisTurn) {
1125
- live.reasoningStartedThisTurn = true;
1126
- live.iteration += 1;
1127
- live.sink({
1128
- type: 'reasoning.started',
1129
- taskId: undefined,
1130
- correlationId: undefined,
1131
- timestamp,
1132
- payload: {
1133
- iterationCount: live.iteration,
1134
- model: finalConfig.model,
1135
- },
1136
- });
1137
- }
1138
- live.streamedTextThisTurn = true;
1123
+ correlationId: typeof state['correlationId'] === 'string' ? state['correlationId'] : undefined,
1124
+ },
1125
+ tools: disableTools ? undefined : (apiTools.length > 0 ? apiTools : baseConfig.tools),
1126
+ toolChoice: disableTools ? 'none' : baseConfig.toolChoice,
1127
+ };
1128
+ // 流式旁路:有本次 stream 袋时启用 LLM token 推送;silent 节点不推给用户
1129
+ const stream = getExecutionStream();
1130
+ const enableStream = Boolean(stream) && !silent;
1131
+ if (stream) {
1132
+ stream.reasoningStartedThisTurn = false;
1133
+ stream.streamedTextThisTurn = false;
1134
+ }
1135
+ const llmNode = new LLMNode(node.id, finalConfig, this.llmProvider, enableStream, enableStream
1136
+ ? (text) => {
1137
+ const live = getExecutionStream();
1138
+ if (!live || !text)
1139
+ return;
1140
+ const timestamp = new Date().toISOString();
1141
+ if (!live.reasoningStartedThisTurn) {
1142
+ live.reasoningStartedThisTurn = true;
1143
+ live.iteration += 1;
1139
1144
  live.sink({
1140
- type: 'response.chunk',
1145
+ type: 'reasoning.started',
1141
1146
  taskId: undefined,
1142
1147
  correlationId: undefined,
1143
1148
  timestamp,
1144
1149
  payload: {
1145
- content: text,
1146
- index: live.chunkIndex++,
1147
- node: node.id,
1150
+ iterationCount: live.iteration,
1151
+ model: finalConfig.model,
1148
1152
  },
1149
1153
  });
1150
1154
  }
1151
- : undefined, this.interceptors);
1155
+ live.streamedTextThisTurn = true;
1156
+ live.sink({
1157
+ type: 'response.chunk',
1158
+ taskId: undefined,
1159
+ correlationId: undefined,
1160
+ timestamp,
1161
+ payload: {
1162
+ content: text,
1163
+ index: live.chunkIndex++,
1164
+ node: node.id,
1165
+ },
1166
+ });
1167
+ }
1168
+ : undefined, this.interceptors);
1169
+ const toolsForSpan = disableTools || !loadedTools
1170
+ ? undefined
1171
+ : Object.values(loadedTools).map((t) => ({
1172
+ name: t.name,
1173
+ ...(t.manifest.description?.trim()
1174
+ ? { description: t.manifest.description.trim() }
1175
+ : {}),
1176
+ }));
1177
+ // 组装后再 createNode:input 必须是发给模型的 system/tools/messages;completeNode 不能补 input。
1178
+ return withGraphSpan(state, {
1179
+ name: node.id,
1180
+ type: 'llm',
1181
+ extra: {
1182
+ input: llmSpanInput({
1183
+ system: assembled.text,
1184
+ tools: toolsForSpan,
1185
+ messages: state['messages'],
1186
+ }),
1187
+ },
1188
+ }, async () => {
1152
1189
  const next = await llmNode.execute(state);
1153
1190
  const hydratedUpdate = loadedTools && loadedTools !== loadedToolsBase ? { loadedTools } : {};
1154
1191
  if (!silent)
@@ -1163,7 +1200,7 @@ export class LangGraphAdapter {
1163
1200
  silentOutput: raw,
1164
1201
  ...hydratedUpdate,
1165
1202
  };
1166
- });
1203
+ }, { outputFrom: llmSpanOutputFromResult });
1167
1204
  };
1168
1205
  }
1169
1206
  /**
@@ -1560,10 +1597,20 @@ export class LangGraphAdapter {
1560
1597
  continue;
1561
1598
  }
1562
1599
  if (msg.role === 'tool') {
1563
- // tool.result / tool.error 由 Tool 节点旁路发出,此处只补 file.created
1600
+ // tool.result / tool.error 由 Tool 节点旁路发出(已带 file/files);此处再补 file.created
1564
1601
  const raw = typeof msg.content === 'string' ? msg.content : '';
1565
- const file = msg.file ?? fileArtifactFromToolOutput(raw);
1566
- if (file) {
1602
+ const files = [
1603
+ ...(msg.file ? [msg.file] : []),
1604
+ ...(msg.files ?? []),
1605
+ ];
1606
+ if (files.length === 0) {
1607
+ const parsed = fileArtifactFromToolOutput(raw);
1608
+ if (parsed)
1609
+ files.push(parsed);
1610
+ }
1611
+ for (const file of files) {
1612
+ if (!file?.url)
1613
+ continue;
1567
1614
  yield {
1568
1615
  type: 'file.created',
1569
1616
  taskId: undefined,
@@ -1630,6 +1677,7 @@ export class LangGraphAdapter {
1630
1677
  success: typeof m['success'] === 'boolean' ? m['success'] : undefined,
1631
1678
  reasoningContent: typeof m['reasoningContent'] === 'string' ? m['reasoningContent'] : undefined,
1632
1679
  file: readFileArtifact(m['file']),
1680
+ files: readFileArtifactList(m['files']),
1633
1681
  };
1634
1682
  });
1635
1683
  }
@@ -1647,3 +1695,14 @@ function readFileArtifact(value) {
1647
1695
  url: rec.url,
1648
1696
  };
1649
1697
  }
1698
+ function readFileArtifactList(value) {
1699
+ if (!Array.isArray(value))
1700
+ return undefined;
1701
+ const out = [];
1702
+ for (const item of value) {
1703
+ const file = readFileArtifact(item);
1704
+ if (file)
1705
+ out.push(file);
1706
+ }
1707
+ return out.length > 0 ? out : undefined;
1708
+ }
@@ -1,5 +1,6 @@
1
1
  import { realpathSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { userRoot } from '../../common/user-root.js';
3
4
  function tryRealpath(p) {
4
5
  try {
5
6
  return realpathSync(p);
@@ -49,12 +50,20 @@ export function classifySkillSpace(bundleOrFilePath, opts) {
49
50
  return 'agent';
50
51
  const uid = opts?.userId?.trim();
51
52
  if (uid) {
52
- const user = `${root}/users/${uid}/skills`;
53
- if (target === user || target.startsWith(`${user}/`))
54
- return 'user';
53
+ try {
54
+ const user = normalizeForMatch(path.join(userRoot(agentDir, uid), 'skills'));
55
+ if (target === user || target.startsWith(`${user}/`))
56
+ return 'user';
57
+ }
58
+ catch {
59
+ // 非法 userId 时不按用户槽判定
60
+ }
55
61
  }
56
- if (new RegExp(`^${escapeRegExp(root)}/users/[^/]+/skills(/|$)`).test(target))
62
+ const usersRoot = normalizeForMatch(path.join(path.dirname(agentDir), 'users'));
63
+ if ((target === usersRoot || target.startsWith(`${usersRoot}/`)) &&
64
+ /(^|\/)skills(\/|$)/.test(target.slice(usersRoot.length))) {
57
65
  return 'user';
66
+ }
58
67
  return 'local';
59
68
  }
60
69
  if (/(^|\/)preinstall\/skills(\/|$)/i.test(target))
@@ -276,13 +276,19 @@ export interface GraphMessage {
276
276
  * 多轮请求必须原样回传,否则上游会 400。
277
277
  */
278
278
  reasoningContent?: string;
279
- /** 工具产出的文件;仅 Runtime 发 file.created,不进模型正文 */
279
+ /** 工具产出的文件;SSE tool.result / tool.error 与 file.created 共用,不进模型正文 */
280
280
  file?: {
281
281
  fileName: string;
282
282
  fileType: string;
283
283
  size: number;
284
284
  url: string;
285
285
  };
286
+ files?: Array<{
287
+ fileName: string;
288
+ fileType: string;
289
+ size: number;
290
+ url: string;
291
+ }>;
286
292
  }
287
293
  /** Tool 调用信息 */
288
294
  export interface ToolCallInfo {
@@ -289,6 +289,12 @@ export class HitlHandler {
289
289
  name: extras.toolName ?? 'hitl',
290
290
  type: 'hitl',
291
291
  parentSpanId: extras.toolSpanId ?? skillSpanId,
292
+ extra: {
293
+ input: {
294
+ question: config.question,
295
+ options: config.options,
296
+ },
297
+ },
292
298
  });
293
299
  const persisted = {
294
300
  taskId,
@@ -10,7 +10,7 @@
10
10
  * @see requirements.md Requirement 8 AC1, AC3, AC5
11
11
  */
12
12
  import type { NodeRetryConfig } from '../contracts/executable-graph.js';
13
- import type { AuthContext } from '../../tool/tool-executor.interface.js';
13
+ import type { AuthContext, ToolCallResult } from '../../tool/tool-executor.interface.js';
14
14
  import type { ToolOrchestrator } from '../../tool/tool-orchestrator.js';
15
15
  import type { ToolResultInjector } from '../../tool/tool-result-injector.js';
16
16
  import type { SkillDirRef } from '../adapters/skill-read.js';
@@ -41,6 +41,9 @@ export interface ToolNodeProgress {
41
41
  arguments?: Record<string, unknown>;
42
42
  content?: string;
43
43
  error?: string;
44
+ /** 与 ToolCallResult.file / files 同形;completed 与失败终态都带 */
45
+ file?: ToolCallResult['file'];
46
+ files?: ToolCallResult['files'];
44
47
  executionTime?: number;
45
48
  node?: string;
46
49
  skillId?: string;