@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
@@ -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));
@@ -12,4 +12,18 @@ export declare function completeGraphSpan(state: Record<string, unknown>, spanId
12
12
  export declare function withGraphSpan(state: Record<string, unknown>, spec: {
13
13
  name: string;
14
14
  type: ExecutionNodeEvent['type'];
15
- }, run: () => Promise<Record<string, unknown>>): Promise<Record<string, unknown>>;
15
+ extra?: Partial<Omit<ExecutionNodeEvent, 'traceId' | 'name' | 'type' | 'parentNodeId'>>;
16
+ }, run: () => Promise<Record<string, unknown>>, complete?: {
17
+ outputFrom?: (result: Record<string, unknown>) => unknown;
18
+ }): Promise<Record<string, unknown>>;
19
+ /** llm Span create 时的 input:组装后的 system、工具短目录、本回合 messages。 */
20
+ export declare function llmSpanInput(input: {
21
+ system?: string;
22
+ tools?: Array<{
23
+ name: string;
24
+ description?: string;
25
+ }>;
26
+ messages: unknown;
27
+ }): Record<string, unknown>;
28
+ /** llm Span complete 的 output:本回合新增 assistant(含 toolCalls)或 silentOutput。 */
29
+ export declare function llmSpanOutputFromResult(result: Record<string, unknown>): unknown;
@@ -43,15 +43,17 @@ export async function completeGraphSpan(state, spanId, params, collectorOverride
43
43
  warnTraceSpanFailure('completeNode', { spanId, traceId: resolveExecutionTrace(state).traceId }, err);
44
44
  }
45
45
  }
