@defai.digital/agent-domain 13.0.3

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 (77) hide show
  1. package/LICENSE +214 -0
  2. package/dist/enhanced-executor.d.ts +170 -0
  3. package/dist/enhanced-executor.d.ts.map +1 -0
  4. package/dist/enhanced-executor.js +1072 -0
  5. package/dist/enhanced-executor.js.map +1 -0
  6. package/dist/executor.d.ts +120 -0
  7. package/dist/executor.d.ts.map +1 -0
  8. package/dist/executor.js +929 -0
  9. package/dist/executor.js.map +1 -0
  10. package/dist/index.d.ts +25 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +34 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/loader.d.ts +50 -0
  15. package/dist/loader.d.ts.map +1 -0
  16. package/dist/loader.js +160 -0
  17. package/dist/loader.js.map +1 -0
  18. package/dist/persistent-registry.d.ts +105 -0
  19. package/dist/persistent-registry.d.ts.map +1 -0
  20. package/dist/persistent-registry.js +183 -0
  21. package/dist/persistent-registry.js.map +1 -0
  22. package/dist/production-factories.d.ts +70 -0
  23. package/dist/production-factories.d.ts.map +1 -0
  24. package/dist/production-factories.js +434 -0
  25. package/dist/production-factories.js.map +1 -0
  26. package/dist/prompt-executor.d.ts +119 -0
  27. package/dist/prompt-executor.d.ts.map +1 -0
  28. package/dist/prompt-executor.js +211 -0
  29. package/dist/prompt-executor.js.map +1 -0
  30. package/dist/registry.d.ts +57 -0
  31. package/dist/registry.d.ts.map +1 -0
  32. package/dist/registry.js +123 -0
  33. package/dist/registry.js.map +1 -0
  34. package/dist/selection-service.d.ts +74 -0
  35. package/dist/selection-service.d.ts.map +1 -0
  36. package/dist/selection-service.js +322 -0
  37. package/dist/selection-service.js.map +1 -0
  38. package/dist/selector.d.ts +51 -0
  39. package/dist/selector.d.ts.map +1 -0
  40. package/dist/selector.js +249 -0
  41. package/dist/selector.js.map +1 -0
  42. package/dist/stub-checkpoint.d.ts +23 -0
  43. package/dist/stub-checkpoint.d.ts.map +1 -0
  44. package/dist/stub-checkpoint.js +137 -0
  45. package/dist/stub-checkpoint.js.map +1 -0
  46. package/dist/stub-delegation-tracker.d.ts +25 -0
  47. package/dist/stub-delegation-tracker.d.ts.map +1 -0
  48. package/dist/stub-delegation-tracker.js +118 -0
  49. package/dist/stub-delegation-tracker.js.map +1 -0
  50. package/dist/stub-parallel-executor.d.ts +19 -0
  51. package/dist/stub-parallel-executor.d.ts.map +1 -0
  52. package/dist/stub-parallel-executor.js +176 -0
  53. package/dist/stub-parallel-executor.js.map +1 -0
  54. package/dist/types.d.ts +614 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +15 -0
  57. package/dist/types.js.map +1 -0
  58. package/dist/workflow-templates.d.ts +117 -0
  59. package/dist/workflow-templates.d.ts.map +1 -0
  60. package/dist/workflow-templates.js +342 -0
  61. package/dist/workflow-templates.js.map +1 -0
  62. package/package.json +51 -0
  63. package/src/enhanced-executor.ts +1395 -0
  64. package/src/executor.ts +1153 -0
  65. package/src/index.ts +172 -0
  66. package/src/loader.ts +191 -0
  67. package/src/persistent-registry.ts +235 -0
  68. package/src/production-factories.ts +613 -0
  69. package/src/prompt-executor.ts +310 -0
  70. package/src/registry.ts +167 -0
  71. package/src/selection-service.ts +411 -0
  72. package/src/selector.ts +299 -0
  73. package/src/stub-checkpoint.ts +187 -0
  74. package/src/stub-delegation-tracker.ts +161 -0
  75. package/src/stub-parallel-executor.ts +224 -0
  76. package/src/types.ts +784 -0
  77. package/src/workflow-templates.ts +393 -0
