@gt-fe/eap-runtime 0.0.30 → 0.0.32
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.
- package/dist/api/handlers/invoke.handler.js +9 -5
- package/dist/api/handlers/stream.handler.js +15 -4
- package/dist/common/index.d.ts +1 -0
- package/dist/common/index.js +1 -0
- package/dist/common/user-root.d.ts +8 -0
- package/dist/common/user-root.js +19 -0
- package/dist/graph/adapters/langgraph.adapter.js +57 -10
- package/dist/graph/adapters/skill-entry-span.d.ts +4 -2
- package/dist/graph/adapters/skill-entry-span.js +10 -3
- package/dist/graph/adapters/skill-read.d.ts +18 -2
- package/dist/graph/adapters/skill-read.js +66 -31
- package/dist/graph/contracts/executable-graph.d.ts +7 -1
- package/dist/graph/nodes/tool-node.d.ts +6 -1
- package/dist/graph/nodes/tool-node.js +18 -3
- package/dist/registry/deployment-snapshot.d.ts +1 -1
- package/dist/registry/deployment-snapshot.js +6 -3
- package/dist/registry/skill-tree.d.ts +1 -1
- package/dist/registry/skill-tree.js +11 -6
- package/dist/runtime/agent-runtime.js +36 -5
- package/dist/session/persist-turn-messages.d.ts +13 -6
- package/dist/session/persist-turn-messages.js +4 -0
- package/dist/sse/events/tool-events.d.ts +29 -0
- package/dist/tool/harness-command.d.ts +1 -1
- package/dist/tool/harness-command.js +8 -7
- package/dist/tool/harness-primitives.d.ts +4 -10
- package/dist/tool/harness-primitives.js +189 -130
- package/dist/tool/index.d.ts +2 -0
- package/dist/tool/index.js +2 -0
- package/dist/tool/inject-public-artifact-links.d.ts +32 -0
- package/dist/tool/inject-public-artifact-links.js +142 -0
- package/dist/tool/skill-relative-read.d.ts +14 -0
- package/dist/tool/skill-relative-read.js +98 -0
- package/dist/tool/static-artifact-url.d.ts +66 -0
- package/dist/tool/static-artifact-url.js +561 -0
- package/dist/tool/system-prompt-builder.js +8 -4
- package/dist/tool/tool-event-emitter.d.ts +2 -1
- package/dist/tool/tool-event-emitter.js +30 -18
- package/dist/tool/tool-executor.interface.d.ts +13 -1
- package/dist/tool/tool-result-injector.d.ts +3 -3
- package/dist/tool/tool-result-injector.js +7 -3
- 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:
|
|
397
|
-
fallbackAssistantContent:
|
|
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:
|
|
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:
|
|
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:
|
|
438
|
+
assistantTexts: suffix ? undefined : streamAssistantTexts(assistantBuckets),
|
|
439
|
+
assistantText: injected,
|
|
429
440
|
finishPoints: graph.finishPoints,
|
|
430
441
|
toolCallReasoningContent,
|
|
431
442
|
assistantReasoningContent,
|
package/dist/common/index.d.ts
CHANGED
package/dist/common/index.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -34,8 +34,8 @@ import { preloadSkillNotFound } from '../../registry/preload-errors.js';
|
|
|
34
34
|
import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
|
|
35
35
|
import { pickContextFields } from '../../token/context-window.js';
|
|
36
36
|
import { isHitlDecision, lastAssistantHasUnansweredToolCalls } from '../hitl/hitl-handler.js';
|
|
37
|
-
import {
|
|
38
|
-
import { skillEntryInvocationFromTool } from './skill-entry-span.js';
|
|
37
|
+
import { classifySkillFile, isReadTool } from './skill-read.js';
|
|
38
|
+
import { recordSkillEntrySpan, skillEntryInvocationFromTool } from './skill-entry-span.js';
|
|
39
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';
|
|
@@ -283,8 +283,10 @@ export class LangGraphAdapter {
|
|
|
283
283
|
...(progress.node ? { node: progress.node } : {}),
|
|
284
284
|
...(progress.skillId ? { skillId: progress.skillId } : {}),
|
|
285
285
|
};
|
|
286
|
-
const skillRead =
|
|
287
|
-
|
|
286
|
+
const skillRead = progress.status === 'completed' && progress.resolvedPath && isReadTool(progress.toolName)
|
|
287
|
+
? classifySkillFile(progress.resolvedPath, progress.skillDirs)
|
|
288
|
+
: null;
|
|
289
|
+
if (skillRead) {
|
|
288
290
|
if (skillRead.kind === 'loaded') {
|
|
289
291
|
const skillId = skillRead.skillId || loc.skillId || skillRead.path;
|
|
290
292
|
stream.sink({
|
|
@@ -299,7 +301,7 @@ export class LangGraphAdapter {
|
|
|
299
301
|
});
|
|
300
302
|
const catalogSkill = skillEntryInvocationFromTool({
|
|
301
303
|
toolName: progress.toolName,
|
|
302
|
-
|
|
304
|
+
resolvedPath: progress.resolvedPath,
|
|
303
305
|
skillDirs: progress.skillDirs,
|
|
304
306
|
alreadyInSkillSpan: progress.inSkillSubgraph,
|
|
305
307
|
});
|
|
@@ -355,12 +357,14 @@ export class LangGraphAdapter {
|
|
|
355
357
|
},
|
|
356
358
|
});
|
|
357
359
|
}
|
|
360
|
+
// resolvedPath / skillDirs 只给上面 classifySkillFile;inSkillSubgraph 只给 skill.started 去重。不进对话流。
|
|
361
|
+
const { resolvedPath: _resolvedPath, skillDirs: _skillDirs, inSkillSubgraph: _inSkillSubgraph, ...progressView } = progress;
|
|
358
362
|
stream.sink({
|
|
359
363
|
type: 'tool.progress',
|
|
360
364
|
taskId,
|
|
361
365
|
correlationId,
|
|
362
366
|
timestamp,
|
|
363
|
-
payload:
|
|
367
|
+
payload: progressView,
|
|
364
368
|
});
|
|
365
369
|
if (progress.status === 'completed') {
|
|
366
370
|
stream.sink({
|
|
@@ -374,6 +378,8 @@ export class LangGraphAdapter {
|
|
|
374
378
|
...(progress.toolset ? { toolset: progress.toolset } : {}),
|
|
375
379
|
content: progress.content,
|
|
376
380
|
success: true,
|
|
381
|
+
...(progress.file ? { file: progress.file } : {}),
|
|
382
|
+
...(progress.files?.length ? { files: progress.files } : {}),
|
|
377
383
|
...loc,
|
|
378
384
|
},
|
|
379
385
|
});
|
|
@@ -390,6 +396,9 @@ export class LangGraphAdapter {
|
|
|
390
396
|
...(progress.toolset ? { toolset: progress.toolset } : {}),
|
|
391
397
|
error: progress.error ?? 'Unknown error',
|
|
392
398
|
willRetry: progress.willRetry === true,
|
|
399
|
+
...(progress.content ? { content: progress.content } : {}),
|
|
400
|
+
...(progress.file ? { file: progress.file } : {}),
|
|
401
|
+
...(progress.files?.length ? { files: progress.files } : {}),
|
|
393
402
|
...loc,
|
|
394
403
|
},
|
|
395
404
|
});
|
|
@@ -931,6 +940,13 @@ export class LangGraphAdapter {
|
|
|
931
940
|
node: node.id,
|
|
932
941
|
});
|
|
933
942
|
const skillDefs = state['skillDefinitions'];
|
|
943
|
+
const jail = resolveJailFromState({
|
|
944
|
+
agentDir: state['agentDir'],
|
|
945
|
+
userId: state['userId'],
|
|
946
|
+
executionContext: state['executionContext'],
|
|
947
|
+
loadedTools: state['loadedTools'],
|
|
948
|
+
skillDefinitions: skillDefs,
|
|
949
|
+
});
|
|
934
950
|
const jailExtraRoots = Object.values(skillDefs ?? {})
|
|
935
951
|
.map((skill) => skill.bundlePath)
|
|
936
952
|
.filter((p) => Boolean(p));
|
|
@@ -941,6 +957,7 @@ export class LangGraphAdapter {
|
|
|
941
957
|
authContext: { userId, delegationToken, agentCode },
|
|
942
958
|
correlationId,
|
|
943
959
|
taskId,
|
|
960
|
+
...(jail.workdir ? { jailWorkdir: jail.workdir } : {}),
|
|
944
961
|
...(jailExtraRoots.length > 0 ? { jailExtraRoots } : {}),
|
|
945
962
|
};
|
|
946
963
|
const retry = resolveToolRetryConfig(node.retry);
|
|
@@ -976,13 +993,21 @@ export class LangGraphAdapter {
|
|
|
976
993
|
executionTime,
|
|
977
994
|
node: node.id,
|
|
978
995
|
...(skillDirs.length > 0 ? { skillDirs } : {}),
|
|
996
|
+
...(result.resolvedPath ? { resolvedPath: result.resolvedPath } : {}),
|
|
979
997
|
});
|
|
998
|
+
if (result.success) {
|
|
999
|
+
await recordSkillEntrySpan(state, {
|
|
1000
|
+
toolName: result.toolName,
|
|
1001
|
+
resolvedPath: result.resolvedPath,
|
|
1002
|
+
skillDirs,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
980
1005
|
// 重要:messages channel 的 reducer 是 append 语义,只返回新增消息
|
|
981
1006
|
return {
|
|
982
1007
|
messages: [
|
|
983
1008
|
{
|
|
984
1009
|
role: 'tool',
|
|
985
|
-
content: result.content
|
|
1010
|
+
content: result.content ?? '',
|
|
986
1011
|
toolCallId,
|
|
987
1012
|
toolName,
|
|
988
1013
|
success: result.success ?? true,
|
|
@@ -1592,10 +1617,20 @@ export class LangGraphAdapter {
|
|
|
1592
1617
|
continue;
|
|
1593
1618
|
}
|
|
1594
1619
|
if (msg.role === 'tool') {
|
|
1595
|
-
// tool.result / tool.error 由 Tool
|
|
1620
|
+
// tool.result / tool.error 由 Tool 节点旁路发出(已带 file/files);此处再补 file.created
|
|
1596
1621
|
const raw = typeof msg.content === 'string' ? msg.content : '';
|
|
1597
|
-
const
|
|
1598
|
-
|
|
1622
|
+
const files = [
|
|
1623
|
+
...(msg.file ? [msg.file] : []),
|
|
1624
|
+
...(msg.files ?? []),
|
|
1625
|
+
];
|
|
1626
|
+
if (files.length === 0) {
|
|
1627
|
+
const parsed = fileArtifactFromToolOutput(raw);
|
|
1628
|
+
if (parsed)
|
|
1629
|
+
files.push(parsed);
|
|
1630
|
+
}
|
|
1631
|
+
for (const file of files) {
|
|
1632
|
+
if (!file?.url)
|
|
1633
|
+
continue;
|
|
1599
1634
|
yield {
|
|
1600
1635
|
type: 'file.created',
|
|
1601
1636
|
taskId: undefined,
|
|
@@ -1662,6 +1697,7 @@ export class LangGraphAdapter {
|
|
|
1662
1697
|
success: typeof m['success'] === 'boolean' ? m['success'] : undefined,
|
|
1663
1698
|
reasoningContent: typeof m['reasoningContent'] === 'string' ? m['reasoningContent'] : undefined,
|
|
1664
1699
|
file: readFileArtifact(m['file']),
|
|
1700
|
+
files: readFileArtifactList(m['files']),
|
|
1665
1701
|
};
|
|
1666
1702
|
});
|
|
1667
1703
|
}
|
|
@@ -1679,3 +1715,14 @@ function readFileArtifact(value) {
|
|
|
1679
1715
|
url: rec.url,
|
|
1680
1716
|
};
|
|
1681
1717
|
}
|
|
1718
|
+
function readFileArtifactList(value) {
|
|
1719
|
+
if (!Array.isArray(value))
|
|
1720
|
+
return undefined;
|
|
1721
|
+
const out = [];
|
|
1722
|
+
for (const item of value) {
|
|
1723
|
+
const file = readFileArtifact(item);
|
|
1724
|
+
if (file)
|
|
1725
|
+
out.push(file);
|
|
1726
|
+
}
|
|
1727
|
+
return out.length > 0 ? out : undefined;
|
|
1728
|
+
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* 成功读 SKILL.md 时写 Trace `type=skill` Span。
|
|
3
3
|
*
|
|
4
4
|
* 假设:Skill 被调用 = 一次成功读取技能包入口(SKILL.md),
|
|
5
|
+
* 入口是执行器返回的实际文件(resolvedPath)对已装配 skillDirs。
|
|
6
|
+
* 没有实际路径不记,不再用 arguments.path 猜。
|
|
5
7
|
* 与子图激活走同一 collector.createNode({ type: 'skill' })。
|
|
6
8
|
* 声明版本在 createNode.version;skillSpace 只在 input。
|
|
7
9
|
* 子图已有 skillSpanId 时不记第二条。write_file / 包内附属读 / 失败读不是调用。
|
|
@@ -23,7 +25,7 @@ export declare function skillSpanFields(identity: SkillTraceIdentity, extra?: {
|
|
|
23
25
|
};
|
|
24
26
|
export declare function skillEntryInvocationFromTool(input: {
|
|
25
27
|
toolName: string;
|
|
26
|
-
|
|
28
|
+
resolvedPath?: string;
|
|
27
29
|
skillDirs?: SkillDirRef[];
|
|
28
30
|
alreadyInSkillSpan?: boolean;
|
|
29
31
|
}): {
|
|
@@ -32,6 +34,6 @@ export declare function skillEntryInvocationFromTool(input: {
|
|
|
32
34
|
} | null;
|
|
33
35
|
export declare function recordSkillEntrySpan(state: Record<string, unknown>, input: {
|
|
34
36
|
toolName: string;
|
|
35
|
-
|
|
37
|
+
resolvedPath?: string;
|
|
36
38
|
skillDirs?: SkillDirRef[];
|
|
37
39
|
}): Promise<void>;
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* 成功读 SKILL.md 时写 Trace `type=skill` Span。
|
|
3
3
|
*
|
|
4
4
|
* 假设:Skill 被调用 = 一次成功读取技能包入口(SKILL.md),
|
|
5
|
+
* 入口是执行器返回的实际文件(resolvedPath)对已装配 skillDirs。
|
|
6
|
+
* 没有实际路径不记,不再用 arguments.path 猜。
|
|
5
7
|
* 与子图激活走同一 collector.createNode({ type: 'skill' })。
|
|
6
8
|
* 声明版本在 createNode.version;skillSpace 只在 input。
|
|
7
9
|
* 子图已有 skillSpanId 时不记第二条。write_file / 包内附属读 / 失败读不是调用。
|
|
8
10
|
*/
|
|
9
|
-
import {
|
|
11
|
+
import { classifySkillFile, classifySkillSpace, isReadTool, } from './skill-read.js';
|
|
10
12
|
import { completeGraphSpan, startGraphSpan } from '../trace-span.js';
|
|
11
13
|
function declaredSkillVersion(skillId, version) {
|
|
12
14
|
const raw = version?.trim();
|
|
@@ -46,7 +48,12 @@ export function skillSpanFields(identity, extra) {
|
|
|
46
48
|
export function skillEntryInvocationFromTool(input) {
|
|
47
49
|
if (input.alreadyInSkillSpan)
|
|
48
50
|
return null;
|
|
49
|
-
|
|
51
|
+
if (!isReadTool(input.toolName))
|
|
52
|
+
return null;
|
|
53
|
+
const resolved = input.resolvedPath?.trim();
|
|
54
|
+
if (!resolved)
|
|
55
|
+
return null;
|
|
56
|
+
const hit = classifySkillFile(resolved, input.skillDirs);
|
|
50
57
|
if (!hit || hit.kind !== 'loaded' || !hit.skillId)
|
|
51
58
|
return null;
|
|
52
59
|
return { skillId: hit.skillId, path: hit.path };
|
|
@@ -55,7 +62,7 @@ export async function recordSkillEntrySpan(state, input) {
|
|
|
55
62
|
const alreadyInSkillSpan = typeof state['skillSpanId'] === 'string' && Boolean(state['skillSpanId']);
|
|
56
63
|
const hit = skillEntryInvocationFromTool({
|
|
57
64
|
toolName: input.toolName,
|
|
58
|
-
|
|
65
|
+
resolvedPath: input.resolvedPath,
|
|
59
66
|
skillDirs: input.skillDirs,
|
|
60
67
|
alreadyInSkillSpan,
|
|
61
68
|
});
|
|
@@ -12,10 +12,26 @@ export declare function classifySkillSpace(bundleOrFilePath: string, opts?: {
|
|
|
12
12
|
agentDir?: string;
|
|
13
13
|
userId?: string;
|
|
14
14
|
}): SkillSpace | undefined;
|
|
15
|
+
/** 与 Harness `read_file` 相同的相对路径定位(workdir / skillDir / overlay extraRoots)。 */
|
|
16
|
+
export interface SkillReadLocate {
|
|
17
|
+
workdir?: string;
|
|
18
|
+
skillDir?: string;
|
|
19
|
+
extraRoots?: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare function isReadTool(toolName: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* 实际读到的文件是否落在已装配技能包内。不解析定位符。
|
|
24
|
+
*/
|
|
25
|
+
export declare function classifySkillFile(absPath: string, skillDirs?: SkillDirRef[]): {
|
|
26
|
+
kind: 'loaded' | 'resource';
|
|
27
|
+
path: string;
|
|
28
|
+
skillId: string;
|
|
29
|
+
} | null;
|
|
15
30
|
/**
|
|
16
|
-
*
|
|
31
|
+
* 给 EAP_SKILL_DIR 推断:把 `arguments.path` 按 Harness 规则落到文件再对 skillDirs。
|
|
32
|
+
* Trace / SSE 入口判定走 `classifySkillFile`(实际读到的文件),不走这里。
|
|
17
33
|
*/
|
|
18
|
-
export declare function classifySkillRead(toolName: string, args: Record<string, unknown> | undefined, skillDirs?: SkillDirRef[]): {
|
|
34
|
+
export declare function classifySkillRead(toolName: string, args: Record<string, unknown> | undefined, skillDirs?: SkillDirRef[], locate?: SkillReadLocate): {
|
|
19
35
|
kind: 'loaded' | 'resource';
|
|
20
36
|
path: string;
|
|
21
37
|
skillId: string;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { realpathSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { userRoot } from '../../common/user-root.js';
|
|
4
|
+
import { resolveSkillReadTarget } from '../../tool/skill-relative-read.js';
|
|
3
5
|
function tryRealpath(p) {
|
|
4
6
|
try {
|
|
5
7
|
return realpathSync(p);
|
|
@@ -29,10 +31,6 @@ function longestSkillMatch(filePath, skillDirs) {
|
|
|
29
31
|
}
|
|
30
32
|
return best;
|
|
31
33
|
}
|
|
32
|
-
function skillIdFromSkillsSegment(norm) {
|
|
33
|
-
const match = norm.match(/(?:^|\/)(?:preinstall\/)?skills\/([^/]+)(?:\/|$)/i);
|
|
34
|
-
return match?.[1] ?? '';
|
|
35
|
-
}
|
|
36
34
|
/**
|
|
37
35
|
* 按实际 bundle 路径判定空间。有 agentDir 时相对预装根与 users/ 判定;
|
|
38
36
|
* 否则看路径段 `preinstall/skills` / `users/<id>/skills`。
|
|
@@ -49,12 +47,20 @@ export function classifySkillSpace(bundleOrFilePath, opts) {
|
|
|
49
47
|
return 'agent';
|
|
50
48
|
const uid = opts?.userId?.trim();
|
|
51
49
|
if (uid) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
try {
|
|
51
|
+
const user = normalizeForMatch(path.join(userRoot(agentDir, uid), 'skills'));
|
|
52
|
+
if (target === user || target.startsWith(`${user}/`))
|
|
53
|
+
return 'user';
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// 非法 userId 时不按用户槽判定
|
|
57
|
+
}
|
|
55
58
|
}
|
|
56
|
-
|
|
59
|
+
const usersRoot = normalizeForMatch(path.join(path.dirname(agentDir), 'users'));
|
|
60
|
+
if ((target === usersRoot || target.startsWith(`${usersRoot}/`)) &&
|
|
61
|
+
/(^|\/)skills(\/|$)/.test(target.slice(usersRoot.length))) {
|
|
57
62
|
return 'user';
|
|
63
|
+
}
|
|
58
64
|
return 'local';
|
|
59
65
|
}
|
|
60
66
|
if (/(^|\/)preinstall\/skills(\/|$)/i.test(target))
|
|
@@ -63,34 +69,63 @@ export function classifySkillSpace(bundleOrFilePath, opts) {
|
|
|
63
69
|
return 'user';
|
|
64
70
|
return undefined;
|
|
65
71
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
72
|
+
function extraRootsForSkillRead(skillDirs, locate) {
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const dir of skillDirs ?? []) {
|
|
75
|
+
if (dir.bundlePath)
|
|
76
|
+
out.push(dir.bundlePath);
|
|
77
|
+
}
|
|
78
|
+
for (const root of locate?.extraRoots ?? []) {
|
|
79
|
+
if (root)
|
|
80
|
+
out.push(root);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
export function isReadTool(toolName) {
|
|
85
|
+
const n = toolName.toLowerCase().replace(/-/g, '_');
|
|
86
|
+
return n === 'read_file' || n === 'read_skill_file' || n === 'readfile';
|
|
68
87
|
}
|
|
69
88
|
/**
|
|
70
|
-
*
|
|
89
|
+
* 实际读到的文件是否落在已装配技能包内。不解析定位符。
|
|
71
90
|
*/
|
|
72
|
-
export function
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
91
|
+
export function classifySkillFile(absPath, skillDirs) {
|
|
92
|
+
const trimmed = absPath.trim();
|
|
93
|
+
if (!trimmed)
|
|
94
|
+
return null;
|
|
95
|
+
const matched = longestSkillMatch(trimmed, skillDirs);
|
|
96
|
+
if (!matched)
|
|
97
|
+
return null;
|
|
98
|
+
const resolved = normalizeForMatch(trimmed);
|
|
99
|
+
const isEntry = /(^|\/)(SKILL\.md|skill.md)$/i.test(resolved);
|
|
100
|
+
return {
|
|
101
|
+
kind: isEntry ? 'loaded' : 'resource',
|
|
102
|
+
path: resolved,
|
|
103
|
+
skillId: matched.skillId,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 给 EAP_SKILL_DIR 推断:把 `arguments.path` 按 Harness 规则落到文件再对 skillDirs。
|
|
108
|
+
* Trace / SSE 入口判定走 `classifySkillFile`(实际读到的文件),不走这里。
|
|
109
|
+
*/
|
|
110
|
+
export function classifySkillRead(toolName, args, skillDirs, locate) {
|
|
111
|
+
if (!isReadTool(toolName))
|
|
75
112
|
return null;
|
|
76
113
|
const filePath = String(args?.path ?? args?.file ?? '');
|
|
77
114
|
if (!filePath)
|
|
78
115
|
return null;
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
116
|
+
const target = resolveSkillReadTarget(filePath, {
|
|
117
|
+
workdir: locate?.workdir,
|
|
118
|
+
skillDir: locate?.skillDir,
|
|
119
|
+
extraRoots: extraRootsForSkillRead(skillDirs, locate),
|
|
120
|
+
});
|
|
121
|
+
const matched = longestSkillMatch(target, skillDirs);
|
|
122
|
+
if (!matched)
|
|
123
|
+
return null;
|
|
124
|
+
const slash = target.replace(/\\/g, '/');
|
|
125
|
+
const isEntry = /(^|\/)(SKILL\.md|skill.md)$/i.test(slash);
|
|
126
|
+
return {
|
|
127
|
+
kind: isEntry ? 'loaded' : 'resource',
|
|
128
|
+
path: target,
|
|
129
|
+
skillId: matched.skillId,
|
|
130
|
+
};
|
|
96
131
|
}
|
|
@@ -276,13 +276,19 @@ export interface GraphMessage {
|
|
|
276
276
|
* 多轮请求必须原样回传,否则上游会 400。
|
|
277
277
|
*/
|
|
278
278
|
reasoningContent?: string;
|
|
279
|
-
/**
|
|
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,10 +41,15 @@ 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;
|
|
47
50
|
skillDirs?: SkillDirRef[];
|
|
51
|
+
/** read_file 实际读到的文件;与 ToolCallResult.resolvedPath 相同 */
|
|
52
|
+
resolvedPath?: string;
|
|
48
53
|
/** 已在 Skill 子图内(state.skillSpanId);读 SKILL.md 不再发 skill.started */
|
|
49
54
|
inSkillSubgraph?: boolean;
|
|
50
55
|
skillSpace?: 'agent' | 'user' | 'local';
|
|
@@ -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 ?? {})
|
|
@@ -287,7 +295,7 @@ export class ToolNode {
|
|
|
287
295
|
if (result.success) {
|
|
288
296
|
const catalog = skillEntryInvocationFromTool({
|
|
289
297
|
toolName: req.toolName,
|
|
290
|
-
|
|
298
|
+
resolvedPath: result.resolvedPath,
|
|
291
299
|
skillDirs,
|
|
292
300
|
alreadyInSkillSpan: inSkillSubgraph,
|
|
293
301
|
});
|
|
@@ -302,7 +310,9 @@ export class ToolNode {
|
|
|
302
310
|
status: 'completed',
|
|
303
311
|
arguments: req.arguments,
|
|
304
312
|
content: result.content,
|
|
313
|
+
...toolArtifactFields(result),
|
|
305
314
|
executionTime,
|
|
315
|
+
...(result.resolvedPath ? { resolvedPath: result.resolvedPath } : {}),
|
|
306
316
|
...(skillIdentity?.skillSpace ? { skillSpace: skillIdentity.skillSpace } : {}),
|
|
307
317
|
...(skillIdentity?.skillVersion ? { skillVersion: skillIdentity.skillVersion } : {}),
|
|
308
318
|
...(skillIdentity?.skillName ? { skillName: skillIdentity.skillName } : {}),
|
|
@@ -317,6 +327,7 @@ export class ToolNode {
|
|
|
317
327
|
status: 'error',
|
|
318
328
|
arguments: req.arguments,
|
|
319
329
|
error: result.error ?? 'unknown error',
|
|
330
|
+
...toolArtifactFields(result),
|
|
320
331
|
executionTime,
|
|
321
332
|
});
|
|
322
333
|
}
|
|
@@ -336,7 +347,7 @@ export class ToolNode {
|
|
|
336
347
|
if (result.success) {
|
|
337
348
|
await recordSkillEntrySpan(state, {
|
|
338
349
|
toolName: req.toolName,
|
|
339
|
-
|
|
350
|
+
resolvedPath: result.resolvedPath,
|
|
340
351
|
skillDirs,
|
|
341
352
|
});
|
|
342
353
|
}
|
|
@@ -421,12 +432,15 @@ export class ToolNode {
|
|
|
421
432
|
role: 'tool',
|
|
422
433
|
content: result.success
|
|
423
434
|
? result.content
|
|
424
|
-
:
|
|
435
|
+
: toolResultHasArtifacts(result) && result.content
|
|
436
|
+
? `${result.content}\n[Tool Error] ${result.error ?? 'unknown error'}`
|
|
437
|
+
: `[Tool Error] ${result.error ?? 'unknown error'}`,
|
|
425
438
|
// 以下字段符合 LLM function-calling 协议(ToolMessage 接口)
|
|
426
439
|
toolCallId: result.toolCallId,
|
|
427
440
|
toolName: result.toolName,
|
|
428
441
|
success: result.success,
|
|
429
442
|
...('file' in result && result.file ? { file: result.file } : {}),
|
|
443
|
+
...('files' in result && result.files?.length ? { files: result.files } : {}),
|
|
430
444
|
}));
|
|
431
445
|
const lastSpanId = [...spanIds].reverse().find((id) => Boolean(id));
|
|
432
446
|
return {
|
|
@@ -477,6 +491,7 @@ export class ToolNode {
|
|
|
477
491
|
status: 'error',
|
|
478
492
|
arguments: req.arguments,
|
|
479
493
|
error: last.error ?? 'unknown error',
|
|
494
|
+
...toolArtifactFields(last),
|
|
480
495
|
willRetry: true,
|
|
481
496
|
});
|
|
482
497
|
await sleepToolRetry(toolRetryDelayMs(attempt, retry.backoff));
|