46
- export async function withGraphSpan(state, spec, run) {
46
+ export async function withGraphSpan(state, spec, run, complete) {
47
47
  const spanId = await startGraphSpan(state, spec);
48
48
  try {
49
49
  const result = await run();
50
50
  const usage = Array.isArray(result['__llmTokenUsage'])
51
51
  ? result['__llmTokenUsage'].at(-1)
52
52
  : undefined;
53
+ const output = complete?.outputFrom?.(result);
53
54
  await completeGraphSpan(state, spanId, {
54
55
  status: 'completed',
56
+ ...(output !== undefined ? { output } : {}),
55
57
  inputTokens: usage?.inputTokens,
56
58
  outputTokens: usage?.outputTokens,
57
59
  totalTokens: usage?.inputTokens != null || usage?.outputTokens != null
@@ -69,3 +71,69 @@ export async function withGraphSpan(state, spec, run) {
69
71
  throw err;
70
72
  }
71
73
  }
74
+ function spanGraphMessages(raw) {
75
+ if (!Array.isArray(raw))
76
+ return [];
77
+ return raw.map((msg) => {
78
+ if (typeof msg !== 'object' || msg === null) {
79
+ return { role: 'user', content: String(msg) };
80
+ }
81
+ const m = msg;
82
+ const out = {
83
+ role: typeof m['role'] === 'string' ? m['role'] : 'user',
84
+ content: typeof m['content'] === 'string' ? m['content'] : '',
85
+ };
86
+ const toolCalls = m['toolCalls'] ?? m['tool_calls'];
87
+ if (toolCalls !== undefined)
88
+ out.toolCalls = toolCalls;
89
+ const toolCallId = typeof m['toolCallId'] === 'string'
90
+ ? m['toolCallId']
91
+ : typeof m['tool_call_id'] === 'string'
92
+ ? m['tool_call_id']
93
+ : undefined;
94
+ if (toolCallId)
95
+ out.toolCallId = toolCallId;
96
+ if (typeof m['toolName'] === 'string')
97
+ out.toolName = m['toolName'];
98
+ if (typeof m['success'] === 'boolean')
99
+ out.success = m['success'];
100
+ const reasoning = typeof m['reasoningContent'] === 'string'
101
+ ? m['reasoningContent']
102
+ : typeof m['reasoning_content'] === 'string'
103
+ ? m['reasoning_content']
104
+ : undefined;
105
+ if (reasoning)
106
+ out.reasoningContent = reasoning;
107
+ return out;
108
+ });
109
+ }
110
+ /** llm Span create 时的 input:组装后的 system、工具短目录、本回合 messages。 */
111
+ export function llmSpanInput(input) {
112
+ const tools = input.tools?.filter((t) => t.name);
113
+ return {
114
+ ...(input.system ? { system: input.system } : {}),
115
+ ...(tools && tools.length > 0 ? { tools } : {}),
116
+ messages: spanGraphMessages(input.messages),
117
+ };
118
+ }
119
+ /** llm Span complete 的 output:本回合新增 assistant(含 toolCalls)或 silentOutput。 */
120
+ export function llmSpanOutputFromResult(result) {
121
+ if (typeof result['silentOutput'] === 'string') {
122
+ return { content: result['silentOutput'] };
123
+ }
124
+ const messages = Array.isArray(result['messages']) ? result['messages'] : [];
125
+ const added = messages[0];
126
+ if (!added || typeof added !== 'object')
127
+ return undefined;
128
+ const m = added;
129
+ const out = {
130
+ content: typeof m['content'] === 'string' ? m['content'] : '',
131
+ };
132
+ const toolCalls = m['toolCalls'] ?? m['tool_calls'];
133
+ if (Array.isArray(toolCalls) && toolCalls.length > 0)
134
+ out.toolCalls = toolCalls;
135
+ if (typeof m['reasoningContent'] === 'string' && m['reasoningContent']) {
136
+ out.reasoningContent = m['reasoningContent'];
137
+ }
138
+ return out;
139
+ }
@@ -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) {
@@ -30,6 +30,19 @@ export interface ToolResultPayload {
30
30
  content: unknown;
31
31
  /** 是否执行成功 */
32
32
  success: boolean;
33
+ /** 与 ToolCallResult.file 同形;SSE / persist / 注入共用 */
34
+ file?: {
35
+ fileName: string;
36
+ fileType: string;
37
+ size: number;
38
+ url: string;
39
+ };
40
+ files?: Array<{
41
+ fileName: string;
42
+ fileType: string;
43
+ size: number;
44
+ url: string;
45
+ }>;
33
46
  }
34
47
  /** 工具执行进度 */
35
48
  export type ToolProgressStatus = 'started' | 'completed' | 'error' | (string & {});
@@ -52,6 +65,8 @@ export interface ToolProgressPayload {
52
65
  content?: unknown;
53
66
  /** status=error 时可选:错误信息 */
54
67
  error?: string;
68
+ file?: ToolResultPayload['file'];
69
+ files?: ToolResultPayload['files'];
55
70
  /** 执行耗时(毫秒),status=completed|error 时可选 */
56
71
  executionTime?: number;
57
72
  }
@@ -65,4 +80,18 @@ export interface ToolErrorPayload {
65
80
  error: string;
66
81
  /** 是否会自动重试 */
67
82
  willRetry: boolean;
83
+ /** 失败且有交付物时保留原正文(与 ToolCallResult.content 同形) */
84
+ content?: unknown;
85
+ file?: {
86
+ fileName: string;
87
+ fileType: string;
88
+ size: number;
89
+ url: string;
90
+ };
91
+ files?: Array<{
92
+ fileName: string;
93
+ fileType: string;
94
+ size: number;
95
+ url: string;
96
+ }>;
68
97
  }
@@ -27,6 +27,6 @@ export declare function ensurePythonShims(workdir: string): string | undefined;
27
27
  * spawn 前替换成绝对路径,不依赖壳去展开。
28
28
  */
29
29
  export declare function substituteSkillDir(command: string, skillDir?: string): string;
30
- export declare function sandboxEnv(workdir: string, cwd: string, allowlist: string[], shimDir?: string, skillDir?: string): NodeJS.ProcessEnv;
30
+ export declare function sandboxEnv(workdir: string, cwd: string, allowlist: string[], shimDir?: string, skillDir?: string, homeDir?: string): NodeJS.ProcessEnv;
31
31
  export declare function formatSpawnError(err: unknown, shellFile: string): Error;
32
32
  export declare function killProcessTree(child: ChildProcess): void;
@@ -21,7 +21,7 @@ export function commandDialectLines(platform = process.platform) {
21
21
  else {
22
22
  lines.push('基础命令:当前目录 `pwd`;列目录 `ls`;环境变量 `$VAR`;串命令用 `&&`。');
23
23
  }
24
- lines.push('解释器:`python`/`python3`、`node`/`nodejs`、`sh`/`bash`(Windows 上 `sh` 需本机有 Git Bash 等)。', '可执行文件:用绝对路径或相对于 cwd(Windows `.exe`/`.bat`/`.cmd`;Unix 二进制需可执行或用 `sh`)。', '`execute_command` 的 cwd 是用户目录。技能包只读。包内任意文件用 `EAP_SKILL_DIR` 加相对路径(不限 `scripts/`)。读到的 SKILL.md 里本包存在的相对路径已展开为绝对路径。不要把 cwd 设成技能目录。相对写出在用户目录。');
24
+ lines.push('解释器:`python`/`python3`、`node`/`nodejs`、`sh`/`bash`(Windows 上 `sh` 需本机有 Git Bash 等)。', '可执行文件:用绝对路径或相对于 cwd(Windows `.exe`/`.bat`/`.cmd`;Unix 二进制需可执行或用 `sh`)。', '`execute_command` 的 cwd 默认是用户目录下的 `files/`。技能包只读。包内任意文件用 `EAP_SKILL_DIR` 加相对路径(不限 `scripts/`)。读到的 SKILL.md 里本包存在的相对路径已展开为绝对路径。不要把 cwd 设成技能目录、`work/` 或 `public/`。相对写出在 `files/`。');
25
25
  return lines;
26
26
  }
27
27
  export function resolveShellSpawn(command, platform = process.platform) {
@@ -179,22 +179,23 @@ export function substituteSkillDir(command, skillDir) {
179
179
  .replace(/\$EAP_SKILL_DIR\b/g, dir)
180
180
  .replace(/%EAP_SKILL_DIR%/gi, dir);
181
181
  }
182
- export function sandboxEnv(workdir, cwd, allowlist, shimDir, skillDir) {
182
+ export function sandboxEnv(workdir, cwd, allowlist, shimDir, skillDir, homeDir) {
183
183
  const win = process.platform === 'win32';
184
184
  const hostPath = process.env.PATH
185
185
  ?? (win
186
186
  ? (process.env.SystemRoot ? path.join(process.env.SystemRoot, 'System32') : 'C:\\Windows\\System32')
187
187
  : '/usr/bin:/bin:/usr/local/bin');
188
188
  const pathValue = shimDir ? `${shimDir}${path.delimiter}${hostPath}` : hostPath;
189
+ const home = homeDir?.trim() ? path.resolve(homeDir) : workdir;
189
190
  const env = {
190
191
  PATH: pathValue,
191
192
  LANG: process.env.LANG ?? 'C.UTF-8',
192
193
  LC_ALL: process.env.LC_ALL ?? process.env.LANG ?? 'C.UTF-8',
193
- HOME: workdir,
194
- USERPROFILE: workdir,
195
- TMPDIR: workdir,
196
- TMP: workdir,
197
- TEMP: workdir,
194
+ HOME: home,
195
+ USERPROFILE: home,
196
+ TMPDIR: home,
197
+ TMP: home,
198
+ TEMP: home,
198
199
  PWD: cwd,
199
200
  PYTHONUTF8: '1',
200
201
  PYTHONIOENCODING: 'utf-8',
@@ -2,9 +2,9 @@
2
2
  * 平台默认 Harness 原语:read_file / write_file / execute_command。
3
3
  *
4
4
  * 不写进 agent.manifest.toolsets。同名业务工具优先(问数 file-io 等)。
5
- * 写:`users/<userId>/`(含用户自己的 skills)。
5
+ * 写:`dirname(agentDir)/users/<userId>/`(含用户自己的 skills)。产物默认 `files/`,公开副本 `public/`。
6
6
  * 读:写根 + 已装配 `preinstall/tools`、`preinstall/skills`。
7
- * 命令:cwd 仅写根。技能包只读,EAP_SKILL_DIR 为整包绝对根(不限 scripts/)。不把预装拷进用户装配槽。
7
+ * 命令:cwd 默认 `files/`。技能包只读,EAP_SKILL_DIR 为整包绝对根(不限 scripts/)。不把预装拷进用户装配槽。
8
8
  * 开发态 overlay:`EAP_LOCAL_SKILLS_DIR` / `EAP_LOCAL_TOOLS_DIR`(只读)。在 `createDevRuntime` /
9
9
  * `createToolExecutor` 时钉进执行器 extraRoots;SDK `createAgent` 构造完会恢复 process.env,
10
10
  * 请求时不再读这两项。相对 `read_file` 会把 overlay 根下直接子目录里带 SKILL.md 的包纳入唯一命中。
@@ -19,7 +19,7 @@ export interface HarnessJailOptions {
19
19
  workdir?: string;
20
20
  extraRoots?: string[];
21
21
  }
22
- /** `{agentDir}/users/<userId>`(写根;含用户 skills) */
22
+ /** `dirname(agentDir)/users/<userId>`(写根;含用户 skills) */
23
23
  export declare function resolveUserDir(agentDir: string, userId: string): string;
24
24
  export declare function collectToolBundlePaths(loadedTools?: LoadedToolMap): string[];
25
25
  export declare function resolveJailFromState(state: {