@@ -0,0 +1,411 @@
1
+ /**
2
+ * Agent Selection Service
3
+ *
4
+ * Domain service for agent recommendation and capability discovery.
5
+ * Implements contract-defined invariants for agent selection.
6
+ *
7
+ * Invariants:
8
+ * - INV-AGT-SEL-001: Selection is deterministic (same input = same output)
9
+ * - INV-AGT-SEL-002: Confidence scores must be between 0 and 1
10
+ * - INV-AGT-SEL-003: Results must be sorted by confidence descending
11
+ * - INV-AGT-SEL-004: Always returns at least one result (fallback to 'standard')
12
+ * - INV-AGT-SEL-005: exampleTasks boost confidence when matched
13
+ * - INV-AGT-SEL-006: notForTasks reduce confidence when matched
14
+ */
15
+
16
+ import type {
17
+ AgentProfile,
18
+ AgentRecommendRequest,
19
+ AgentRecommendResult,
20
+ AgentCapabilitiesRequest,
21
+ AgentCapabilitiesResult,
22
+ AgentCategory,
23
+ } from '@defai.digital/contracts';
24
+ import type { AgentRegistry } from './types.js';
25
+
26
+ // ============================================================================
27
+ // Constants
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Default agent ID when no match is found (INV-AGT-SEL-004)
32
+ */
33
+ const FALLBACK_AGENT_ID = 'standard';
34
+
35
+ /**
36
+ * Default confidence for fallback agent (INV-AGT-SEL-004)
37
+ */
38
+ const FALLBACK_CONFIDENCE = 0.5;
39
+
40
+ /**
41
+ * Minimum confidence threshold for selection
42
+ */
43
+ const MIN_CONFIDENCE_THRESHOLD = 0.1;
44
+
45
+ /**
46
+ * Scoring weights (INV-AGT-SEL-005, INV-AGT-SEL-006)
47
+ */
48
+ const SCORE_WEIGHTS = {
49
+ EXACT_EXAMPLE_MATCH: 0.6, // INV-AGT-SEL-005: Exact match adds +0.6
50
+ SUBSTRING_EXAMPLE_MATCH: 0.4, // INV-AGT-SEL-005: Substring match adds +0.4
51
+ NOT_FOR_TASK_PENALTY: -0.5, // INV-AGT-SEL-006: NotForTask subtracts -0.5
52
+ PRIMARY_INTENT: 0.3,
53
+ KEYWORD: 0.15,
54
+ ANTI_KEYWORD: -0.2,
55
+ NEGATIVE_INTENT: -0.3,
56
+ CAPABILITY: 0.1,
57
+ REQUIRED_CAPABILITY_FULL: 0.25,
58
+ EXPERTISE: 0.15,
59
+ ROLE: 0.1,
60
+ DESCRIPTION_WORD: 0.05,
61
+ TEAM: 0.1,
62
+ REDIRECT: -0.5,
63
+ } as const;
64
+
65
+ // ============================================================================
66
+ // Types
67
+ // ============================================================================
68
+
69
+ /**
70
+ * Internal scored agent result
71
+ */
72
+ interface ScoredAgent {
73
+ agentId: string;
74
+ confidence: number;
75
+ reason: string;
76
+ matchDetails: string[];
77
+ }
78
+
79
+ /**
80
+ * Selection service port interface
81
+ */
82
+ export interface AgentSelectionServicePort {
83
+ /**
84
+ * Recommend the best agent for a task
85
+ */
86
+ recommend(request: AgentRecommendRequest): Promise<AgentRecommendResult>;
87
+
88
+ /**
89
+ * Get all agent capabilities
90
+ */
91
+ getCapabilities(request: AgentCapabilitiesRequest): Promise<AgentCapabilitiesResult>;
92
+ }
93
+
94
+ // ============================================================================
95
+ // Agent Selection Service Implementation
96
+ // ============================================================================
97
+
98
+ /**
99
+ * Agent Selection Service
100
+ *
101
+ * Implements deterministic agent selection based on task matching.
102
+ */
103
+ export class AgentSelectionService implements AgentSelectionServicePort {
104
+ constructor(private readonly registry: AgentRegistry) {}
105
+
106
+ /**
107
+ * Recommend the best agent for a task
108
+ *
109
+ * Implements:
110
+ * - INV-AGT-SEL-001: Deterministic selection
111
+ * - INV-AGT-SEL-002: Confidence range [0,1]
112
+ * - INV-AGT-SEL-003: Sorted by confidence descending
113
+ * - INV-AGT-SEL-004: Fallback to 'standard'
114
+ */
115
+ async recommend(request: AgentRecommendRequest): Promise<AgentRecommendResult> {
116
+ // Get all enabled agents, optionally filtered by team
117
+ const filter = request.team !== undefined
118
+ ? { team: request.team, enabled: true as const }
119
+ : { enabled: true as const };
120
+
121
+ const agents = await this.registry.list(filter);
122
+
123
+ // Filter out excluded agents
124
+ const filteredAgents = request.excludeAgents
125
+ ? agents.filter((a) => !request.excludeAgents!.includes(a.agentId))
126
+ : agents;
127
+
128
+ // Score each agent (INV-AGT-SEL-001: deterministic)
129
+ const scored = filteredAgents
130
+ .map((agent) => this.scoreAgent(agent, request.task, request.requiredCapabilities))
131
+ .filter((result) => result.confidence >= MIN_CONFIDENCE_THRESHOLD);
132
+
133
+ // INV-AGT-SEL-003: Sort by confidence descending, then by agentId for tie-breaking
134
+ scored.sort((a, b) => {
135
+ const confidenceDiff = b.confidence - a.confidence;
136
+ if (confidenceDiff !== 0) return confidenceDiff;
137
+ return a.agentId.localeCompare(b.agentId); // Deterministic tie-breaking
138
+ });
139
+
140
+ // INV-AGT-SEL-004: Always return at least one result
141
+ if (scored.length === 0) {
142
+ return {
143
+ recommended: FALLBACK_AGENT_ID,
144
+ confidence: FALLBACK_CONFIDENCE,
145
+ reason: 'No specific agent matched, using default generalist agent',
146
+ alternatives: [],
147
+ };
148
+ }
149
+
150
+ // Get top N results based on maxResults
151
+ const topResults = scored.slice(0, request.maxResults);
152
+ const best = topResults[0]!;
153
+
154
+ return {
155
+ recommended: best.agentId,
156
+ confidence: best.confidence,
157
+ reason: best.reason,
158
+ alternatives: topResults.slice(1).map((s) => ({
159
+ agentId: s.agentId,
160
+ confidence: s.confidence,
161
+ reason: s.reason,
162
+ })),
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Get all agent capabilities
168
+ */
169
+ async getCapabilities(request: AgentCapabilitiesRequest): Promise<AgentCapabilitiesResult> {
170
+ // Get agents based on request filters
171
+ const filter = request.includeDisabled ? {} : { enabled: true as const };
172
+ let agents = await this.registry.list(filter);
173
+
174
+ // Filter by category if specified
175
+ if (request.category) {
176
+ agents = agents.filter(
177
+ (a) => a.selectionMetadata?.agentCategory === request.category
178
+ );
179
+ }
180
+
181
+ // Build capability mappings
182
+ const allCapabilities = new Set<string>();
183
+ const agentsByCapability: Record<string, string[]> = {};
184
+ const capabilitiesByAgent: Record<string, string[]> = {};
185
+ const categoriesByAgent: Record<string, AgentCategory> = {};
186
+
187
+ for (const agent of agents) {
188
+ const caps = agent.capabilities ?? [];
189
+ capabilitiesByAgent[agent.agentId] = caps;
190
+
191
+ // Track category
192
+ if (agent.selectionMetadata?.agentCategory) {
193
+ categoriesByAgent[agent.agentId] = agent.selectionMetadata.agentCategory;
194
+ }
195
+
196
+ // Build reverse mapping
197
+ for (const cap of caps) {
198
+ allCapabilities.add(cap);
199
+ if (!agentsByCapability[cap]) {
200
+ agentsByCapability[cap] = [];
201
+ }
202
+ agentsByCapability[cap].push(agent.agentId);
203
+ }
204
+ }
205
+
206
+ return {
207
+ capabilities: Array.from(allCapabilities).sort(),
208
+ agentsByCapability,
209
+ capabilitiesByAgent,
210
+ categoriesByAgent: Object.keys(categoriesByAgent).length > 0 ? categoriesByAgent : undefined,
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Score an agent for a task
216
+ *
217
+ * Implements:
218
+ * - INV-AGT-SEL-002: Confidence clamped to [0,1]
219
+ * - INV-AGT-SEL-005: exampleTasks boost
220
+ * - INV-AGT-SEL-006: notForTasks penalty
221
+ */
222
+ private scoreAgent(
223
+ agent: AgentProfile,
224
+ task: string,
225
+ requiredCapabilities?: string[]
226
+ ): ScoredAgent {
227
+ const taskLower = task.toLowerCase();
228
+ const taskNormalized = this.normalizeText(taskLower);
229
+ const taskWords = this.extractWords(taskLower);
230
+
231
+ let score = 0;
232
+ const matchDetails: string[] = [];
233
+
234
+ // INV-AGT-SEL-005: Score based on exampleTasks (highest priority)
235
+ const exampleTasks = agent.selectionMetadata?.exampleTasks ?? [];
236
+ for (const example of exampleTasks) {
237
+ const exampleNormalized = this.normalizeText(example.toLowerCase());
238
+ if (taskNormalized === exampleNormalized) {
239
+ score += SCORE_WEIGHTS.EXACT_EXAMPLE_MATCH;
240
+ matchDetails.push(`exact example match: "${example}"`);
241
+ break;
242
+ } else if (
243
+ taskLower.includes(example.toLowerCase()) ||
244
+ example.toLowerCase().includes(taskLower)
245
+ ) {
246
+ score += SCORE_WEIGHTS.SUBSTRING_EXAMPLE_MATCH;
247
+ matchDetails.push(`example match: "${example}"`);
248
+ break;
249
+ }
250
+ }
251
+
252
+ // INV-AGT-SEL-006: Negative score for notForTasks
253
+ const notForTasks = agent.selectionMetadata?.notForTasks ?? [];
254
+ for (const notFor of notForTasks) {
255
+ if (
256
+ taskLower.includes(notFor.toLowerCase()) ||
257
+ notFor.toLowerCase().includes(taskLower)
258
+ ) {
259
+ score += SCORE_WEIGHTS.NOT_FOR_TASK_PENALTY;
260
+ matchDetails.push(`not-for match: "${notFor}"`);
261
+ break;
262
+ }
263
+ }
264
+
265
+ // Score based on primary intents
266
+ const primaryIntents = agent.selectionMetadata?.primaryIntents ?? [];
267
+ for (const intent of primaryIntents) {
268
+ if (taskLower.includes(intent.toLowerCase())) {
269
+ score += SCORE_WEIGHTS.PRIMARY_INTENT;
270
+ matchDetails.push(`primary intent: ${intent}`);
271
+ }
272
+ }
273
+
274
+ // Score based on keywords
275
+ const keywords = agent.selectionMetadata?.keywords ?? [];
276
+ for (const keyword of keywords) {
277
+ if (taskLower.includes(keyword.toLowerCase())) {
278
+ score += SCORE_WEIGHTS.KEYWORD;
279
+ matchDetails.push(`keyword: ${keyword}`);
280
+ }
281
+ }
282
+
283
+ // Negative score for anti-keywords
284
+ const antiKeywords = agent.selectionMetadata?.antiKeywords ?? [];
285
+ for (const antiKeyword of antiKeywords) {
286
+ if (taskLower.includes(antiKeyword.toLowerCase())) {
287
+ score += SCORE_WEIGHTS.ANTI_KEYWORD;
288
+ matchDetails.push(`anti-keyword: ${antiKeyword}`);
289
+ }
290
+ }
291
+
292
+ // Score based on negative intents
293
+ const negativeIntents = agent.selectionMetadata?.negativeIntents ?? [];
294
+ for (const intent of negativeIntents) {
295
+ if (taskLower.includes(intent.toLowerCase())) {
296
+ score += SCORE_WEIGHTS.NEGATIVE_INTENT;
297
+ matchDetails.push(`negative intent: ${intent}`);
298
+ }
299
+ }
300
+
301
+ // Score based on capabilities
302
+ const capabilities = agent.capabilities ?? [];
303
+ for (const cap of capabilities) {
304
+ if (taskWords.some((w) => cap.toLowerCase().includes(w))) {
305
+ score += SCORE_WEIGHTS.CAPABILITY;
306
+ matchDetails.push(`capability: ${cap}`);
307
+ }
308
+ }
309
+
310
+ // Score based on required capabilities
311
+ if (requiredCapabilities && requiredCapabilities.length > 0) {
312
+ const matches = requiredCapabilities.filter((req) =>
313
+ capabilities.some((cap) => cap.toLowerCase().includes(req.toLowerCase()))
314
+ );
315
+ if (matches.length === requiredCapabilities.length) {
316
+ score += SCORE_WEIGHTS.REQUIRED_CAPABILITY_FULL;
317
+ matchDetails.push('all required capabilities');
318
+ } else if (matches.length > 0) {
319
+ const partial = SCORE_WEIGHTS.CAPABILITY * (matches.length / requiredCapabilities.length);
320
+ score += partial;
321
+ matchDetails.push(`${matches.length}/${requiredCapabilities.length} required capabilities`);
322
+ }
323
+ }
324
+
325
+ // Score based on expertise
326
+ const expertise = agent.expertise ?? [];
327
+ for (const exp of expertise) {
328
+ if (taskWords.some((w) => exp.toLowerCase().includes(w))) {
329
+ score += SCORE_WEIGHTS.EXPERTISE;
330
+ matchDetails.push(`expertise: ${exp}`);
331
+ }
332
+ }
333
+
334
+ // Score based on role match
335
+ if (agent.role) {
336
+ const roleWords = this.extractWords(agent.role.toLowerCase());
337
+ const roleMatches = roleWords.filter((r) => taskWords.includes(r));
338
+ if (roleMatches.length > 0) {
339
+ score += SCORE_WEIGHTS.ROLE;
340
+ matchDetails.push(`role: ${agent.role}`);
341
+ }
342
+ }
343
+
344
+ // Score based on description match
345
+ if (agent.description) {
346
+ const descWords = this.extractWords(agent.description.toLowerCase());
347
+ const descMatches = descWords.filter((d) => taskWords.includes(d)).length;
348
+ if (descMatches > 2) {
349
+ score += SCORE_WEIGHTS.DESCRIPTION_WORD * Math.min(descMatches, 5);
350
+ matchDetails.push(`description: ${descMatches} words`);
351
+ }
352
+ }
353
+
354
+ // Check redirect rules (suggests this agent is NOT right)
355
+ const redirectRules = agent.selectionMetadata?.redirectWhen ?? [];
356
+ for (const rule of redirectRules) {
357
+ if (taskLower.includes(rule.phrase.toLowerCase())) {
358
+ score += SCORE_WEIGHTS.REDIRECT;
359
+ matchDetails.push(`redirect to: ${rule.suggest}`);
360
+ }
361
+ }
362
+
363
+ // INV-AGT-SEL-002: Clamp confidence to [0,1]
364
+ const confidence = Math.max(0, Math.min(1, score));
365
+
366
+ // Build reason from match details
367
+ const reason = matchDetails.length > 0
368
+ ? matchDetails.slice(0, 3).join('; ')
369
+ : 'no specific match';
370
+
371
+ return {
372
+ agentId: agent.agentId,
373
+ confidence,
374
+ reason,
375
+ matchDetails,
376
+ };
377
+ }
378
+
379
+ /**
380
+ * Normalize text for comparison
381
+ */
382
+ private normalizeText(text: string): string {
383
+ return text
384
+ .replace(/[^\w\s]/g, ' ')
385
+ .replace(/\s+/g, ' ')
386
+ .trim();
387
+ }
388
+
389
+ /**
390
+ * Extract meaningful words from text
391
+ */
392
+ private extractWords(text: string): string[] {
393
+ return text
394
+ .split(/[\s\-_,./]+/)
395
+ .filter((w) => w.length > 2)
396
+ .map((w) => w.toLowerCase());
397
+ }
398
+ }
399
+
400
+ // ============================================================================
401
+ // Factory Function
402
+ // ============================================================================
403
+
404
+ /**
405
+ * Creates an agent selection service
406
+ */
407
+ export function createAgentSelectionService(
408
+ registry: AgentRegistry
409
+ ): AgentSelectionServicePort {
410
+ return new AgentSelectionService(registry);
411
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Agent Selector
3
+ *
4
+ * Selects the best agent for a given task based on keywords,
5
+ * capabilities, context, and example tasks.
6
+ *
7
+ * Invariants:
8
+ * - INV-AGT-SEL-001: Selection is deterministic (same input = same output)
9
+ * - INV-AGT-SEL-002: Confidence scores must be between 0 and 1
10
+ * - INV-AGT-SEL-003: Results must be sorted by confidence descending
11
+ * - INV-AGT-SEL-004: Always returns at least one result (fallback to 'standard')
12
+ * - INV-AGT-SEL-005: exampleTasks boost confidence when matched
13
+ * - INV-AGT-SEL-006: notForTasks reduce confidence when matched
14
+ */
15
+
16
+ import type { AgentProfile } from '@defai.digital/contracts';
17
+ import type {
18
+ AgentRegistry,
19
+ AgentSelector,
20
+ AgentSelectionResult,
21
+ AgentSelectionContext,
22
+ } from './types.js';
23
+
24
+ // ============================================================================
25
+ // Constants
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Default agent ID when no match is found
30
+ */
31
+ const DEFAULT_AGENT_ID = 'standard';
32
+
33
+ /**
34
+ * Minimum confidence threshold for selection
35
+ */
36
+ const MIN_CONFIDENCE_THRESHOLD = 0.1;
37
+
38
+ // ============================================================================
39
+ // Keyword-Based Agent Selector
40
+ // ============================================================================
41
+
42
+ /**
43
+ * Keyword-based agent selector implementation
44
+ */
45
+ export class KeywordAgentSelector implements AgentSelector {
46
+ constructor(private readonly registry: AgentRegistry) {}
47
+
48
+ /**
49
+ * Select the best agent for a task
50
+ */
51
+ async select(
52
+ task: string,
53
+ context?: AgentSelectionContext
54
+ ): Promise<AgentSelectionResult> {
55
+ const matches = await this.match(task, context);
56
+
57
+ if (matches.length === 0) {
58
+ // Return default agent
59
+ return {
60
+ agentId: DEFAULT_AGENT_ID,
61
+ confidence: 0.5,
62
+ reason: 'No specific agent matched, using default',
63
+ alternatives: [],
64
+ };
65
+ }
66
+
67
+ const best = matches[0]!;
68
+ return {
69
+ ...best,
70
+ alternatives: matches.slice(1).map((m) => ({
71
+ agentId: m.agentId,
72
+ confidence: m.confidence,
73
+ })),
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Get all agents that match a task
79
+ */
80
+ async match(
81
+ task: string,
82
+ context?: AgentSelectionContext
83
+ ): Promise<AgentSelectionResult[]> {
84
+ const filter = context?.team !== undefined
85
+ ? { team: context.team, enabled: true as const }
86
+ : { enabled: true as const };
87
+ const agents = await this.registry.list(filter);
88
+
89
+ // Filter by excluded agents
90
+ const filtered = context?.excludeAgents
91
+ ? agents.filter((a) => !context.excludeAgents!.includes(a.agentId))
92
+ : agents;
93
+
94
+ // Score each agent
95
+ const scored = filtered
96
+ .map((agent) => this.scoreAgent(agent, task, context))
97
+ .filter((result) => result.confidence >= MIN_CONFIDENCE_THRESHOLD);
98
+
99
+ // INV-AGT-SEL-003: Sort by confidence descending
100
+ scored.sort((a, b) => b.confidence - a.confidence);
101
+
102
+ return scored;
103
+ }
104
+
105
+ /**
106
+ * Score an agent for a task
107
+ *
108
+ * Implements invariants:
109
+ * - INV-AGT-SEL-005: exampleTasks boost confidence when matched
110
+ * - INV-AGT-SEL-006: notForTasks reduce confidence when matched
111
+ */
112
+ private scoreAgent(
113
+ agent: AgentProfile,
114
+ task: string,
115
+ context?: AgentSelectionContext
116
+ ): AgentSelectionResult {
117
+ const taskLower = task.toLowerCase();
118
+ const taskNormalized = this.normalizeText(taskLower);
119
+ const taskWords = this.extractWords(taskLower);
120
+
121
+ let score = 0;
122
+ const reasons: string[] = [];
123
+
124
+ // INV-AGT-SEL-005: Score based on exampleTasks (highest priority)
125
+ const exampleTasks = agent.selectionMetadata?.exampleTasks ?? [];
126
+ for (const example of exampleTasks) {
127
+ const exampleNormalized = this.normalizeText(example.toLowerCase());
128
+ if (taskNormalized === exampleNormalized) {
129
+ // Exact match (normalized) adds +0.6
130
+ score += 0.6;
131
+ reasons.push(`exact example match: "${example}"`);
132
+ break; // Only count best match
133
+ } else if (taskLower.includes(example.toLowerCase()) || example.toLowerCase().includes(taskLower)) {
134
+ // Substring match adds +0.4
135
+ score += 0.4;
136
+ reasons.push(`example match: "${example}"`);
137
+ break; // Only count best match
138
+ }
139
+ }
140
+
141
+ // INV-AGT-SEL-006: Negative score for notForTasks
142
+ const notForTasks = agent.selectionMetadata?.notForTasks ?? [];
143
+ for (const notFor of notForTasks) {
144
+ if (taskLower.includes(notFor.toLowerCase()) || notFor.toLowerCase().includes(taskLower)) {
145
+ // NotForTask match subtracts -0.5
146
+ score -= 0.5;
147
+ reasons.push(`not-for match: "${notFor}"`);
148
+ break; // Only count first match
149
+ }
150
+ }
151
+
152
+ // Score based on primary intents
153
+ const primaryIntents = agent.selectionMetadata?.primaryIntents ?? [];
154
+ for (const intent of primaryIntents) {
155
+ if (taskLower.includes(intent.toLowerCase())) {
156
+ score += 0.3;
157
+ reasons.push(`primary intent match: ${intent}`);
158
+ }
159
+ }
160
+
161
+ // Score based on keywords
162
+ const keywords = agent.selectionMetadata?.keywords ?? [];
163
+ for (const keyword of keywords) {
164
+ if (taskLower.includes(keyword.toLowerCase())) {
165
+ score += 0.15;
166
+ reasons.push(`keyword match: ${keyword}`);
167
+ }
168
+ }
169
+
170
+ // Negative score for anti-keywords
171
+ const antiKeywords = agent.selectionMetadata?.antiKeywords ?? [];
172
+ for (const antiKeyword of antiKeywords) {
173
+ if (taskLower.includes(antiKeyword.toLowerCase())) {
174
+ score -= 0.2;
175
+ reasons.push(`anti-keyword match: ${antiKeyword}`);
176
+ }
177
+ }
178
+
179
+ // Score based on negative intents
180
+ const negativeIntents = agent.selectionMetadata?.negativeIntents ?? [];
181
+ for (const intent of negativeIntents) {
182
+ if (taskLower.includes(intent.toLowerCase())) {
183
+ score -= 0.3;
184
+ reasons.push(`negative intent match: ${intent}`);
185
+ }
186
+ }
187
+
188
+ // Score based on capabilities
189
+ const capabilities = agent.capabilities ?? [];
190
+ for (const cap of capabilities) {
191
+ if (taskWords.some((w) => cap.toLowerCase().includes(w))) {
192
+ score += 0.1;
193
+ reasons.push(`capability match: ${cap}`);
194
+ }
195
+ }
196
+
197
+ // Score based on required capabilities in context
198
+ if (context?.requiredCapabilities) {
199
+ const matches = context.requiredCapabilities.filter((req) =>
200
+ capabilities.some((cap) =>
201
+ cap.toLowerCase().includes(req.toLowerCase())
202
+ )
203
+ );
204
+ if (matches.length === context.requiredCapabilities.length) {
205
+ score += 0.25;
206
+ reasons.push('all required capabilities match');
207
+ } else if (matches.length > 0) {
208
+ score += 0.1 * (matches.length / context.requiredCapabilities.length);
209
+ reasons.push(`${matches.length}/${context.requiredCapabilities.length} required capabilities`);
210
+ }
211
+ }
212
+
213
+ // Score based on expertise
214
+ const expertise = agent.expertise ?? [];
215
+ for (const exp of expertise) {
216
+ if (taskWords.some((w) => exp.toLowerCase().includes(w))) {
217
+ score += 0.15;
218
+ reasons.push(`expertise match: ${exp}`);
219
+ }
220
+ }
221
+
222
+ // Score based on role match
223
+ if (agent.role) {
224
+ const roleWords = this.extractWords(agent.role.toLowerCase());
225
+ const roleMatches = roleWords.filter((r) => taskWords.includes(r));
226
+ if (roleMatches.length > 0) {
227
+ score += 0.1;
228
+ reasons.push(`role match: ${agent.role}`);
229
+ }
230
+ }
231
+
232
+ // Score based on description match
233
+ if (agent.description) {
234
+ const descWords = this.extractWords(agent.description.toLowerCase());
235
+ const descMatches = descWords.filter((d) => taskWords.includes(d)).length;
236
+ if (descMatches > 2) {
237
+ score += 0.05 * Math.min(descMatches, 5);
238
+ reasons.push(`description match: ${descMatches} words`);
239
+ }
240
+ }
241
+
242
+ // Team bonus
243
+ if (context?.team && agent.team === context.team) {
244
+ score += 0.1;
245
+ reasons.push(`team match: ${agent.team}`);
246
+ }
247
+
248
+ // Check redirect rules
249
+ const redirectRules = agent.selectionMetadata?.redirectWhen ?? [];
250
+ for (const rule of redirectRules) {
251
+ if (taskLower.includes(rule.phrase.toLowerCase())) {
252
+ // This agent suggests redirecting to another
253
+ score -= 0.5;
254
+ reasons.push(`redirect suggested to: ${rule.suggest}`);
255
+ }
256
+ }
257
+
258
+ // INV-AGT-SEL-002: Clamp confidence between 0 and 1
259
+ const confidence = Math.max(0, Math.min(1, score));
260
+
261
+ return {
262
+ agentId: agent.agentId,
263
+ confidence,
264
+ reason: reasons.length > 0 ? reasons.join('; ') : 'no specific match',
265
+ alternatives: [],
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Normalize text for comparison (remove punctuation, extra spaces)
271
+ */
272
+ private normalizeText(text: string): string {
273
+ return text
274
+ .replace(/[^\w\s]/g, ' ')
275
+ .replace(/\s+/g, ' ')
276
+ .trim();
277
+ }
278
+
279
+ /**
280
+ * Extract words from text
281
+ */
282
+ private extractWords(text: string): string[] {
283
+ return text
284
+ .split(/[\s\-_,./]+/)
285
+ .filter((w) => w.length > 2)
286
+ .map((w) => w.toLowerCase());
287
+ }
288
+ }
289
+
290
+ // ============================================================================
291
+ // Factory Function
292
+ // ============================================================================
293
+
294
+ /**
295
+ * Creates a keyword-based agent selector
296
+ */
297
+ export function createAgentSelector(registry: AgentRegistry): AgentSelector {
298
+ return new KeywordAgentSelector(registry);
299
+ }