@gt-fe/eap-runtime 0.0.30 → 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 (36) 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 +30 -3
  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/nodes/tool-node.d.ts +4 -1
  11. package/dist/graph/nodes/tool-node.js +15 -1
  12. package/dist/registry/deployment-snapshot.d.ts +1 -1
  13. package/dist/registry/deployment-snapshot.js +6 -3
  14. package/dist/registry/skill-tree.d.ts +1 -1
  15. package/dist/registry/skill-tree.js +11 -6
  16. package/dist/runtime/agent-runtime.js +36 -5
  17. package/dist/session/persist-turn-messages.d.ts +13 -6
  18. package/dist/session/persist-turn-messages.js +4 -0
  19. package/dist/sse/events/tool-events.d.ts +29 -0
  20. package/dist/tool/harness-command.d.ts +1 -1
  21. package/dist/tool/harness-command.js +8 -7
  22. package/dist/tool/harness-primitives.d.ts +3 -3
  23. package/dist/tool/harness-primitives.js +185 -60
  24. package/dist/tool/index.d.ts +2 -0
  25. package/dist/tool/index.js +2 -0
  26. package/dist/tool/inject-public-artifact-links.d.ts +32 -0
  27. package/dist/tool/inject-public-artifact-links.js +142 -0
  28. package/dist/tool/static-artifact-url.d.ts +66 -0
  29. package/dist/tool/static-artifact-url.js +561 -0
  30. package/dist/tool/system-prompt-builder.js +8 -4
  31. package/dist/tool/tool-event-emitter.d.ts +2 -1
  32. package/dist/tool/tool-event-emitter.js +30 -18
  33. package/dist/tool/tool-executor.interface.d.ts +8 -1
  34. package/dist/tool/tool-result-injector.d.ts +3 -3
  35. package/dist/tool/tool-result-injector.js +7 -3
  36. 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
+ }
@@ -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
  });
@@ -1592,10 +1597,20 @@ export class LangGraphAdapter {
1592
1597
  continue;
1593
1598
  }
1594
1599
  if (msg.role === 'tool') {
1595
- // tool.result / tool.error 由 Tool 节点旁路发出,此处只补 file.created
1600
+ // tool.result / tool.error 由 Tool 节点旁路发出(已带 file/files);此处再补 file.created
1596
1601
  const raw = typeof msg.content === 'string' ? msg.content : '';
1597
- const file = msg.file ?? fileArtifactFromToolOutput(raw);
1598
- 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;
1599
1614
  yield {
1600
1615
  type: 'file.created',
1601
1616
  taskId: undefined,
@@ -1662,6 +1677,7 @@ export class LangGraphAdapter {
1662
1677
  success: typeof m['success'] === 'boolean' ? m['success'] : undefined,
1663
1678
  reasoningContent: typeof m['reasoningContent'] === 'string' ? m['reasoningContent'] : undefined,
1664
1679
  file: readFileArtifact(m['file']),
1680
+ files: readFileArtifactList(m['files']),
1665
1681
  };
1666
1682
  });
1667
1683
  }
@@ -1679,3 +1695,14 @@ function readFileArtifact(value) {
1679
1695
  url: rec.url,
1680
1696
  };
