diff --git a/packages/@n8n/instance-ai/src/tools/orchestration/build-workflow-agent.tool.ts b/packages/@n8n/instance-ai/src/tools/orchestration/build-workflow-agent.tool.ts index e94ed47d0d1..7778daa1361 100644 --- a/packages/@n8n/instance-ai/src/tools/orchestration/build-workflow-agent.tool.ts +++ b/packages/@n8n/instance-ai/src/tools/orchestration/build-workflow-agent.tool.ts @@ -59,6 +59,16 @@ interface BuilderMemoryBinding { thread: string; } +function toToolRegistry( + tools: ReadonlyArray, +): InstanceAiToolRegistry { + const registry: InstanceAiToolRegistry = {}; + for (const tool of tools) { + registry[tool.name] = tool; + } + return registry; +} + function createBuilderResourceId(userId: string): string { return `${userId}:workflow-builder`; } @@ -850,6 +860,7 @@ export async function startBuildWorkflowAgentTask( builderTools, 'workflow-builder', ); + const runtimeWorkspaceTools = toToolRegistry(workspace.getTools()); const shouldUseBuilderMemory = false; const subAgent = new Agent('Workflow Builder Agent') @@ -876,6 +887,7 @@ export async function startBuildWorkflowAgentTask( buildAgentTraceInputs({ systemPrompt: prompt, tools: tracedBuilderTools, + runtimeTools: runtimeWorkspaceTools, modelId: context.modelId, }), ); diff --git a/packages/@n8n/instance-ai/src/tools/orchestration/plan-with-agent.tool.ts b/packages/@n8n/instance-ai/src/tools/orchestration/plan-with-agent.tool.ts index b94de0bf43c..065224c9c4d 100644 --- a/packages/@n8n/instance-ai/src/tools/orchestration/plan-with-agent.tool.ts +++ b/packages/@n8n/instance-ai/src/tools/orchestration/plan-with-agent.tool.ts @@ -31,6 +31,7 @@ import { } from './tracing-utils'; import { MAX_STEPS } from '../../constants/max-steps'; import { consumeStreamWithHitl } from '../../stream/consume-with-hitl'; +import { buildAgentTraceInputs, mergeTraceRunInputs } from '../../tracing/langsmith-tracing'; import type { InstanceAiToolRegistry, OrchestrationContext } from '../../types'; import { createTemplatesTool } from '../templates.tool'; @@ -321,6 +322,14 @@ export function createPlanWithAgentTool(context: OrchestrationContext) { if (telemetry) { subAgent.telemetry(telemetry); } + mergeTraceRunInputs( + traceRun, + buildAgentTraceInputs({ + systemPrompt: PLANNER_AGENT_PROMPT, + tools: tracedPlannerTools, + modelId: context.modelId, + }), + ); const resultText = await withTraceRun(context, traceRun, async () => { const stream = await subAgent.stream(briefing, { diff --git a/packages/@n8n/instance-ai/src/tools/orchestration/tracing-utils.ts b/packages/@n8n/instance-ai/src/tools/orchestration/tracing-utils.ts index f7e32397520..b601c8416d9 100644 --- a/packages/@n8n/instance-ai/src/tools/orchestration/tracing-utils.ts +++ b/packages/@n8n/instance-ai/src/tools/orchestration/tracing-utils.ts @@ -31,7 +31,8 @@ export async function startSubAgentTrace( if (!context.tracing) return undefined; return await context.tracing.startChildRun(context.tracing.actorRun, { - name: `instance-ai.subagent.${options.role}.stream`, + name: `agent: ${options.role}`, + canonicalName: `instance-ai.subagent.${options.role}.stream`, tags: ['sub-agent'], metadata: { agent_role: options.role, diff --git a/packages/@n8n/instance-ai/src/tracing/__tests__/langsmith-tracing.test.ts b/packages/@n8n/instance-ai/src/tracing/__tests__/langsmith-tracing.test.ts index dcb2e84f46a..e727352eb0b 100644 --- a/packages/@n8n/instance-ai/src/tracing/__tests__/langsmith-tracing.test.ts +++ b/packages/@n8n/instance-ai/src/tracing/__tests__/langsmith-tracing.test.ts @@ -469,7 +469,8 @@ async function startForegroundActor( tracing: NonNullable>>, ) { const actorRun = await tracing.startChildRun(tracing.rootRun, { - name: 'instance-ai.agent.orchestrator', + name: 'agent: orchestrator', + canonicalName: 'instance-ai.agent.orchestrator', tags: ['orchestrator'], metadata: { agent_role: 'orchestrator', @@ -536,6 +537,15 @@ describe('createInstanceAiTraceContext', () => { }); expect(tracing).toBeDefined(); + expect(tracing?.rootRun.name).toBe('turn'); + expect(tracing?.rootRun.metadata).toEqual( + expect.objectContaining({ + display_name: 'turn', + display_kind: 'turn', + display_group: 'message-turn', + 'instance_ai.canonical_name': 'instance-ai.message_turn', + }), + ); await startForegroundActor(tracing!); expect(tracing?.orchestratorRun.parentRunId).toBe(tracing?.messageRun.id); }); @@ -737,6 +747,28 @@ describe('createInstanceAiTraceContext', () => { }); }); + it('renames native LLM spans for LangSmith display while keeping SDK operation metadata', () => { + const span = { + name: 'ai.streamText.doStream', + attributes: { + 'ai.operationId': 'ai.streamText.doStream', + 'ai.telemetry.metadata.agent_role': 'workflow-builder', + }, + }; + + const redacted = redactLangSmithTelemetrySpan(span) as { + name: string; + attributes: Record; + }; + + expect(redacted.name).toBe('llm: workflow-builder'); + expect(redacted.attributes['langsmith.trace.name']).toBe('llm: workflow-builder'); + expect(redacted.attributes['ai_sdk.operation']).toBe('ai.streamText.doStream'); + expect(redacted.attributes['instance_ai.canonical_name']).toBe('ai.streamText.doStream'); + expect(redacted.attributes.display_kind).toBe('llm'); + expect(redacted.attributes.display_group).toBe('workflow-builder'); + }); + it('normalizes AI SDK tool messages for LangSmith chat rendering', () => { const span = { attributes: { @@ -881,7 +913,12 @@ describe('createInstanceAiTraceContext', () => { expect(continuedTracing.traceKind).toBe('orchestrator_resume'); expect(continuedTracing.rootRun.id).not.toBe(tracing?.rootRun.id); expect(continuedTracing.rootRun.parentRunId).toBeUndefined(); - expect(continuedTracing.rootRun.name).toBe('instance-ai.orchestrator_resume'); + expect(continuedTracing.rootRun.name).toBe('resume: background task completed'); + expect(continuedTracing.rootRun.metadata).toEqual( + expect.objectContaining({ + 'instance_ai.canonical_name': 'instance-ai.orchestrator_resume', + }), + ); expect(continuedTracing.rootRun.metadata).toEqual( expect.objectContaining({ trace_kind: 'orchestrator_resume', @@ -895,7 +932,12 @@ describe('createInstanceAiTraceContext', () => { ); expect(continuedTracing.orchestratorRun.id).not.toBe(tracing?.orchestratorRun.id); expect(continuedTracing.orchestratorRun.parentRunId).toBe(continuedTracing.rootRun.id); - expect(continuedTracing.orchestratorRun.name).toBe('instance-ai.agent.orchestrator'); + expect(continuedTracing.orchestratorRun.name).toBe('agent: orchestrator'); + expect(continuedTracing.orchestratorRun.metadata).toEqual( + expect.objectContaining({ + 'instance_ai.canonical_name': 'instance-ai.agent.orchestrator', + }), + ); }); it('creates an orchestrator resume root without a previous trace when tracing is enabled', async () => { @@ -911,7 +953,7 @@ describe('createInstanceAiTraceContext', () => { expect(tracing).toBeDefined(); expect(tracing?.traceKind).toBe('orchestrator_resume'); - expect(tracing?.rootRun.name).toBe('instance-ai.orchestrator_resume'); + expect(tracing?.rootRun.name).toBe('resume: planned checkpoint'); expect(tracing?.rootRun.parentRunId).toBeUndefined(); expect(tracing?.rootRun.metadata).toEqual( expect.objectContaining({ @@ -949,7 +991,7 @@ describe('createInstanceAiTraceContext', () => { expect(tracing).toBeDefined(); expect(tracing?.traceKind).toBe('internal_operation'); - expect(tracing?.rootRun.name).toBe('instance-ai.internal.thread_title'); + expect(tracing?.rootRun.name).toBe('internal: thread-title'); expect(tracing?.rootRun.parentRunId).toBeUndefined(); expect(tracing?.rootRun.metadata).toEqual( expect.objectContaining({ @@ -958,6 +1000,7 @@ describe('createInstanceAiTraceContext', () => { operation_name: 'thread_title', agent_role: 'thread_title', thread_id: 'thread-1', + 'instance_ai.canonical_name': 'instance-ai.internal.thread_title', }), ); @@ -1003,8 +1046,8 @@ describe('createInstanceAiTraceContext', () => { expect(tracing?.traceKind).toBe('background_subagent'); expect(tracing?.rootRun.id).not.toBe(tracing?.actorRun.id); expect(tracing?.rootRun.parentRunId).toBeUndefined(); - expect(tracing?.rootRun.name).toBe('instance-ai.background_subagent'); - expect(tracing?.actorRun.name).toBe('instance-ai.agent.workflow-builder'); + expect(tracing?.rootRun.name).toBe('background task: workflow-builder'); + expect(tracing?.actorRun.name).toBe('agent: workflow-builder'); expect(tracing?.actorRun.parentRunId).toBe(tracing?.rootRun.id); expect(tracing?.rootRun.metadata).toEqual( expect.objectContaining({ @@ -1021,6 +1064,12 @@ describe('createInstanceAiTraceContext', () => { spawned_by_agent_id: 'agent-001', spawned_by_agent_role: 'orchestrator', spawned_by_tool_call_id: 'toolu-1', + 'instance_ai.canonical_name': 'instance-ai.background_subagent', + }), + ); + expect(tracing?.actorRun.metadata).toEqual( + expect.objectContaining({ + 'instance_ai.canonical_name': 'instance-ai.agent.workflow-builder', }), ); @@ -1071,6 +1120,12 @@ describe('createInstanceAiTraceContext', () => { description: 'Submit a workflow to n8n.', }, } as never, + runtimeTools: { + workspace_read_file: { + name: 'workspace_read_file', + description: 'Read a file from the workspace.', + }, + } as never, modelId: 'anthropic/claude-sonnet-4-6', }), ); @@ -1086,7 +1141,19 @@ describe('createInstanceAiTraceContext', () => { expect(actorInputs.model).toBe('anthropic/claude-sonnet-4-6'); expect(actorInputs.loaded_tool_count).toBe(2); expect(actorInputs.loaded_tool_names).toEqual(['build-workflow', 'submit-workflow']); + expect(actorInputs.assigned_tool_count).toBe(2); + expect(actorInputs.assigned_tool_names).toEqual(['build-workflow', 'submit-workflow']); + expect(actorInputs.runtime_tool_count).toBe(1); + expect(actorInputs.runtime_tool_names).toEqual(['workspace_read_file']); expect(actorInputs.loaded_tool_schema_hash).toEqual(expect.any(String)); + const actorSpan = agentsMock + .getSpans() + .find((span) => span.id === tracing?.actorRun.otelSpanId); + const spanInputs = jsonParse>( + actorSpan?.attributes['gen_ai.prompt'] as string, + ); + expect(spanInputs.assigned_tool_names).toEqual(['build-workflow', 'submit-workflow']); + expect(spanInputs.runtime_tool_names).toEqual(['workspace_read_file']); expect(loadedTools).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -1262,10 +1329,10 @@ describe('createInstanceAiTraceContext', () => { const spans = agentsMock.getSpans(); const spanNames = spans.map((span) => span.name); - expect(spanNames).toContain('instance-ai.hitl.suspend'); - expect( - spans.find((span) => span.name === 'instance-ai.hitl.suspend')?.attributes.tool_call_id, - ).toBe('toolu-ask'); + expect(spanNames).toContain('hitl: suspend'); + expect(spans.find((span) => span.name === 'hitl: suspend')?.attributes.tool_call_id).toBe( + 'toolu-ask', + ); expect(spanNames.some((name) => name.startsWith('instance-ai.tool.'))).toBe(false); }); @@ -1312,7 +1379,8 @@ describe('createInstanceAiTraceContext', () => { expect(tracing).toBeDefined(); const subAgentRun = await tracing!.startChildRun(tracing!.orchestratorRun, { - name: 'instance-ai.subagent.workflow-builder.stream', + name: 'agent: workflow-builder', + canonicalName: 'instance-ai.subagent.workflow-builder.stream', tags: ['sub-agent'], metadata: { agent_role: 'workflow-builder' }, inputs: { task: 'Build a workflow' }, @@ -1399,8 +1467,8 @@ describe('createInstanceAiTraceContext', () => { }); const spanNames = agentsMock.getSpans().map((span) => span.name); - expect(spanNames).toContain('instance-ai.hitl.resume'); - expect(spanNames).not.toContain('instance-ai.hitl.suspend'); + expect(spanNames).toContain('hitl: resume'); + expect(spanNames).not.toContain('hitl: suspend'); expect(spanNames.some((name) => name.startsWith('instance-ai.tool.'))).toBe(false); }); @@ -1473,7 +1541,7 @@ describe('createInstanceAiTraceContext', () => { expect(tracing).toBeDefined(); - const rootSpan = agentsMock.getSpans().find((span) => span.name === 'instance-ai.message_turn'); + const rootSpan = agentsMock.getSpans().find((span) => span.name === 'turn'); expect(rootSpan).toBeDefined(); expect(langsmithMock.getCreatedLegacyLegacyRunTrees()).toHaveLength(0); }); @@ -1490,7 +1558,7 @@ describe('createInstanceAiTraceContext', () => { input: { message: 'no proxy test' }, }); - const rootSpan = agentsMock.getSpans().find((span) => span.name === 'instance-ai.message_turn'); + const rootSpan = agentsMock.getSpans().find((span) => span.name === 'turn'); expect(rootSpan).toBeDefined(); expect(langsmithMock.getCreatedLegacyLegacyRunTrees()).toHaveLength(0); }); @@ -1577,8 +1645,8 @@ describe('createInstanceAiTraceContext', () => { await tracing!.finishRun(tracing!.rootRun, { outputs: { status: 'done' } }); const spans = agentsMock.getSpans(); - const rootSpan = spans.find((span) => span.name === 'instance-ai.message_turn'); - const orchestratorSpan = spans.find((span) => span.name === 'instance-ai.agent.orchestrator'); + const rootSpan = spans.find((span) => span.name === 'turn'); + const orchestratorSpan = spans.find((span) => span.name === 'agent: orchestrator'); const providerSpan = spans.find((span) => span.name === 'ai.streamText.doStream'); const localToolSpan = spans.find((span) => span.name === 'ai.toolCall'); diff --git a/packages/@n8n/instance-ai/src/tracing/langsmith-tracing.ts b/packages/@n8n/instance-ai/src/tracing/langsmith-tracing.ts index b5786d61422..2e6c14a109b 100644 --- a/packages/@n8n/instance-ai/src/tracing/langsmith-tracing.ts +++ b/packages/@n8n/instance-ai/src/tracing/langsmith-tracing.ts @@ -84,6 +84,12 @@ const LANGSMITH_SPAN_KIND = 'langsmith.span.kind'; const LANGSMITH_SPAN_TAGS = 'langsmith.span.tags'; const GEN_AI_PROMPT = 'gen_ai.prompt'; const GEN_AI_COMPLETION = 'gen_ai.completion'; +const LLM_AI_SDK_OPERATION_IDS = new Set([ + 'ai.generateText.doGenerate', + 'ai.streamText.doStream', + 'ai.generateObject.doGenerate', + 'ai.streamObject.doStream', +]); interface ProductOtelTraceRuntime { telemetry: BuiltTelemetry; @@ -123,8 +129,101 @@ function stableDottedOrder(parentRun: InstanceAiTraceRun | undefined, runId: str return parentRun?.dottedOrder ? `${parentRun.dottedOrder}.${runId}` : runId; } +function formatTraceLabel(value: string): string { + return value + .trim() + .replace(/[._\s]+/g, '-') + .replace(/-+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +function formatAgentRoleLabel(role: string): string { + return formatTraceLabel(role.replace(/^instance-ai[._-]?/, '')); +} + +function formatResumeReasonLabel(reason: unknown): string { + if (typeof reason !== 'string' || reason.trim().length === 0) { + return 'checkpoint'; + } + + return reason + .trim() + .replace(/[._-]+/g, ' ') + .replace(/\s+/g, ' '); +} + +function formatInternalOperationLabel(operationName: string): string { + return formatAgentRoleLabel(operationName); +} + +function inferDisplayKind(name: string): string { + if (name === 'turn') return 'turn'; + if (name.startsWith('agent:')) return 'agent'; + if (name.startsWith('llm:')) return 'llm'; + if (name.startsWith('tool:')) return 'tool'; + if (name.startsWith('prepare:')) return 'prepare'; + if (name.startsWith('resume:')) return 'resume'; + if (name.startsWith('background task:')) return 'background_task'; + if (name.startsWith('hitl:')) return 'hitl'; + if (name.startsWith('internal:')) return 'internal'; + return 'operation'; +} + +function inferDisplayGroup( + metadata: Record | undefined, + name: string, +): string | undefined { + const role = + typeof metadata?.agent_role === 'string' + ? metadata.agent_role + : typeof metadata?.subagent_role === 'string' + ? metadata.subagent_role + : undefined; + if (role) { + return formatAgentRoleLabel(role); + } + + if (name.startsWith('prepare:')) return 'preparation'; + if (name.startsWith('hitl:')) return 'human-in-the-loop'; + if (name === 'turn') return 'conversation'; + return undefined; +} + +function inferDisplayPhase(metadata: Record | undefined): string | undefined { + return typeof metadata?.execution_mode === 'string' + ? formatTraceLabel(metadata.execution_mode) + : undefined; +} + +function buildProductSpanMetadata(options: { + name: string; + canonicalName?: string; + metadata?: Record; +}): Record { + const canonicalName = options.canonicalName ?? options.name; + const displayGroup = inferDisplayGroup(options.metadata, options.name); + const displayPhase = inferDisplayPhase(options.metadata); + const displayDefaults = { + trace_version: OTEL_TRACE_VERSION, + 'instance_ai.trace_version': OTEL_TRACE_VERSION, + display_kind: inferDisplayKind(options.name), + ...(displayGroup ? { display_group: displayGroup } : {}), + ...(displayPhase ? { display_phase: displayPhase } : {}), + }; + + return ( + mergeMetadata(displayDefaults, options.metadata, { + display_name: options.name, + 'instance_ai.display_name': options.name, + 'instance_ai.canonical_name': canonicalName, + 'instance_ai.run_name': canonicalName, + }) ?? {} + ); +} + function buildProductSpanAttributes(options: { name: string; + canonicalName?: string; runType?: string; tags?: string[]; metadata?: Record; @@ -142,10 +241,7 @@ function buildProductSpanAttributes(options: { attributes[LANGSMITH_SPAN_TAGS] = tags; } - const metadata = mergeMetadata(options.metadata, { - trace_version: OTEL_TRACE_VERSION, - 'instance_ai.trace_version': OTEL_TRACE_VERSION, - }); + const metadata = buildProductSpanMetadata(options); for (const [key, value] of Object.entries(metadata ?? {})) { const attributeValue = toTelemetryAttributeValue(value); if (attributeValue === undefined) continue; @@ -176,6 +272,7 @@ function startProductSpan( options: { projectName: string; name: string; + canonicalName?: string; runType?: string; tags?: string[]; metadata?: Record; @@ -189,6 +286,7 @@ function startProductSpan( throw new Error('Instance AI tracing requires an OpenTelemetry tracer'); } + const spanMetadata = buildProductSpanMetadata(options); const parentContext = options.root ? ROOT_CONTEXT : (options.parentContext ?? @@ -205,8 +303,8 @@ function startProductSpan( const traceId = langsmithTraceIdFromOtelTraceId(spanContext.traceId); const runId = langsmithRunIdFromOtelSpanId(spanContext.spanId); const spanContextWithSpan = otelTrace.setSpan(parentContext ?? otelContext.active(), span); - const parentRun = options.parentRun; + const runMetadata = mergeMetadata(parentRun?.metadata, spanMetadata); const run: InstanceAiTraceRun = { id: runId, name: options.name, @@ -221,7 +319,7 @@ function startProductSpan( childExecutionOrder: 0, ...(parentRun ? { parentRunId: parentRun.id } : {}), ...(options.tags ? { tags: normalizeTags(DEFAULT_TAGS, parentRun?.tags, options.tags) } : {}), - ...(options.metadata ? { metadata: mergeMetadata(parentRun?.metadata, options.metadata) } : {}), + ...(runMetadata ? { metadata: runMetadata } : {}), ...(options.inputs !== undefined ? { inputs: sanitizeTracePayload(options.inputs) } : {}), }; @@ -423,6 +521,7 @@ interface CreateInternalOperationTraceContextOptions interface CurrentTraceSpanOptions { name: string; + canonicalName?: string; runType?: string; tags?: string[]; metadata?: Record; @@ -434,6 +533,7 @@ interface AgentTraceInputOptions { systemPrompt?: string; tools?: InstanceAiToolRegistry; deferredTools?: InstanceAiToolRegistry; + runtimeTools?: InstanceAiToolRegistry; modelId?: unknown; memory?: unknown; toolSearchEnabled?: boolean; @@ -1023,6 +1123,94 @@ function normalizeAnthropicUsageForLangSmith(attributes: Record }); } +function readStringAttribute( + attributes: Record, + keys: string[], +): string | undefined { + for (const key of keys) { + const value = attributes[key]; + if (typeof value === 'string' && value.length > 0) { + return value; + } + } + return undefined; +} + +function inferNativeLlmRole(attributes: Record): string | undefined { + return readStringAttribute(attributes, [ + 'ai.telemetry.metadata.agent_role', + 'langsmith.metadata.agent_role', + 'agent_role', + ]); +} + +function displayNameForNativeLlmSpan(attributes: Record): string { + const role = inferNativeLlmRole(attributes); + if (role === 'thread_title') { + return 'llm: title'; + } + + if (role) { + return `llm: ${formatAgentRoleLabel(role)}`; + } + + const functionId = readStringAttribute(attributes, [ + 'ai.telemetry.functionId', + 'resource.name', + 'operation.name', + ]); + if (functionId) { + return `llm: ${formatAgentRoleLabel(functionId.replace(/^instance-ai[._-]?/, ''))}`; + } + + return 'llm'; +} + +function setLangSmithMetadataAttribute( + attributes: Record, + key: string, + value: unknown, +): void { + attributes[key] = value; + if (!key.startsWith('langsmith.metadata.')) { + attributes[`langsmith.metadata.${key}`] = value; + } +} + +function renameNativeLlmSpanForLangSmith( + span: Record, + attributes: Record, +): void { + const operationId = readStringAttribute(attributes, ['ai.operationId']); + if (!operationId || !LLM_AI_SDK_OPERATION_IDS.has(operationId)) { + return; + } + + const displayName = displayNameForNativeLlmSpan(attributes); + span.name = displayName; + attributes[LANGSMITH_TRACE_NAME] = displayName; + const displayGroup = inferNativeLlmRole(attributes); + setLangSmithMetadataAttribute(attributes, 'display_name', displayName); + setLangSmithMetadataAttribute(attributes, 'display_kind', 'llm'); + setLangSmithMetadataAttribute( + attributes, + 'display_group', + displayGroup ? formatAgentRoleLabel(displayGroup) : 'llm', + ); + const executionMode = readStringAttribute(attributes, [ + 'ai.telemetry.metadata.execution_mode', + 'langsmith.metadata.execution_mode', + 'execution_mode', + ]); + if (executionMode) { + setLangSmithMetadataAttribute(attributes, 'display_phase', formatTraceLabel(executionMode)); + } + setLangSmithMetadataAttribute(attributes, 'ai_sdk.operation', operationId); + setLangSmithMetadataAttribute(attributes, 'instance_ai.display_name', displayName); + setLangSmithMetadataAttribute(attributes, 'instance_ai.canonical_name', operationId); + setLangSmithMetadataAttribute(attributes, 'instance_ai.run_name', operationId); +} + export function redactLangSmithTelemetrySpan(span: unknown): unknown { if (!isRecord(span) || !isRecord(span.attributes)) { return span; @@ -1034,6 +1222,7 @@ export function redactLangSmithTelemetrySpan(span: unknown): unknown { } enrichLangSmithPromptAttribute(attributes); normalizeAnthropicUsageForLangSmith(attributes); + renameNativeLlmSpanForLangSmith(span, attributes); span.attributes = attributes; return span; } @@ -1221,7 +1410,7 @@ function summarizeToolForManifest(name: string, tool: unknown): Record { if (!tools || Object.keys(tools).length === 0) { @@ -1241,8 +1430,18 @@ function summarizeToolSet( const toolNames = summaries .map((tool) => (typeof tool.name === 'string' ? tool.name : undefined)) .filter((name): name is string => name !== undefined); + const aliases: Record = {}; + if (fieldPrefix === 'loaded') { + aliases.assigned_tool_count = summaries.length; + aliases.assigned_tool_names = toolNames; + } + if (fieldPrefix === 'runtime') { + aliases.runtime_tool_count = summaries.length; + aliases.runtime_tool_names = toolNames; + } return { + ...aliases, [`${fieldPrefix}_tool_count`]: summaries.length, [`${fieldPrefix}_tool_names`]: toolNames, [`${fieldPrefix}_tool_manifest`]: serializeTraceText(JSON.stringify(summaries)), @@ -1479,13 +1678,13 @@ export function mergeTraceRunInputs( return; } - const mergedInputs = sanitizeTracePayload(mergeTraceInputs(run.inputs, inputs)); - run.inputs = mergedInputs; - - const currentProductTrace = getCurrentProductTrace(); - if (currentProductTrace) { - updateProductRunInputs(currentProductTrace.runtime, run, inputs); + const runtime = getCurrentProductTrace()?.runtime ?? otelTraceRuntimes.get(run.traceId); + if (runtime) { + updateProductRunInputs(runtime, run, inputs); + return; } + + run.inputs = sanitizeTracePayload(mergeTraceInputs(run.inputs, inputs)); } export function buildAgentTraceInputs(options: AgentTraceInputOptions): Record { @@ -1499,6 +1698,7 @@ export function buildAgentTraceInputs(options: AgentTraceInputOptions): Record( const spanRun = startProductSpan(currentProductTrace.runtime, { projectName: currentProductTrace.currentRun.projectName, name: options.name, + canonicalName: options.canonicalName, runType: options.runType ?? 'chain', tags: options.tags, metadata: options.metadata, @@ -1575,6 +1776,7 @@ async function startAndFinishProductChildSpan( currentTrace: { runtime: ProductOtelTraceRuntime; currentRun: InstanceAiTraceRun }, options: { name: string; + canonicalName?: string; runType?: string; tags?: string[]; metadata?: Record; @@ -1587,6 +1789,7 @@ async function startAndFinishProductChildSpan( const childRun = startProductSpan(currentTrace.runtime, { projectName: currentTrace.currentRun.projectName, name: options.name, + canonicalName: options.canonicalName, runType: options.runType ?? 'chain', tags: options.tags, metadata: options.metadata, @@ -1620,7 +1823,8 @@ async function traceProductSuspendableToolExecute( ...context, suspend: async (suspendPayload: unknown) => { await startAndFinishProductChildSpan(currentTrace, { - name: 'instance-ai.hitl.suspend', + name: 'hitl: suspend', + canonicalName: 'instance-ai.hitl.suspend', runType: 'chain', tags: ['hitl'], metadata: mergeMetadata(buildSuspendMetadata(tool.name, suspendPayload), { @@ -1636,7 +1840,8 @@ async function traceProductSuspendableToolExecute( if (isResume) { await startAndFinishProductChildSpan(currentTrace, { - name: 'instance-ai.hitl.resume', + name: 'hitl: resume', + canonicalName: 'instance-ai.hitl.resume', runType: 'chain', tags: ['hitl', 'resume'], metadata: mergeMetadata(buildSuspendMetadata(tool.name, resumeData), { @@ -1693,6 +1898,7 @@ function createTraceContext( return startProductSpan(otelRuntime, { projectName, name: init.name, + canonicalName: init.canonicalName, runType: init.runType, tags: init.tags, metadata: mergeMetadata(parentRun.metadata, init.metadata), @@ -2101,7 +2307,8 @@ export async function createInstanceAiTraceContext( const traceContextRef: { current?: InstanceAiTraceContext } = {}; const messageRun = startProductSpan(otelRuntime, { projectName, - name: 'instance-ai.message_turn', + name: 'turn', + canonicalName: 'instance-ai.message_turn', runType: 'chain', tags: ['message-turn'], metadata: mergeMetadata(baseMetadata, { @@ -2184,7 +2391,8 @@ export async function continueInstanceAiTraceContext( const otelRuntime = await createProductOtelRuntime(projectName, proxyConfig); const rootRun = startProductSpan(otelRuntime, { projectName, - name: 'instance-ai.orchestrator_resume', + name: `resume: ${formatResumeReasonLabel(options.metadata?.resume_reason)}`, + canonicalName: 'instance-ai.orchestrator_resume', runType: 'chain', tags: ['orchestrator-resume'], metadata: mergeMetadata(baseMetadata, { @@ -2198,7 +2406,8 @@ export async function continueInstanceAiTraceContext( }); const orchestratorRun = startProductSpan(otelRuntime, { projectName, - name: 'instance-ai.agent.orchestrator', + name: 'agent: orchestrator', + canonicalName: 'instance-ai.agent.orchestrator', runType: 'chain', tags: ['orchestrator', 'resume'], metadata: mergeMetadata(baseMetadata, { @@ -2253,7 +2462,8 @@ export async function createDetachedSubAgentTraceContext( const otelRuntime = await createProductOtelRuntime(projectName, options.proxyConfig); const rootRun = startProductSpan(otelRuntime, { projectName, - name: 'instance-ai.background_subagent', + name: `background task: ${formatAgentRoleLabel(options.role)}`, + canonicalName: 'instance-ai.background_subagent', runType: 'chain', tags: normalizeTags( ['sub-agent', 'background'], @@ -2285,7 +2495,8 @@ export async function createDetachedSubAgentTraceContext( }); const actorRun = startProductSpan(otelRuntime, { projectName, - name: `instance-ai.agent.${options.role}`, + name: `agent: ${formatAgentRoleLabel(options.role)}`, + canonicalName: `instance-ai.agent.${options.role}`, runType: 'chain', tags: normalizeTags( ['sub-agent', 'background'], @@ -2375,7 +2586,8 @@ export async function createInternalOperationTraceContext( const otelRuntime = await createProductOtelRuntime(projectName, options.proxyConfig); const rootRun = startProductSpan(otelRuntime, { projectName, - name: `instance-ai.internal.${options.operationName}`, + name: `internal: ${formatInternalOperationLabel(options.operationName)}`, + canonicalName: `instance-ai.internal.${options.operationName}`, runType: 'chain', tags: ['internal-operation'], metadata: mergeMetadata(baseMetadata, { diff --git a/packages/@n8n/instance-ai/src/types.ts b/packages/@n8n/instance-ai/src/types.ts index 9059ab32345..26a4be55b63 100644 --- a/packages/@n8n/instance-ai/src/types.ts +++ b/packages/@n8n/instance-ai/src/types.ts @@ -819,6 +819,7 @@ export interface InstanceAiTraceRun { export interface InstanceAiTraceRunInit { name: string; + canonicalName?: string; runType?: string; tags?: string[]; metadata?: Record; diff --git a/packages/cli/src/modules/instance-ai/instance-ai.service.ts b/packages/cli/src/modules/instance-ai/instance-ai.service.ts index 4a3e6fd8b13..19ec923aca0 100644 --- a/packages/cli/src/modules/instance-ai/instance-ai.service.ts +++ b/packages/cli/src/modules/instance-ai/instance-ai.service.ts @@ -2270,7 +2270,8 @@ export class InstanceAiService { }); const contextCompactionRun = tracing ? await tracing.startChildRun(tracing.messageRun, { - name: 'instance-ai.context_compaction', + name: 'prepare: context', + canonicalName: 'instance-ai.context_compaction', tags: ['context'], metadata: { agent_role: 'context_compaction' }, inputs: { @@ -2320,7 +2321,8 @@ export class InstanceAiService { const promptBuildRun = tracing ? await tracing.startChildRun(tracing.messageRun, { - name: 'instance-ai.prompt_build', + name: 'prepare: prompt', + canonicalName: 'instance-ai.prompt_build', tags: ['prompt'], metadata: { agent_role: 'prompt_build' }, inputs: { @@ -2389,7 +2391,8 @@ export class InstanceAiService { if (tracing && tracing.actorRun.id === tracing.rootRun.id) { const actorRun = await tracing.startChildRun(tracing.rootRun, { - name: 'instance-ai.agent.orchestrator', + name: 'agent: orchestrator', + canonicalName: 'instance-ai.agent.orchestrator', tags: ['orchestrator'], metadata: { agent_role: 'orchestrator',