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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-26 20:42:15 -08:00
parent 3ed20cdf08
commit 3278696f4c
1 changed files with 44 additions and 1 deletions

View File

@ -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<EngineStreamEvent> {
@ -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<string, any> = {
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<Array<{ name: string; content: string }>> {
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