1681
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 {
@@ -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;
@@ -19,9 +19,17 @@ import { resolveCheckpointThreadId } from '../../session/persist-turn-messages.j
19
19
  import { mcpToolsetCode, toolTraceIdentity } from '../../tool/mcp-list.js';
20
20
  import { resolveExecutionTrace, warnTraceSpanFailure, } from '../execution-trace-context.js';
21
21
  import { emitRuntimeLog } from '../../common/logger.js';
22
+ import { toolResultHasArtifacts } from '../../tool/inject-public-artifact-links.js';
22
23
  import { decideBeforeTool, } from '../../execution/interceptors.js';
23
24
  import { isRetryableToolFailure, resolveToolRetryConfig, sleepToolRetry, toolRetryDelayMs, } from '../../tool/tool-retry.js';
24
25
  import { isIsolatedSkillState } from '../adapters/skill-isolate.js';
26
+ function toolArtifactFields(result) {
27
+ return {
28
+ ...(result.content ? { content: result.content } : {}),
29
+ ...(result.file ? { file: result.file } : {}),
30
+ ...(result.files?.length ? { files: result.files } : {}),
31
+ };
32
+ }
25
33
  function skillDirsFromState(state) {
26
34
  const defs = state['skillDefinitions'];
27
35
  return Object.values(defs ?? {})
@@ -302,6 +310,7 @@ export class ToolNode {
302
310
  status: 'completed',
303
311
  arguments: req.arguments,
304
312
  content: result.content,
313
+ ...toolArtifactFields(result),
305
314
  executionTime,
306
315
  ...(skillIdentity?.skillSpace ? { skillSpace: skillIdentity.skillSpace } : {}),
307
316
  ...(skillIdentity?.skillVersion ? { skillVersion: skillIdentity.skillVersion } : {}),
@@ -317,6 +326,7 @@ export class ToolNode {
317
326
  status: 'error',
318
327
  arguments: req.arguments,
319
328
  error: result.error ?? 'unknown error',
329
+ ...toolArtifactFields(result),
320
330
  executionTime,
321
331
  });
322
332
  }
@@ -421,12 +431,15 @@ export class ToolNode {
421
431
  role: 'tool',
422
432
  content: result.success
423
433
  ? result.content
424
- : `[Tool Error] ${result.error ?? 'unknown error'}`,
434
+ : toolResultHasArtifacts(result) && result.content
435
+ ? `${result.content}\n[Tool Error] ${result.error ?? 'unknown error'}`
436
+ : `[Tool Error] ${result.error ?? 'unknown error'}`,
425
437
  // 以下字段符合 LLM function-calling 协议(ToolMessage 接口)
426
438
  toolCallId: result.toolCallId,
427
439
  toolName: result.toolName,
428
440
  success: result.success,
429
441
  ...('file' in result && result.file ? { file: result.file } : {}),
442
+ ...('files' in result && result.files?.length ? { files: result.files } : {}),
430
443
  }));
431
444
  const lastSpanId = [...spanIds].reverse().find((id) => Boolean(id));
432
445
  return {
@@ -477,6 +490,7 @@ export class ToolNode {
477
490
  status: 'error',
478
491
  arguments: req.arguments,
479
492
  error: last.error ?? 'unknown error',
493
+ ...toolArtifactFields(last),
480
494
  willRetry: true,
481
495
  });
482
496
  await sleepToolRetry(toolRetryDelayMs(attempt, retry.backoff));
@@ -4,7 +4,7 @@
4
4
  * 目录(Agent 自包含,无全局共享层):
5
5
  * {root}/agents/<agentCode>/<version>/preinstall/tools/<toolsetCode>/
6
6
  * {root}/agents/<agentCode>/<version>/preinstall/skills/<skillCode>/
7
- * {root}/agents/<agentCode>/<version>/users/<userId>/skills/<skillCode>/
7
+ * {root}/agents/<agentCode>/users/<userId>/skills/<skillCode>/
8
8
  *
9
9
  * 命名:agentCode / toolsetCode / skillCode / toolName 均为语义化小写标识;
10
10
  * Skill 目录无版本层:包内必含 SKILL.md(Agent Skills)。
@@ -4,7 +4,7 @@
4
4
  * 目录(Agent 自包含,无全局共享层):
5
5
  * {root}/agents/<agentCode>/<version>/preinstall/tools/<toolsetCode>/
6
6
  * {root}/agents/<agentCode>/<version>/preinstall/skills/<skillCode>/
7
- * {root}/agents/<agentCode>/<version>/users/<userId>/skills/<skillCode>/
7
+ * {root}/agents/<agentCode>/users/<userId>/skills/<skillCode>/
8
8
  *
9
9
  * 命名:agentCode / toolsetCode / skillCode / toolName 均为语义化小写标识;
10
10
  * Skill 目录无版本层:包内必含 SKILL.md(Agent Skills)。
