@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.
- package/LICENSE +214 -0
- package/dist/enhanced-executor.d.ts +170 -0
- package/dist/enhanced-executor.d.ts.map +1 -0
- package/dist/enhanced-executor.js +1072 -0
- package/dist/enhanced-executor.js.map +1 -0
- package/dist/executor.d.ts +120 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +929 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +50 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +160 -0
- package/dist/loader.js.map +1 -0
- package/dist/persistent-registry.d.ts +105 -0
- package/dist/persistent-registry.d.ts.map +1 -0
- package/dist/persistent-registry.js +183 -0
- package/dist/persistent-registry.js.map +1 -0
- package/dist/production-factories.d.ts +70 -0
- package/dist/production-factories.d.ts.map +1 -0
- package/dist/production-factories.js +434 -0
- package/dist/production-factories.js.map +1 -0
- package/dist/prompt-executor.d.ts +119 -0
- package/dist/prompt-executor.d.ts.map +1 -0
- package/dist/prompt-executor.js +211 -0
- package/dist/prompt-executor.js.map +1 -0
- package/dist/registry.d.ts +57 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +123 -0
- package/dist/registry.js.map +1 -0
- package/dist/selection-service.d.ts +74 -0
- package/dist/selection-service.d.ts.map +1 -0
- package/dist/selection-service.js +322 -0
- package/dist/selection-service.js.map +1 -0
- package/dist/selector.d.ts +51 -0
- package/dist/selector.d.ts.map +1 -0
- package/dist/selector.js +249 -0
- package/dist/selector.js.map +1 -0
- package/dist/stub-checkpoint.d.ts +23 -0
- package/dist/stub-checkpoint.d.ts.map +1 -0
- package/dist/stub-checkpoint.js +137 -0
- package/dist/stub-checkpoint.js.map +1 -0
- package/dist/stub-delegation-tracker.d.ts +25 -0
- package/dist/stub-delegation-tracker.d.ts.map +1 -0
- package/dist/stub-delegation-tracker.js +118 -0
- package/dist/stub-delegation-tracker.js.map +1 -0
- package/dist/stub-parallel-executor.d.ts +19 -0
- package/dist/stub-parallel-executor.d.ts.map +1 -0
- package/dist/stub-parallel-executor.js +176 -0
- package/dist/stub-parallel-executor.js.map +1 -0
- package/dist/types.d.ts +614 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-templates.d.ts +117 -0
- package/dist/workflow-templates.d.ts.map +1 -0
- package/dist/workflow-templates.js +342 -0
- package/dist/workflow-templates.js.map +1 -0
- package/package.json +51 -0
- package/src/enhanced-executor.ts +1395 -0
- package/src/executor.ts +1153 -0
- package/src/index.ts +172 -0
- package/src/loader.ts +191 -0
- package/src/persistent-registry.ts +235 -0
- package/src/production-factories.ts +613 -0
- package/src/prompt-executor.ts +310 -0
- package/src/registry.ts +167 -0
- package/src/selection-service.ts +411 -0
- package/src/selector.ts +299 -0
- package/src/stub-checkpoint.ts +187 -0
- package/src/stub-delegation-tracker.ts +161 -0
- package/src/stub-parallel-executor.ts +224 -0
- package/src/types.ts +784 -0
- 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
|
+
}
|
package/src/selector.ts
ADDED
|
@@ -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
|
+
}
|