From 3278696f4c2d26e4d8098d26e94851815c7bd9ed Mon Sep 17 00:00:00 2001 From: hailin Date: Thu, 26 Feb 2026 20:42:15 -0800 Subject: [PATCH] feat: inject tenant skills into agent system prompt Load active skills from the tenant's schema `skills` table and append them to the system prompt before passing to the Claude Agent SDK. This closes the gap where skills existed in the DB but were never surfaced to the agent during task execution. Co-Authored-By: Claude Opus 4.6 --- .../claude-agent-sdk-engine.ts | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/services/agent-service/src/infrastructure/engines/claude-agent-sdk/claude-agent-sdk-engine.ts b/packages/services/agent-service/src/infrastructure/engines/claude-agent-sdk/claude-agent-sdk-engine.ts index 68e271b..65e9314 100644 --- a/packages/services/agent-service/src/infrastructure/engines/claude-agent-sdk/claude-agent-sdk-engine.ts +++ b/packages/services/agent-service/src/infrastructure/engines/claude-agent-sdk/claude-agent-sdk-engine.ts @@ -17,6 +17,7 @@ */ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { DataSource } from 'typeorm'; import { AgentEnginePort, EngineTaskParams, EngineStreamEvent } from '../../../domain/ports/outbound/agent-engine.port'; import { AgentEngineType } from '../../../domain/value-objects/agent-engine-type.vo'; import { CommandRiskLevel } from '../../../domain/value-objects/command-risk-level.vo'; @@ -52,6 +53,7 @@ export class ClaudeAgentSdkEngine implements AgentEnginePort { private readonly configService: ConfigService, private readonly tenantConfigService: TenantAgentConfigService, private readonly allowedToolsResolver: AllowedToolsResolverService, + private readonly dataSource: DataSource, ) {} async *executeTask(params: EngineTaskParams): AsyncGenerator { @@ -105,9 +107,13 @@ export class ClaudeAgentSdkEngine implements AgentEnginePort { // Set tenant-isolated HOME directory so SDK session files are separated per tenant this.ensureTenantHome(tenantId, env); + // Load tenant skills and build enriched system prompt + const skills = await this.loadTenantSkills(tenantId); + const enrichedSystemPrompt = this.buildSystemPromptWithSkills(params.systemPrompt, skills); + // Build SDK query options const sdkOptions: Record = { - systemPrompt: params.systemPrompt || undefined, + systemPrompt: enrichedSystemPrompt, allowedTools: params.allowedTools?.length ? params.allowedTools : undefined, maxTurns: params.maxTurns, maxBudgetUsd: params.maxBudgetUsd, @@ -361,6 +367,43 @@ export class ClaudeAgentSdkEngine implements AgentEnginePort { return this.activeSessions.get(sessionId)?.sdkSessionId; } + /** + * Load active skills from the tenant's schema `skills` table. + * Skills are injected into the system prompt so the agent knows what it can do. + */ + private async loadTenantSkills(tenantId: string): Promise> { + const safeId = tenantId.replace(/[^a-zA-Z0-9_]/g, ''); + const schemaName = `it0_t_${safeId}`; + try { + const skills: Array<{ name: string; content: string }> = await this.dataSource.query( + `SELECT name, content FROM "${schemaName}".skills WHERE is_active = true ORDER BY name`, + ); + this.logger.log(`Loaded ${skills.length} skills for tenant ${tenantId}`); + return skills; + } catch (err: any) { + this.logger.warn(`Failed to load skills for tenant ${tenantId}: ${err.message}`); + return []; + } + } + + /** + * Build enriched system prompt by appending tenant skills. + */ + private buildSystemPromptWithSkills( + basePrompt: string | undefined, + skills: Array<{ name: string; content: string }>, + ): string | undefined { + if (skills.length === 0) return basePrompt || undefined; + + const skillsSection = skills + .map((s) => `### ${s.name}\n${s.content}`) + .join('\n\n'); + + const enrichment = `\n\n## Available Skills\n\nThe following skills are configured for this tenant. Use them as reference knowledge when executing tasks.\n\n${skillsSection}`; + + return basePrompt ? `${basePrompt}${enrichment}` : enrichment.trim(); + } + /** * Set up a tenant-isolated HOME directory so SDK session files * are stored separately per tenant. Creates the directory structure