@@ -18,6 +18,7 @@ import { findSkillEntryFile } from './skill-tree.js';
18
18
  import { preloadMissing, preloadPackageCorrupt, } from './preload-errors.js';
19
19
  import { isRequireChecksum, resolvePreloadBehaviorFlags, } from './preload-flags.js';
20
20
  import { emitRuntimeLog } from '../common/logger.js';
21
+ import { userRoot, USERS_DIR_NAME } from '../common/user-root.js';
21
22
  async function pathExists(p) {
22
23
  try {
23
24
  await access(p, fsConstants.F_OK);
@@ -141,7 +142,9 @@ async function pickLatestVersionDir(parent) {
141
142
  if (!(await pathExists(parent)))
142
143
  return null;
143
144
  const entries = await readdir(parent, { withFileTypes: true });
144
- const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
145
+ const dirs = entries
146
+ .filter((e) => e.isDirectory() && e.name !== USERS_DIR_NAME)
147
+ .map((e) => e.name);
145
148
  if (dirs.includes('current')) {
146
149
  return path.join(parent, 'current');
147
150
  }
@@ -534,7 +537,7 @@ export class FileSystemDeploymentSnapshot {
534
537
  // 用户装配 → 开发态作者源 extraRoots → Agent 预装
535
538
  const candidates = [];
536
539
  if (userId != null && userId !== '') {
537
- candidates.push(path.join(agentDir, 'users', userId, 'skills', ref.skillId));
540
+ candidates.push(path.join(userRoot(agentDir, userId), 'skills', ref.skillId));
538
541
  }
539
542
  for (const root of extraRoots) {
540
543
  if (root)
@@ -25,6 +25,6 @@ export declare function inspectAgentSkillTree(agentDir: string, userId?: string,
25
25
  */
26
26
  export declare function watchPreinstallSkillRoot(preinstallRoot: string, onChange: () => void, debounceMs?: number): () => void;
27
27
  export declare function watchSkillRoots(roots: string[], onChange: () => void, debounceMs?: number): () => void;
28
- export declare function watchAgentSkillDirs(agentDir: string, onChange: () => void, debounceMs?: number, extraRoots?: string[]): () => void;
28
+ export declare function watchAgentSkillDirs(agentDir: string, onChange: () => void, debounceMs?: number, extraRoots?: string[], userId?: string): () => void;
29
29
  /** EAP_SKILL_HOT_RELOAD:dev 默认开;server/desktop 默认关 */
30
30
  export declare function resolveSkillHotReload(deploymentContext: string, explicit?: boolean): boolean;
@@ -2,7 +2,7 @@
2
2
  * Agent 自带 Skill 目录扫描(§3.1)
3
3
  *
4
4
  * agents/<code>/<ver>/preinstall/skills/<skillCode>/
5
- * agents/<code>/<ver>/users/<userId>/skills/<skillCode>/ 同名覆盖
5
+ * agents/<code>/users/<userId>/skills/<skillCode>/ 同名覆盖(写根与 <ver> 平级)
6
6
  *
7
7
  * 热加载用 stamp:目录/文件 mtime 变化则缓存失效。
8
8
  */
@@ -11,6 +11,7 @@ import { watch } from 'node:fs';
11
11
  import { constants as fsConstants } from 'node:fs';
12
12
  import path from 'node:path';
13
13
  import { SKILL_ENTRY_CANDIDATES } from '@gt-fe/eap-contracts';
14
+ import { userRoot } from '../common/user-root.js';
14
15
  const SKILL_CODE = /^[a-z0-9][a-z0-9-]*$/;
15
16
  async function exists(p) {
16
17
  try {
@@ -75,7 +76,7 @@ export async function readSkillAssembly(agentDir, userId) {
75
76
  const candidates = [];
76
77
  const uid = userId?.trim();
77
78
  if (uid)
78
- candidates.push(path.join(agentDir, 'users', uid, '.assembly.json'));
79
+ candidates.push(path.join(userRoot(agentDir, uid), '.assembly.json'));
79
80
  candidates.push(path.join(agentDir, '.assembly.json'), path.join(agentDir, 'assembly.json'));
80
81
  for (const file of candidates) {
81
82
  const parsed = await readAssemblyFile(file);
@@ -102,7 +103,7 @@ export async function skillAssemblyStamp(agentDir, userId) {
102
103
  const candidates = [];
103
104
  const uid = userId?.trim();
104
105
  if (uid)
105
- candidates.push(path.join(agentDir, 'users', uid, '.assembly.json'));
106
+ candidates.push(path.join(userRoot(agentDir, uid), '.assembly.json'));
106
107
  candidates.push(path.join(agentDir, '.assembly.json'), path.join(agentDir, 'assembly.json'));
107
108
  let latest = 0;
108
109
  for (const file of candidates) {
@@ -114,7 +115,7 @@ export function preinstallSkillsDir(agentDir) {
114
115
  return path.join(agentDir, 'preinstall', 'skills');
115
116
  }
116
117
  export function userSkillsDir(agentDir, userId) {
117
- return path.join(agentDir, 'users', userId, 'skills');
118
+ return path.join(userRoot(agentDir, userId), 'skills');
118
119
  }
119
120
  export async function inspectAgentSkillTree(agentDir, userId, extraRoots = []) {
120
121
  const roots = [preinstallSkillsDir(agentDir), ...extraRoots.filter(Boolean)];
@@ -205,8 +206,12 @@ export function watchSkillRoots(roots, onChange, debounceMs = 200) {
205
206
  watcher.close();
206
207
  };
207
208
  }
208
- export function watchAgentSkillDirs(agentDir, onChange, debounceMs = 200, extraRoots = []) {
209
- return watchSkillRoots([preinstallSkillsDir(agentDir), path.join(agentDir, 'users'), ...extraRoots], onChange, debounceMs);
209
+ export function watchAgentSkillDirs(agentDir, onChange, debounceMs = 200, extraRoots = [], userId) {
210
+ const roots = [preinstallSkillsDir(agentDir), ...extraRoots];
211
+ const uid = userId?.trim();
212
+ if (uid)
213
+ roots.push(userSkillsDir(agentDir, uid));
214
+ return watchSkillRoots(roots, onChange, debounceMs);
210
215
  }
211
216
  /** EAP_SKILL_HOT_RELOAD:dev 默认开;server/desktop 默认关 */
212
217
  export function resolveSkillHotReload(deploymentContext, explicit) {
@@ -16,6 +16,7 @@ import { toLoadedSkillMap } from '../tool/system-prompt-builder.js';
16
16
  import { assembleLoadedTools } from '../tool/harness-primitives.js';
17
17
  import { createFileLlmTurnLogFromEnv, } from '../debug/llm-turn-log.js';
18
18
  import { buildHitlWaitingTaskOutput, buildTurnTranscriptFromStreamEvents, loadModelFacingHistory, turnGraphMessagesAfterCompression, extractHitlThreadId, extractStreamReasoning, applyStreamTextDelta, createStreamAssistantBuckets, visibleStreamAssistantText, streamAssistantTexts, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, resolveCheckpointThreadId, appendTerminalToolResult, } from '../session/persist-turn-messages.js';
19
+ import { collectPublicArtifactLinks, collectPublicArtifactLinksFromEvent, injectPublicArtifactLinks, mergeArtifactLinks, } from '../tool/inject-public-artifact-links.js';
19
20
  import { iterSessionCompression, withCompressionSummaryModel, } from '../session/apply-session-compression.js';
20
21
  import { applySessionMemory } from '../session/apply-session-memory.js';
21
22
  import { buildSessionTitleUserPayload, invokeSessionTitle, } from '../session/session-title.js';
@@ -378,6 +379,7 @@ export class AgentRuntime {
378
379
  let assistantReasoningContent;
379
380
  const pendingToolCalls = [];
380
381
  const toolResults = [];
382
+ const artifactLinks = [];
381
383
  const nodeTokenUsages = [];
382
384
  try {
383
385
  for await (const runtimeEvent of runWithExecutionTrace({ abortSignal }, () => this.deps.graphEngine.stream(graph, {
@@ -421,6 +423,7 @@ export class AgentRuntime {
421
423
  }
422
424
  // 终态 tool.result / tool.error 回填历史;willRetry 的 error 不进 transcript
423
425
  appendTerminalToolResult(runtimeEvent.type, runtimeEvent.payload, toolResults);
426
+ collectPublicArtifactLinksFromEvent(runtimeEvent.type, runtimeEvent.payload, artifactLinks);
424
427
  const streamedReasoning = extractStreamReasoning(runtimeEvent.type, runtimeEvent.payload);
425
428
  if (streamedReasoning)
426
429
  assistantReasoningContent = streamedReasoning;
@@ -433,6 +436,19 @@ export class AgentRuntime {
433
436
  await recordTaskTokenUsages(this.deps.taskService, task.id, nodeTokenUsages);
434
437
  await this.deps.taskService.updateStatus(task.id, 'success');
435
438
  const tokenSummary = summarizeTokenUsages(nodeTokenUsages);
439
+ const visible = visibleStreamAssistantText(assistantBuckets);
440
+ const links = mergeArtifactLinks(artifactLinks, collectPublicArtifactLinks(toolResults));
441
+ const { injected, suffix } = injectPublicArtifactLinks(visible, links);
442
+ if (suffix) {
443
+ yield this.toSSE('response.chunk', correlationId, task.id, {
444
+ content: suffix,
445
+ format: 'text',
446
+ });
447
+ }
448
+ yield this.toSSE('response.final', correlationId, task.id, {
449
+ content: injected,
450
+ format: 'text',
451
+ });
436
452
  await persistTurnTranscript(this.deps.messageService, {
437
453
  sessionId,
438
454
  taskId: task.id,
@@ -440,8 +456,8 @@ export class AgentRuntime {
440
456
  messages: buildTurnTranscriptFromStreamEvents({
441
457
  pendingToolCalls,
442
458
  toolResults,
443
- assistantTexts: streamAssistantTexts(assistantBuckets),
444
- assistantText: visibleStreamAssistantText(assistantBuckets),
459
+ assistantTexts: suffix ? undefined : streamAssistantTexts(assistantBuckets),
460
+ assistantText: injected,
445
461
  finishPoints: graph.finishPoints,
446
462
  toolCallReasoningContent,
447
463
  assistantReasoningContent,
@@ -621,6 +637,7 @@ export class AgentRuntime {
621
637
  let assistantReasoningContent;
622
638
  const pendingToolCalls = [];
623
639
  const toolResults = [];
640
+ const artifactLinks = [];
624
641
  const nodeTokenUsages = [];
625
642
  try {
626
643
  for await (const runtimeEvent of runWithExecutionTrace({ abortSignal, abortDeadlineMs: continueState.abortDeadlineMs }, () => this.deps.graphEngine.stream(graph, continueState))) {
@@ -638,6 +655,7 @@ export class AgentRuntime {
638
655
  }
639
656
  }
640
657
  appendTerminalToolResult(runtimeEvent.type, runtimeEvent.payload, toolResults);
658
+ collectPublicArtifactLinksFromEvent(runtimeEvent.type, runtimeEvent.payload, artifactLinks);
641
659
  const streamedReasoning = extractStreamReasoning(runtimeEvent.type, runtimeEvent.payload);
642
660
  if (streamedReasoning)
643
661
  assistantReasoningContent = streamedReasoning;
@@ -647,8 +665,21 @@ export class AgentRuntime {
647
665
  yield this.runtimeEventToSSE(runtimeEvent, correlationId, taskId);
648
666
  }
649
667
  }
668
+ const visible = visibleStreamAssistantText(assistantBuckets);
669
+ const links = mergeArtifactLinks(artifactLinks, collectPublicArtifactLinks(toolResults));
670
+ const { injected, suffix } = injectPublicArtifactLinks(visible, links);
671
+ if (suffix) {
672
+ yield this.toSSE('response.chunk', correlationId, taskId, {
673
+ content: suffix,
674
+ format: 'text',
675
+ });
676
+ }
677
+ yield this.toSSE('response.final', correlationId, taskId, {
678
+ content: injected,
679
+ format: 'text',
680
+ });
650
681
  await recordTaskTokenUsages(this.deps.taskService, taskId, nodeTokenUsages);
651
- await this.deps.taskService.updateStatus(taskId, 'success', visibleStreamAssistantText(assistantBuckets) || undefined);
682
+ await this.deps.taskService.updateStatus(taskId, 'success', injected || undefined);
652
683
  const tokenSummary = summarizeTokenUsages(nodeTokenUsages);
653
684
  if (sessionId) {
654
685
  await persistTurnTranscript(this.deps.messageService, {
@@ -658,8 +689,8 @@ export class AgentRuntime {
658
689
  messages: buildTurnTranscriptFromStreamEvents({
659
690
  pendingToolCalls,
660
691
  toolResults,
661
- assistantTexts: streamAssistantTexts(assistantBuckets),
662
- assistantText: visibleStreamAssistantText(assistantBuckets),
692
+ assistantTexts: suffix ? undefined : streamAssistantTexts(assistantBuckets),
693
+ assistantText: injected,
663
694
  finishPoints: graph.finishPoints,
664
695
  toolCallReasoningContent,
665
696
  assistantReasoningContent,
@@ -91,12 +91,7 @@ export declare function sessionVisibleGraphMessages(messages: GraphMessage[], fi
91
91
  export declare function extractStreamReasoning(eventType: string, payload: unknown): string | undefined;
92
92
  /** 落盘 / 回填历史只收终态:result,或不会再试的 error。willRetry 的 error 只给直播 UI。 */
93
93
  export declare function isTerminalToolStreamEvent(eventType: string, payload: unknown): boolean;
94
- export declare function appendTerminalToolResult(eventType: string, payload: unknown, bucket: Array<{
95
- toolCallId: string;
96
- toolName: string;
97
- content: unknown;
98
- success: boolean;
99
- }>): void;
94
+ export declare function appendTerminalToolResult(eventType: string, payload: unknown, bucket: StreamToolResult[]): void;
100
95
  /** 历史 MessageRecord → GraphMessage(保留 tool 元数据与 reasoning) */
101
96
  export declare function messageRecordsToGraphMessages(records: MessageRecord[]): GraphMessage[];
102
97
  type CheckpointLoad = {
@@ -203,6 +198,18 @@ export type StreamToolResult = {
203
198
  toolName: string;
204
199
  content: unknown;
205
200
  success: boolean;
201
+ file?: {
202
+ fileName: string;
203
+ fileType: string;
204
+ size: number;
205
+ url: string;
206
+ };
207
+ files?: Array<{
208
+ fileName: string;
209
+ fileType: string;
210
+ size: number;
211
+ url: string;
212
+ }>;
206
213
  };
207
214
  /**
208
215
  * 将 stream 侧累加的 tool 事件规范为与 invoke GraphState 增量同序的 messages:
@@ -242,6 +242,8 @@ export function appendTerminalToolResult(eventType, payload, bucket) {
242
242
  toolName: p.toolName,
243
243
  content: p.content ?? p.error,
244
244
  success: eventType === 'tool.result' && p.success !== false,
245
+ ...(p.file ? { file: p.file } : {}),
246
+ ...(p.files?.length ? { files: p.files } : {}),
245
247
  });
246
248
  }
247
249
  /** 历史 MessageRecord → GraphMessage(保留 tool 元数据与 reasoning) */
@@ -525,6 +527,8 @@ export function buildTurnTranscriptFromStreamEvents(params) {
525
527
  toolCallId: tr.toolCallId,
526
528
  toolName: tr.toolName,
527
529
  success: tr.success,
530
+ ...(tr.file ? { file: tr.file } : {}),
531
+ ...(tr.files?.length ? { files: tr.files } : {}),
528
532
  });
529
533
  }
530
534
  if (texts.length > 0) {