@taodigital/agents 0.0.0-stage → 1.2.1

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 (76) hide show
  1. package/README.md +99 -2
  2. package/dist/api-test-agent.d.ts +126 -0
  3. package/dist/api-test-agent.js +267 -0
  4. package/dist/artifact-intel-agent.d.ts +11 -0
  5. package/dist/artifact-intel-agent.js +152 -0
  6. package/dist/coverage-gap-agent.d.ts +25 -0
  7. package/dist/coverage-gap-agent.js +269 -0
  8. package/dist/coverage-map-agent.d.ts +15 -0
  9. package/dist/coverage-map-agent.js +255 -0
  10. package/dist/crawl-diff-agent.d.ts +27 -0
  11. package/dist/crawl-diff-agent.js +412 -0
  12. package/dist/data-agent.d.ts +101 -0
  13. package/dist/data-agent.js +2284 -0
  14. package/dist/document-agent.d.ts +10 -0
  15. package/dist/document-agent.js +32 -0
  16. package/dist/enhancement-agent.d.ts +48 -0
  17. package/dist/enhancement-agent.js +342 -0
  18. package/dist/field-detector-agent.d.ts +35 -0
  19. package/dist/field-detector-agent.js +220 -0
  20. package/dist/flow-agent.d.ts +130 -0
  21. package/dist/flow-agent.js +169 -0
  22. package/dist/github-agent.d.ts +18 -0
  23. package/dist/github-agent.js +39 -0
  24. package/dist/github-write-agent.d.ts +67 -0
  25. package/dist/github-write-agent.js +249 -0
  26. package/dist/grouping-agent.d.ts +18 -0
  27. package/dist/grouping-agent.js +338 -0
  28. package/dist/healing-agent.d.ts +73 -0
  29. package/dist/healing-agent.js +137 -0
  30. package/dist/index.d.ts +36 -0
  31. package/dist/index.js +67 -0
  32. package/dist/integration-test-agent.d.ts +118 -0
  33. package/dist/integration-test-agent.js +165 -0
  34. package/dist/label-assignment-agent.d.ts +15 -0
  35. package/dist/label-assignment-agent.js +97 -0
  36. package/dist/llm-config.d.ts +38 -0
  37. package/dist/llm-config.js +287 -0
  38. package/dist/llm-mock.d.ts +77 -0
  39. package/dist/llm-mock.js +192 -0
  40. package/dist/llm.d.ts +58 -0
  41. package/dist/llm.js +438 -0
  42. package/dist/nlp-agent.d.ts +208 -0
  43. package/dist/nlp-agent.js +340 -0
  44. package/dist/orchestrator.d.ts +285 -0
  45. package/dist/orchestrator.js +141 -0
  46. package/dist/plan-agent.d.ts +4 -0
  47. package/dist/plan-agent.js +200 -0
  48. package/dist/repo-scan-agent.d.ts +189 -0
  49. package/dist/repo-scan-agent.js +2032 -0
  50. package/dist/requirements-agent.d.ts +59 -0
  51. package/dist/requirements-agent.js +641 -0
  52. package/dist/scenario-agent.d.ts +20 -0
  53. package/dist/scenario-agent.js +123 -0
  54. package/dist/script-agent.d.ts +447 -0
  55. package/dist/script-agent.js +11813 -0
  56. package/dist/security-test-agent.d.ts +111 -0
  57. package/dist/security-test-agent.js +141 -0
  58. package/dist/skill-file-agent.d.ts +264 -0
  59. package/dist/skill-file-agent.js +3470 -0
  60. package/dist/step-coverage.d.ts +46 -0
  61. package/dist/step-coverage.js +288 -0
  62. package/dist/step-utils.d.ts +2 -0
  63. package/dist/step-utils.js +62 -0
  64. package/dist/test-case-agent.d.ts +206 -0
  65. package/dist/test-case-agent.js +2141 -0
  66. package/dist/test-import-agent.d.ts +148 -0
  67. package/dist/test-import-agent.js +3235 -0
  68. package/dist/test-script-agent.d.ts +16 -0
  69. package/dist/test-script-agent.js +48 -0
  70. package/dist/token-pricing.d.ts +14 -0
  71. package/dist/token-pricing.js +62 -0
  72. package/dist/types.d.ts +232 -0
  73. package/dist/types.js +7 -0
  74. package/dist/visual-agent.d.ts +114 -0
  75. package/dist/visual-agent.js +261 -0
  76. package/package.json +26 -4
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.labelTestCasesBatch = labelTestCasesBatch;
4
+ const llm_js_1 = require("./llm.js");
5
+ const llm_config_js_1 = require("./llm-config.js");
6
+ const core_1 = require("@taodigital/core");
7
+ const BATCH_SIZE = 50;
8
+ const SYSTEM_PROMPT = `You are a QA test case classification assistant. Given a batch of test cases, assign each one:
9
+ - priority: one of CRITICAL, HIGH, MEDIUM, LOW
10
+ - inferredKind: one of POSITIVE, NEGATIVE, EDGE, SECURITY, ACCESSIBILITY
11
+ - tags: relevant string tags (e.g. ["@auth","@login"])
12
+ - confidence: a float 0–1 representing classification confidence
13
+
14
+ Priority heuristic rules:
15
+ - CRITICAL: test involves login, auth, payment, checkout, password, billing, security, or accessibility
16
+ - HIGH: test involves register, signup, onboard, data loss, or is a negative test with more than 5 steps
17
+ - MEDIUM: test involves filter, sort, search, export, or navigation
18
+ - LOW: everything else
19
+
20
+ Kind inference rules:
21
+ - NEGATIVE: title or steps contain keywords: invalid, wrong, incorrect, fail, error, reject
22
+ - SECURITY: title or steps contain keywords: security, xss, injection, csrf, permission, unauthorized
23
+ - ACCESSIBILITY: title or steps contain keywords: accessibility, a11y, screen reader, wcag, aria
24
+ - EDGE: title or steps contain keywords: boundary, edge, limit, maximum, minimum, empty, null
25
+ - POSITIVE: default when none of the above apply
26
+
27
+ Return ONLY a valid JSON array with no markdown, no explanation, no code fences. Each element must have exactly these keys: id, priority, inferredKind, tags, confidence.
28
+
29
+ Example output format:
30
+ [{"id":"tc-1","priority":"HIGH","inferredKind":"POSITIVE","tags":["@auth","@login"],"confidence":0.85}]`;
31
+ function buildUserPrompt(batch) {
32
+ const items = batch.map((c) => ({
33
+ id: c.id,
34
+ title: c.title,
35
+ stepsText: c.stepsText,
36
+ expectedResult: c.expectedResult ?? '',
37
+ }));
38
+ return `Classify the following test cases:\n${JSON.stringify(items, null, 2)}`;
39
+ }
40
+ function fallbackResults(batch) {
41
+ return batch.map((c) => ({
42
+ id: c.id,
43
+ priority: 'HIGH',
44
+ inferredKind: 'POSITIVE',
45
+ tags: [],
46
+ confidence: 0.5,
47
+ needsReview: true,
48
+ }));
49
+ }
50
+ function parseRaw(raw, batch) {
51
+ let cleaned = raw.trim();
52
+ // Strip markdown code fences if present
53
+ cleaned = cleaned.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim();
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(cleaned);
57
+ }
58
+ catch {
59
+ return fallbackResults(batch);
60
+ }
61
+ if (!Array.isArray(parsed))
62
+ return fallbackResults(batch);
63
+ const idSet = new Set(batch.map((c) => c.id));
64
+ const resultMap = new Map();
65
+ for (const item of parsed) {
66
+ if (!item?.id || !idSet.has(item.id))
67
+ continue;
68
+ const confidence = typeof item.confidence === 'number' ? Math.min(1, Math.max(0, item.confidence)) : 0.5;
69
+ resultMap.set(item.id, {
70
+ id: item.id,
71
+ priority: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].includes(item.priority) ? item.priority : 'HIGH',
72
+ inferredKind: typeof item.inferredKind === 'string' && item.inferredKind ? item.inferredKind : 'POSITIVE',
73
+ tags: Array.isArray(item.tags) ? item.tags.filter((t) => typeof t === 'string') : [],
74
+ confidence,
75
+ needsReview: confidence < 0.70,
76
+ });
77
+ }
78
+ // Fill in any cases the LLM omitted with fallback
79
+ return batch.map((c) => resultMap.get(c.id) ?? { ...fallbackResults([c])[0] });
80
+ }
81
+ async function labelTestCasesBatch(cases, tenantId) {
82
+ const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(tenantId, 'testGeneration');
83
+ const results = [];
84
+ for (let i = 0; i < cases.length; i += BATCH_SIZE) {
85
+ const batch = cases.slice(i, i + BATCH_SIZE);
86
+ try {
87
+ const seed = (0, llm_js_1.deterministicSeed)(`${tenantId}:label-batch:${batch.map((c) => c.id).join('|')}`);
88
+ const effectiveSystem = await (0, core_1.getAgentPrompt)('label-assignment', undefined, tenantId) ?? SYSTEM_PROMPT;
89
+ const { text } = await (0, llm_js_1.askTracked)(effectiveSystem, buildUserPrompt(batch), llmCfg, 4096, undefined, { temperature: 0, seed });
90
+ results.push(...parseRaw(text, batch));
91
+ }
92
+ catch {
93
+ results.push(...fallbackResults(batch));
94
+ }
95
+ }
96
+ return results;
97
+ }
@@ -0,0 +1,38 @@
1
+ import type { LLMTaskType } from '@taodigital/app-brain';
2
+ import type { LLMConfig } from './llm.js';
3
+ export declare function getAllowedModels(extra?: string[]): string[];
4
+ export declare function getLLMBaseUrls(overrides?: Record<string, string>): {
5
+ gemini: string;
6
+ grok: string;
7
+ groq: string;
8
+ mistral: string;
9
+ deepseek: string;
10
+ perplexity: string;
11
+ togetherai: string;
12
+ openrouter: string;
13
+ nvidia: string;
14
+ ollama: string;
15
+ };
16
+ /** Load the tenant's configured LLM provider. Falls back to platform admin config, then env vars. */
17
+ export declare function getAgentLLMConfig(tenantId: string): Promise<LLMConfig | undefined>;
18
+ /**
19
+ * Returns the platform admin LLM config directly, bypassing tenant config.
20
+ * Use this when a tenant-configured key fails at call time (401/403) to retry with the platform key.
21
+ */
22
+ export declare function getPlatformFallbackConfig(): Promise<LLMConfig | undefined>;
23
+ /**
24
+ * Runs `fn(tenantCfg)` and, if the provider rejects the API key (401/403), retries with
25
+ * platform admin config. Prevents a single expired workspace key from blocking all agent jobs.
26
+ *
27
+ * When tenantCfg is undefined (no tenant or platform config resolved), fails fast with a
28
+ * clear "No AI provider configured" error rather than making a live call with empty credentials.
29
+ */
30
+ export declare function tryWithPlatformFallback<T>(tenantCfg: LLMConfig | undefined, fn: (cfg: LLMConfig | undefined) => Promise<T>): Promise<T>;
31
+ /** Load workspace domain config for a project — used to inject domain hints into prompts. */
32
+ export declare function getWorkspaceDomainContext(tenantId: string, projectId: string): Promise<string>;
33
+ /** Returns true if the workspace has PHI masking enabled — redact real PII field values before LLM calls. */
34
+ export declare function getWorkspacePhiMasking(tenantId: string, projectId: string): Promise<boolean>;
35
+ /** Returns the tenant's LLM config swapped to the provider's fast mini-model for lower-cost passes. */
36
+ export declare function getFastPassLLMConfig(tenantId: string): Promise<LLMConfig | undefined>;
37
+ /** Load the tenant's LLM config for a specific task, respecting per-task overrides. */
38
+ export declare function getTaskAgentLLMConfig(tenantId: string, task: LLMTaskType): Promise<LLMConfig | undefined>;
@@ -0,0 +1,287 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAllowedModels = getAllowedModels;
4
+ exports.getLLMBaseUrls = getLLMBaseUrls;
5
+ exports.getAgentLLMConfig = getAgentLLMConfig;
6
+ exports.getPlatformFallbackConfig = getPlatformFallbackConfig;
7
+ exports.tryWithPlatformFallback = tryWithPlatformFallback;
8
+ exports.getWorkspaceDomainContext = getWorkspaceDomainContext;
9
+ exports.getWorkspacePhiMasking = getWorkspacePhiMasking;
10
+ exports.getFastPassLLMConfig = getFastPassLLMConfig;
11
+ exports.getTaskAgentLLMConfig = getTaskAgentLLMConfig;
12
+ const app_brain_1 = require("@taodigital/app-brain");
13
+ const core_1 = require("@taodigital/core");
14
+ const ALLOWED_MODELS_BASE = [
15
+ 'claude-3-5-sonnet-20241022',
16
+ 'claude-3-5-haiku-20241022',
17
+ 'claude-3-opus-20240229',
18
+ 'claude-sonnet-4-6',
19
+ 'claude-haiku-4-5-20251001',
20
+ 'gpt-4o',
21
+ 'gpt-4o-mini',
22
+ 'gpt-4-turbo',
23
+ 'gemini-2.5-flash',
24
+ 'gemini-1.5-flash',
25
+ ];
26
+ const ALLOWED_MODELS_EXTRA_ENV = process.env.ALLOWED_MODELS_EXTRA
27
+ ? process.env.ALLOWED_MODELS_EXTRA.split(',').map((m) => m.trim()).filter(Boolean)
28
+ : [];
29
+ function getAllowedModels(extra) {
30
+ return [...ALLOWED_MODELS_BASE, ...ALLOWED_MODELS_EXTRA_ENV, ...(extra ?? [])];
31
+ }
32
+ const ALLOWED_MODELS = new Set([...ALLOWED_MODELS_BASE, ...ALLOWED_MODELS_EXTRA_ENV]);
33
+ function assertAllowedModel(model) {
34
+ if (!ALLOWED_MODELS.has(model)) {
35
+ throw new Error(`Model '${model}' is not allowed. Set ALLOWED_MODELS_EXTRA env var to add models.`);
36
+ }
37
+ }
38
+ const LLM_BASE_URLS_DEFAULT = {
39
+ gemini: process.env.GEMINI_BASE_URL ?? 'https://generativelanguage.googleapis.com/v1beta/openai/',
40
+ grok: process.env.GROK_BASE_URL ?? 'https://api.x.ai/v1',
41
+ groq: process.env.GROQ_BASE_URL ?? 'https://api.groq.com/openai/v1',
42
+ mistral: process.env.MISTRAL_BASE_URL ?? 'https://api.mistral.ai/v1',
43
+ deepseek: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com/v1',
44
+ perplexity: process.env.PERPLEXITY_BASE_URL ?? 'https://api.perplexity.ai',
45
+ togetherai: process.env.TOGETHERAI_BASE_URL ?? 'https://api.together.xyz/v1',
46
+ openrouter: process.env.OPENROUTER_BASE_URL ?? 'https://openrouter.ai/api/v1',
47
+ nvidia: process.env.NVIDIA_BASE_URL ?? 'https://integrate.api.nvidia.com/v1',
48
+ ollama: process.env.OLLAMA_BASE_URL ?? 'http://localhost:11434/v1',
49
+ };
50
+ function getLLMBaseUrls(overrides) {
51
+ return { ...LLM_BASE_URLS_DEFAULT, ...overrides };
52
+ }
53
+ const LLM_BASE_URLS = LLM_BASE_URLS_DEFAULT;
54
+ const PLATFORM_DEFAULT_MODELS = {
55
+ openai: process.env.DEFAULT_MODEL_OPENAI ?? 'gpt-4o',
56
+ anthropic: process.env.DEFAULT_MODEL_ANTHROPIC ?? 'claude-sonnet-4-6',
57
+ gemini: process.env.DEFAULT_MODEL_GEMINI ?? 'gemini-2.5-pro',
58
+ grok: process.env.DEFAULT_MODEL_GROK ?? 'grok-3',
59
+ groq: process.env.DEFAULT_MODEL_GROQ ?? 'llama-3.3-70b-versatile',
60
+ mistral: process.env.DEFAULT_MODEL_MISTRAL ?? 'mistral-large-latest',
61
+ deepseek: process.env.DEFAULT_MODEL_DEEPSEEK ?? 'deepseek-chat',
62
+ perplexity: process.env.DEFAULT_MODEL_PERPLEXITY ?? 'llama-3.1-sonar-large-128k-online',
63
+ togetherai: process.env.DEFAULT_MODEL_TOGETHERAI ?? 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
64
+ openrouter: process.env.DEFAULT_MODEL_OPENROUTER ?? 'anthropic/claude-sonnet-4-6',
65
+ nvidia: process.env.DEFAULT_MODEL_NVIDIA ?? 'meta/llama-3.1-70b-instruct',
66
+ ollama: process.env.DEFAULT_MODEL_OLLAMA ?? 'llama3.1',
67
+ };
68
+ // Platform admin LLM categories in priority order (first configured wins).
69
+ // apiKeyField: which DB field holds the API key (default: 'apiKey'); used for NVIDIA multi-key slots.
70
+ // noApiKey: provider works without an API key (ollama — check baseUrl instead).
71
+ const PLATFORM_LLM_CATEGORIES = [
72
+ { category: 'llm_openai', provider: 'openai', defaultModel: PLATFORM_DEFAULT_MODELS.openai },
73
+ { category: 'llm', provider: 'anthropic', defaultModel: PLATFORM_DEFAULT_MODELS.anthropic },
74
+ { category: 'llm_gemini', provider: 'gemini', defaultModel: PLATFORM_DEFAULT_MODELS.gemini,
75
+ baseUrl: LLM_BASE_URLS.gemini },
76
+ { category: 'llm_grok', provider: 'grok', defaultModel: PLATFORM_DEFAULT_MODELS.grok,
77
+ baseUrl: LLM_BASE_URLS.grok },
78
+ { category: 'llm_groq', provider: 'openrouter', defaultModel: PLATFORM_DEFAULT_MODELS.groq,
79
+ baseUrl: LLM_BASE_URLS.groq },
80
+ { category: 'llm_mistral', provider: 'mistral', defaultModel: PLATFORM_DEFAULT_MODELS.mistral,
81
+ baseUrl: LLM_BASE_URLS.mistral },
82
+ { category: 'llm_deepseek', provider: 'deepseek', defaultModel: PLATFORM_DEFAULT_MODELS.deepseek,
83
+ baseUrl: LLM_BASE_URLS.deepseek },
84
+ { category: 'llm_perplexity', provider: 'perplexity', defaultModel: PLATFORM_DEFAULT_MODELS.perplexity,
85
+ baseUrl: LLM_BASE_URLS.perplexity },
86
+ { category: 'llm_togetherai', provider: 'togetherai', defaultModel: PLATFORM_DEFAULT_MODELS.togetherai,
87
+ baseUrl: LLM_BASE_URLS.togetherai },
88
+ { category: 'llm_openrouter', provider: 'openrouter', defaultModel: PLATFORM_DEFAULT_MODELS.openrouter,
89
+ baseUrl: LLM_BASE_URLS.openrouter },
90
+ // NVIDIA NIM — 10 independent API key slots, tried in order for failover
91
+ { category: 'llm_nvidia', provider: 'nvidia', defaultModel: PLATFORM_DEFAULT_MODELS.nvidia,
92
+ baseUrl: LLM_BASE_URLS.nvidia, apiKeyField: 'apiKey' },
93
+ { category: 'llm_nvidia', provider: 'nvidia', defaultModel: PLATFORM_DEFAULT_MODELS.nvidia,
94
+ baseUrl: LLM_BASE_URLS.nvidia, apiKeyField: 'apiKey2' },
95
+ { category: 'llm_nvidia', provider: 'nvidia', defaultModel: PLATFORM_DEFAULT_MODELS.nvidia,
96
+ baseUrl: LLM_BASE_URLS.nvidia, apiKeyField: 'apiKey3' },
97
+ { category: 'llm_nvidia', provider: 'nvidia', defaultModel: PLATFORM_DEFAULT_MODELS.nvidia,
98
+ baseUrl: LLM_BASE_URLS.nvidia, apiKeyField: 'apiKey4' },
99
+ { category: 'llm_nvidia', provider: 'nvidia', defaultModel: PLATFORM_DEFAULT_MODELS.nvidia,
100
+ baseUrl: LLM_BASE_URLS.nvidia, apiKeyField: 'apiKey5' },
101
+ { category: 'llm_nvidia', provider: 'nvidia', defaultModel: PLATFORM_DEFAULT_MODELS.nvidia,
102
+ baseUrl: LLM_BASE_URLS.nvidia, apiKeyField: 'apiKey6' },
103
+ { category: 'llm_nvidia', provider: 'nvidia', defaultModel: PLATFORM_DEFAULT_MODELS.nvidia,
104
+ baseUrl: LLM_BASE_URLS.nvidia, apiKeyField: 'apiKey7' },
105
+ { category: 'llm_nvidia', provider: 'nvidia', defaultModel: PLATFORM_DEFAULT_MODELS.nvidia,
106
+ baseUrl: LLM_BASE_URLS.nvidia, apiKeyField: 'apiKey8' },
107
+ { category: 'llm_nvidia', provider: 'nvidia', defaultModel: PLATFORM_DEFAULT_MODELS.nvidia,
108
+ baseUrl: LLM_BASE_URLS.nvidia, apiKeyField: 'apiKey9' },
109
+ { category: 'llm_nvidia', provider: 'nvidia', defaultModel: PLATFORM_DEFAULT_MODELS.nvidia,
110
+ baseUrl: LLM_BASE_URLS.nvidia, apiKeyField: 'apiKey10' },
111
+ // Ollama — no API key required; configured when admin sets a custom base URL
112
+ { category: 'llm_ollama', provider: 'ollama', defaultModel: PLATFORM_DEFAULT_MODELS.ollama,
113
+ baseUrl: LLM_BASE_URLS.ollama, noApiKey: true },
114
+ ];
115
+ /** Resolve platform admin LLM config — first configured provider wins. */
116
+ async function getPlatformLLMConfig() {
117
+ // Cache per-category configs to avoid redundant DB calls for NVIDIA multi-key slots
118
+ const configCache = new Map();
119
+ for (const entry of PLATFORM_LLM_CATEGORIES) {
120
+ const { category, provider, defaultModel, apiKeyField, noApiKey } = entry;
121
+ try {
122
+ let cfg = configCache.get(category);
123
+ if (!cfg) {
124
+ cfg = await (0, core_1.getSystemConfig)(category);
125
+ configCache.set(category, cfg);
126
+ }
127
+ const resolvedBaseUrl = cfg.baseUrl || entry.baseUrl;
128
+ if (noApiKey) {
129
+ // For ollama: only activate if admin explicitly set a base URL or has a model configured
130
+ if (cfg.model || (resolvedBaseUrl && resolvedBaseUrl !== entry.baseUrl)) {
131
+ const model = cfg.model || defaultModel;
132
+ return { provider, model, apiKey: '', baseUrl: resolvedBaseUrl };
133
+ }
134
+ continue;
135
+ }
136
+ const keyField = apiKeyField ?? 'apiKey';
137
+ const apiKey = cfg[keyField];
138
+ if (apiKey) {
139
+ const model = cfg.model || defaultModel;
140
+ // assertAllowedModel not called — platform admin may configure any provider model
141
+ return { provider, model, apiKey, baseUrl: resolvedBaseUrl };
142
+ }
143
+ }
144
+ catch { /* try next */ }
145
+ }
146
+ // ENV fallback — last resort when neither workspace nor platform admin is configured.
147
+ if (process.env.OPENAI_API_KEY) {
148
+ return { provider: 'openai', model: PLATFORM_DEFAULT_MODELS.openai, apiKey: process.env.OPENAI_API_KEY };
149
+ }
150
+ if (process.env.ANTHROPIC_API_KEY) {
151
+ return { provider: 'anthropic', model: PLATFORM_DEFAULT_MODELS.anthropic, apiKey: process.env.ANTHROPIC_API_KEY };
152
+ }
153
+ return undefined;
154
+ }
155
+ /** Load the tenant's configured LLM provider. Falls back to platform admin config, then env vars. */
156
+ async function getAgentLLMConfig(tenantId) {
157
+ await (0, app_brain_1.enforceTokenBudget)(tenantId);
158
+ try {
159
+ const stored = await app_brain_1.AppBrain.getTenantLLMConfig(tenantId);
160
+ // apiKey check uses !== undefined so a decryption failure (undefined) correctly falls through,
161
+ // but a legitimate empty-string key doesn't silently drop the tenant config.
162
+ // assertAllowedModel is NOT called here — tenant-configured models are validated at save time
163
+ // and may include custom providers (NVIDIA NIM, Ollama, OpenRouter) not in the platform allowlist.
164
+ if (stored?.provider && stored?.model && stored?.apiKey !== undefined && stored?.apiKey !== null) {
165
+ return {
166
+ provider: stored.provider,
167
+ model: stored.model,
168
+ apiKey: stored.apiKey,
169
+ baseUrl: stored.baseUrl,
170
+ };
171
+ }
172
+ }
173
+ catch (e) {
174
+ console.warn('[llm-config] Failed to load tenant LLM config, falling back to platform config:', e);
175
+ }
176
+ // Fall back to platform admin config (set in Admin → Config → AI/LLM Providers)
177
+ return getPlatformLLMConfig();
178
+ }
179
+ /**
180
+ * Returns the platform admin LLM config directly, bypassing tenant config.
181
+ * Use this when a tenant-configured key fails at call time (401/403) to retry with the platform key.
182
+ */
183
+ async function getPlatformFallbackConfig() {
184
+ return getPlatformLLMConfig();
185
+ }
186
+ /**
187
+ * Runs `fn(tenantCfg)` and, if the provider rejects the API key (401/403), retries with
188
+ * platform admin config. Prevents a single expired workspace key from blocking all agent jobs.
189
+ *
190
+ * When tenantCfg is undefined (no tenant or platform config resolved), fails fast with a
191
+ * clear "No AI provider configured" error rather than making a live call with empty credentials.
192
+ */
193
+ async function tryWithPlatformFallback(tenantCfg, fn) {
194
+ if (!tenantCfg) {
195
+ // getTaskAgentLLMConfig already tried tenant → platform → env. All paths returned undefined.
196
+ throw new Error('No AI provider configured. Go to Admin → AI/LLM Providers and add an API key, ' +
197
+ 'or set OPENAI_API_KEY / ANTHROPIC_API_KEY in your environment.');
198
+ }
199
+ try {
200
+ return await fn(tenantCfg);
201
+ }
202
+ catch (e) {
203
+ const isAuthError = e?.name === 'LLMProviderError' && /API key|Unauthorized|rejected/i.test(e?.message ?? '');
204
+ if (!isAuthError || !tenantCfg.apiKey)
205
+ throw e;
206
+ const platformCfg = await getPlatformLLMConfig();
207
+ // Only retry if platform config has a different key — otherwise we'd loop with same failure.
208
+ if (!platformCfg || platformCfg.apiKey === tenantCfg.apiKey)
209
+ throw e;
210
+ console.warn('[llm-config] Workspace API key rejected (401/403), retrying with platform admin config');
211
+ return await fn(platformCfg);
212
+ }
213
+ }
214
+ /** Load workspace domain config for a project — used to inject domain hints into prompts. */
215
+ async function getWorkspaceDomainContext(tenantId, projectId) {
216
+ try {
217
+ const project = await app_brain_1.AppBrain.getProject(tenantId, projectId);
218
+ if (!project?.workspaceId)
219
+ return '';
220
+ // Load workspace settings directly via AppBrain
221
+ const ws = await app_brain_1.AppBrain.getWorkspaceSettings(tenantId, project.workspaceId);
222
+ if (!ws)
223
+ return '';
224
+ const parts = [];
225
+ if (ws.vertical)
226
+ parts.push(`Domain vertical: ${ws.vertical}`);
227
+ if (ws.industry)
228
+ parts.push(`Industry: ${ws.industry}`);
229
+ if (ws.complianceFrameworks?.length)
230
+ parts.push(`Compliance: ${ws.complianceFrameworks.join(', ')}`);
231
+ if (ws.requirementTypes?.length)
232
+ parts.push(`Requirement types: ${ws.requirementTypes.join(', ')}`);
233
+ return parts.length ? `\n\n## Workspace Domain Context\n${parts.join('\n')}` : '';
234
+ }
235
+ catch {
236
+ return '';
237
+ }
238
+ }
239
+ /** Returns true if the workspace has PHI masking enabled — redact real PII field values before LLM calls. */
240
+ async function getWorkspacePhiMasking(tenantId, projectId) {
241
+ try {
242
+ const project = await app_brain_1.AppBrain.getProject(tenantId, projectId);
243
+ if (!project?.workspaceId)
244
+ return false;
245
+ const ws = await app_brain_1.AppBrain.getWorkspaceSettings(tenantId, project.workspaceId);
246
+ return !!ws?.phiMaskingEnabled;
247
+ }
248
+ catch {
249
+ return false;
250
+ }
251
+ }
252
+ const PROVIDER_FAST_MODELS = {
253
+ anthropic: process.env.FAST_MODEL_ANTHROPIC ?? 'claude-haiku-4-5-20251001',
254
+ openai: process.env.FAST_MODEL_OPENAI ?? 'gpt-4o-mini',
255
+ gemini: process.env.FAST_MODEL_GEMINI ?? 'gemini-2.5-flash',
256
+ openrouter: process.env.FAST_MODEL_OPENROUTER ?? 'llama-3.3-70b-versatile',
257
+ };
258
+ /** Returns the tenant's LLM config swapped to the provider's fast mini-model for lower-cost passes. */
259
+ async function getFastPassLLMConfig(tenantId) {
260
+ const base = await getAgentLLMConfig(tenantId);
261
+ if (!base)
262
+ return undefined;
263
+ const fastModel = PROVIDER_FAST_MODELS[base.provider];
264
+ if (!fastModel)
265
+ return base;
266
+ return { ...base, model: fastModel };
267
+ }
268
+ /** Load the tenant's LLM config for a specific task, respecting per-task overrides. */
269
+ async function getTaskAgentLLMConfig(tenantId, task) {
270
+ await (0, app_brain_1.enforceTokenBudget)(tenantId);
271
+ try {
272
+ const stored = await app_brain_1.AppBrain.getTaskLLMConfig(tenantId, task);
273
+ if (!stored?.provider || !stored?.model || stored?.apiKey === undefined || stored?.apiKey === null) {
274
+ return getAgentLLMConfig(tenantId);
275
+ }
276
+ return {
277
+ provider: stored.provider,
278
+ model: stored.model,
279
+ apiKey: stored.apiKey,
280
+ baseUrl: stored.baseUrl,
281
+ };
282
+ }
283
+ catch (e) {
284
+ console.warn('[llm-config] Failed to load task LLM override, falling back to primary config:', e);
285
+ return getAgentLLMConfig(tenantId);
286
+ }
287
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Mock / record / replay layer for the LLM choke points in llm.ts.
3
+ *
4
+ * Purpose: let agent flows (routing, dedup, persistence, token accounting) run
5
+ * deterministically in CI without LLM spend. See docs/120-AGENT-E2E-HARNESS-PLAN.md.
6
+ *
7
+ * Env flags
8
+ * ZETA_LLM_MOCK '1' | 'replay' → replay fixtures (fallback if no fixture)
9
+ * 'record' → replay when a fixture exists, otherwise call
10
+ * the real provider and save the response
11
+ * ZETA_LLM_MOCK_DIR fixture directory (default: <cwd>/eval/fixtures/llm)
12
+ * ZETA_LLM_MOCK_FAIL '429' | '503' | 'malformed-json' | 'empty' → failure injection
13
+ *
14
+ * This module intentionally does NOT import llm.ts (avoids a circular import);
15
+ * llm.ts turns a `{ kind: 'error' }` outcome into a sanitized LLMProviderError.
16
+ */
17
+ import type { LLMUsage } from '@taodigital/core';
18
+ export type MockMode = 'off' | 'replay' | 'record';
19
+ export interface MockAskResult {
20
+ text: string;
21
+ usage: LLMUsage;
22
+ }
23
+ export type MockOutcome = {
24
+ kind: 'result';
25
+ res: MockAskResult;
26
+ source: 'fixture' | 'fallback' | 'injected';
27
+ } | {
28
+ kind: 'error';
29
+ status: number;
30
+ }
31
+ /** record mode only: no fixture exists, caller must hit the real provider then call saveFixture(). */
32
+ | {
33
+ kind: 'miss';
34
+ };
35
+ export type MockSystem = string | Array<{
36
+ text: string;
37
+ cache?: boolean;
38
+ }>;
39
+ export interface LlmCallStats {
40
+ calls: number;
41
+ promptTokens: number;
42
+ completionTokens: number;
43
+ estimatedCostUsd: number;
44
+ /** mock keys seen this window → how each was answered (fixture | fallback | injected | recorded | real) */
45
+ mockKeys: Record<string, string>;
46
+ }
47
+ export declare function resetLlmStats(): void;
48
+ export declare function snapshotLlmStats(): LlmCallStats;
49
+ export declare function recordLlmCall(usage: Partial<LLMUsage> | undefined, key?: string, source?: string): void;
50
+ interface FixtureFile {
51
+ key: string;
52
+ text: string;
53
+ usage?: Partial<LLMUsage>;
54
+ recordedAt?: string;
55
+ }
56
+ export declare function getMockMode(): MockMode;
57
+ export declare function fixtureDir(): string;
58
+ /**
59
+ * Stable agent/pass identifier derived from the pass-specific (last) system block.
60
+ * e.g. "…ZeTa Test Standard v2.0 Pass 3: Security…" → "pass-3-security"
61
+ * Falls back to a short hash of the block's first line so unknown agents still get a key.
62
+ */
63
+ export declare function agentId(system: MockSystem): string;
64
+ /**
65
+ * Key = <agent/pass id>__<screen slug or head-of-prompt hash>.
66
+ * Keys deliberately avoid hashing the full prompt so wording tweaks to prompts
67
+ * don't invalidate every fixture; only a different agent or screen changes the key.
68
+ */
69
+ export declare function mockKey(system: MockSystem, user: string, kind?: 'text' | 'vision'): string;
70
+ export declare function loadFixture(key: string): FixtureFile | null;
71
+ export declare function saveFixture(key: string, res: MockAskResult): void;
72
+ /** Decide how to answer a call while mocking is on. Never performs a network call itself. */
73
+ export declare function resolveMock(mode: Exclude<MockMode, 'off'>, system: MockSystem, user: string, opts?: {
74
+ schema?: unknown;
75
+ kind?: 'text' | 'vision';
76
+ }): MockOutcome;
77
+ export {};