@wundr.io/autogen-orchestrator 1.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.
@@ -0,0 +1,704 @@
1
+ /**
2
+ * Termination Condition Handlers for AutoGen-style Group Chat
3
+ *
4
+ * Implements various termination conditions for multi-agent conversations
5
+ * including keyword-based, round-based, timeout, and custom evaluators.
6
+ */
7
+
8
+ import type {
9
+ Message,
10
+ ChatParticipant,
11
+ ChatContext,
12
+ TerminationCondition,
13
+ TerminationConditionType,
14
+ TerminationConditionValue,
15
+ TerminationResult,
16
+ TerminationEvaluator,
17
+ ConsensusConfigType,
18
+ } from './types';
19
+
20
+ /**
21
+ * Handler interface for termination conditions
22
+ */
23
+ export interface TerminationHandler {
24
+ /**
25
+ * Evaluate whether the termination condition is met
26
+ * @param messages - Conversation messages
27
+ * @param participants - Chat participants
28
+ * @param context - Current chat context
29
+ * @returns Termination result
30
+ */
31
+ evaluate(
32
+ messages: Message[],
33
+ participants: ChatParticipant[],
34
+ context: ChatContext
35
+ ): Promise<TerminationResult>;
36
+ }
37
+
38
+ /**
39
+ * Type guard to check if value is a number
40
+ */
41
+ function isNumber(value: TerminationConditionValue): value is number {
42
+ return typeof value === 'number';
43
+ }
44
+
45
+ /**
46
+ * Type guard to check if value is a string or string array
47
+ */
48
+ function isStringOrStringArray(
49
+ value: TerminationConditionValue,
50
+ ): value is string | string[] {
51
+ return typeof value === 'string' || Array.isArray(value);
52
+ }
53
+
54
+ /**
55
+ * Type guard to check if value is a ConsensusConfigType
56
+ */
57
+ function isConsensusConfig(
58
+ value: TerminationConditionValue,
59
+ ): value is ConsensusConfigType {
60
+ return (
61
+ typeof value === 'object' &&
62
+ value !== null &&
63
+ 'threshold' in value &&
64
+ 'agreementKeywords' in value
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Factory function to create termination handlers
70
+ * @param condition - Termination condition configuration
71
+ * @returns Appropriate termination handler
72
+ */
73
+ export function createTerminationHandler(
74
+ condition: TerminationCondition,
75
+ ): TerminationHandler {
76
+ switch (condition.type) {
77
+ case 'max_rounds':
78
+ if (!isNumber(condition.value)) {
79
+ throw new Error('max_rounds condition requires a number value');
80
+ }
81
+ return new MaxRoundsHandler(condition.value);
82
+ case 'max_messages':
83
+ if (!isNumber(condition.value)) {
84
+ throw new Error('max_messages condition requires a number value');
85
+ }
86
+ return new MaxMessagesHandler(condition.value);
87
+ case 'keyword':
88
+ if (!isStringOrStringArray(condition.value)) {
89
+ throw new Error(
90
+ 'keyword condition requires a string or string[] value',
91
+ );
92
+ }
93
+ return new KeywordHandler(condition.value);
94
+ case 'timeout':
95
+ if (!isNumber(condition.value)) {
96
+ throw new Error('timeout condition requires a number value');
97
+ }
98
+ return new TimeoutHandler(condition.value);
99
+ case 'function':
100
+ if (!condition.evaluator) {
101
+ throw new Error('function condition requires an evaluator');
102
+ }
103
+ return new FunctionHandler(condition.evaluator);
104
+ case 'consensus':
105
+ if (!isConsensusConfig(condition.value)) {
106
+ throw new Error(
107
+ 'consensus condition requires a ConsensusConfigType value',
108
+ );
109
+ }
110
+ return new ConsensusHandler(condition.value);
111
+ case 'custom':
112
+ return new CustomHandler(condition);
113
+ default: {
114
+ const exhaustiveCheck: never = condition.type;
115
+ throw new Error(`Unknown termination condition type: ${exhaustiveCheck}`);
116
+ }
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Handler for maximum rounds termination
122
+ */
123
+ export class MaxRoundsHandler implements TerminationHandler {
124
+ private maxRounds: number;
125
+
126
+ /**
127
+ * Create a max rounds handler
128
+ * @param maxRounds - Maximum number of rounds allowed
129
+ */
130
+ constructor(maxRounds: number) {
131
+ this.maxRounds = maxRounds;
132
+ }
133
+
134
+ /**
135
+ * Evaluate if max rounds has been reached
136
+ * @param messages - Conversation messages
137
+ * @param participants - Chat participants
138
+ * @param context - Current chat context
139
+ * @returns Termination result
140
+ */
141
+ async evaluate(
142
+ _messages: Message[],
143
+ _participants: ChatParticipant[],
144
+ context: ChatContext,
145
+ ): Promise<TerminationResult> {
146
+ const shouldTerminate = context.currentRound >= this.maxRounds;
147
+
148
+ return {
149
+ shouldTerminate,
150
+ reason: shouldTerminate
151
+ ? `Maximum rounds reached: ${context.currentRound}/${this.maxRounds}`
152
+ : undefined,
153
+ summary: shouldTerminate
154
+ ? `Conversation ended after ${this.maxRounds} rounds.`
155
+ : undefined,
156
+ };
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Handler for maximum messages termination
162
+ */
163
+ export class MaxMessagesHandler implements TerminationHandler {
164
+ private maxMessages: number;
165
+
166
+ /**
167
+ * Create a max messages handler
168
+ * @param maxMessages - Maximum number of messages allowed
169
+ */
170
+ constructor(maxMessages: number) {
171
+ this.maxMessages = maxMessages;
172
+ }
173
+
174
+ /**
175
+ * Evaluate if max messages has been reached
176
+ * @param messages - Conversation messages
177
+ * @param participants - Chat participants
178
+ * @param context - Current chat context
179
+ * @returns Termination result
180
+ */
181
+ async evaluate(
182
+ messages: Message[],
183
+ _participants: ChatParticipant[],
184
+ _context: ChatContext,
185
+ ): Promise<TerminationResult> {
186
+ const shouldTerminate = messages.length >= this.maxMessages;
187
+
188
+ return {
189
+ shouldTerminate,
190
+ reason: shouldTerminate
191
+ ? `Maximum messages reached: ${messages.length}/${this.maxMessages}`
192
+ : undefined,
193
+ summary: shouldTerminate
194
+ ? `Conversation ended after ${this.maxMessages} messages.`
195
+ : undefined,
196
+ };
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Handler for keyword-based termination
202
+ */
203
+ export class KeywordHandler implements TerminationHandler {
204
+ private keywords: string[];
205
+ private caseSensitive: boolean;
206
+ private requireAll: boolean;
207
+
208
+ /**
209
+ * Create a keyword handler
210
+ * @param keywords - Keywords to detect
211
+ * @param caseSensitive - Whether matching is case-sensitive
212
+ * @param requireAll - Whether all keywords must be present
213
+ */
214
+ constructor(
215
+ keywords: string | string[],
216
+ caseSensitive = false,
217
+ requireAll = false,
218
+ ) {
219
+ this.keywords = Array.isArray(keywords) ? keywords : [keywords];
220
+ this.caseSensitive = caseSensitive;
221
+ this.requireAll = requireAll;
222
+ }
223
+
224
+ /**
225
+ * Evaluate if termination keywords are found
226
+ * @param messages - Conversation messages
227
+ * @param participants - Chat participants
228
+ * @param context - Current chat context
229
+ * @returns Termination result
230
+ */
231
+ async evaluate(
232
+ messages: Message[],
233
+ _participants: ChatParticipant[],
234
+ _context: ChatContext,
235
+ ): Promise<TerminationResult> {
236
+ const lastMessage = messages[messages.length - 1];
237
+ if (!lastMessage) {
238
+ return { shouldTerminate: false };
239
+ }
240
+
241
+ const content = this.caseSensitive
242
+ ? lastMessage.content
243
+ : lastMessage.content.toLowerCase();
244
+
245
+ const matchedKeywords: string[] = [];
246
+ for (const keyword of this.keywords) {
247
+ const searchKeyword = this.caseSensitive
248
+ ? keyword
249
+ : keyword.toLowerCase();
250
+ if (content.includes(searchKeyword)) {
251
+ matchedKeywords.push(keyword);
252
+ }
253
+ }
254
+
255
+ const shouldTerminate = this.requireAll
256
+ ? matchedKeywords.length === this.keywords.length
257
+ : matchedKeywords.length > 0;
258
+
259
+ return {
260
+ shouldTerminate,
261
+ reason: shouldTerminate
262
+ ? `Termination keyword(s) detected: ${matchedKeywords.join(', ')}`
263
+ : undefined,
264
+ summary: shouldTerminate
265
+ ? `Conversation terminated by keyword: "${matchedKeywords[0]}"`
266
+ : undefined,
267
+ data: shouldTerminate ? { matchedKeywords } : undefined,
268
+ };
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Handler for timeout-based termination
274
+ */
275
+ export class TimeoutHandler implements TerminationHandler {
276
+ private timeoutMs: number;
277
+
278
+ /**
279
+ * Create a timeout handler
280
+ * @param timeoutMs - Timeout duration in milliseconds
281
+ */
282
+ constructor(timeoutMs: number) {
283
+ this.timeoutMs = timeoutMs;
284
+ }
285
+
286
+ /**
287
+ * Evaluate if timeout has been reached
288
+ * @param messages - Conversation messages
289
+ * @param participants - Chat participants
290
+ * @param context - Current chat context
291
+ * @returns Termination result
292
+ */
293
+ async evaluate(
294
+ _messages: Message[],
295
+ _participants: ChatParticipant[],
296
+ context: ChatContext,
297
+ ): Promise<TerminationResult> {
298
+ const elapsed = Date.now() - context.startTime.getTime();
299
+ const shouldTerminate = elapsed >= this.timeoutMs;
300
+
301
+ return {
302
+ shouldTerminate,
303
+ reason: shouldTerminate
304
+ ? `Timeout reached: ${Math.round(elapsed / 1000)}s / ${Math.round(this.timeoutMs / 1000)}s`
305
+ : undefined,
306
+ summary: shouldTerminate
307
+ ? `Conversation timed out after ${Math.round(elapsed / 1000)} seconds.`
308
+ : undefined,
309
+ data: shouldTerminate
310
+ ? { elapsedMs: elapsed, timeoutMs: this.timeoutMs }
311
+ : undefined,
312
+ };
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Handler for function-based termination
318
+ */
319
+ export class FunctionHandler implements TerminationHandler {
320
+ private evaluator: TerminationEvaluator;
321
+
322
+ /**
323
+ * Create a function handler
324
+ * @param evaluator - Custom evaluation function
325
+ */
326
+ constructor(evaluator: TerminationEvaluator) {
327
+ this.evaluator = evaluator;
328
+ }
329
+
330
+ /**
331
+ * Evaluate using the custom function
332
+ * @param messages - Conversation messages
333
+ * @param participants - Chat participants
334
+ * @param context - Current chat context
335
+ * @returns Termination result
336
+ */
337
+ async evaluate(
338
+ messages: Message[],
339
+ participants: ChatParticipant[],
340
+ context: ChatContext,
341
+ ): Promise<TerminationResult> {
342
+ try {
343
+ return await this.evaluator(messages, participants, context);
344
+ } catch (error) {
345
+ const errorMessage =
346
+ error instanceof Error ? error.message : String(error);
347
+ return {
348
+ shouldTerminate: false,
349
+ reason: `Function evaluator error: ${errorMessage}`,
350
+ };
351
+ }
352
+ }
353
+ }
354
+
355
+ /**
356
+ * ConsensusConfig is re-exported for backwards compatibility
357
+ * @deprecated Use ConsensusConfigType from ./types instead
358
+ */
359
+ export type ConsensusConfig = ConsensusConfigType;
360
+
361
+ /**
362
+ * Handler for consensus-based termination
363
+ */
364
+ export class ConsensusHandler implements TerminationHandler {
365
+ private config: ConsensusConfigType;
366
+
367
+ /**
368
+ * Create a consensus handler
369
+ * @param config - Consensus configuration
370
+ */
371
+ constructor(config: ConsensusConfigType) {
372
+ this.config = {
373
+ threshold: config.threshold,
374
+ agreementKeywords: config.agreementKeywords || [
375
+ 'agree',
376
+ 'consensus',
377
+ 'approved',
378
+ 'done',
379
+ 'complete',
380
+ 'finished',
381
+ ],
382
+ disagreementKeywords: config.disagreementKeywords || [
383
+ 'disagree',
384
+ 'no',
385
+ 'reject',
386
+ 'veto',
387
+ 'continue',
388
+ ],
389
+ minParticipants: config.minParticipants || 2,
390
+ windowSize: config.windowSize || 10,
391
+ };
392
+ }
393
+
394
+ /**
395
+ * Evaluate if consensus has been reached
396
+ * @param messages - Conversation messages
397
+ * @param participants - Chat participants
398
+ * @param context - Current chat context
399
+ * @returns Termination result
400
+ */
401
+ async evaluate(
402
+ messages: Message[],
403
+ _participants: ChatParticipant[],
404
+ _context: ChatContext,
405
+ ): Promise<TerminationResult> {
406
+ const windowSize = this.config.windowSize || 10;
407
+ const recentMessages = messages.slice(-windowSize);
408
+
409
+ // Track votes per participant
410
+ const votes: Map<string, 'agree' | 'disagree' | 'neutral'> = new Map();
411
+
412
+ for (const message of recentMessages) {
413
+ const contentLower = message.content.toLowerCase();
414
+
415
+ // Check for agreement keywords
416
+ const hasAgreement = this.config.agreementKeywords.some(keyword =>
417
+ contentLower.includes(keyword.toLowerCase()),
418
+ );
419
+
420
+ // Check for disagreement keywords
421
+ const hasDisagreement = this.config.disagreementKeywords.some(keyword =>
422
+ contentLower.includes(keyword.toLowerCase()),
423
+ );
424
+
425
+ if (hasAgreement && !hasDisagreement) {
426
+ votes.set(message.name, 'agree');
427
+ } else if (hasDisagreement) {
428
+ votes.set(message.name, 'disagree');
429
+ }
430
+ }
431
+
432
+ const totalVoters = votes.size;
433
+ const agreements = Array.from(votes.values()).filter(
434
+ v => v === 'agree',
435
+ ).length;
436
+
437
+ const minParticipants = this.config.minParticipants || 2;
438
+
439
+ // Check if enough participants have voted
440
+ if (totalVoters < minParticipants) {
441
+ return {
442
+ shouldTerminate: false,
443
+ reason: `Not enough participants voted: ${totalVoters}/${minParticipants}`,
444
+ };
445
+ }
446
+
447
+ const agreementRate = agreements / totalVoters;
448
+ const shouldTerminate = agreementRate >= this.config.threshold;
449
+
450
+ return {
451
+ shouldTerminate,
452
+ reason: shouldTerminate
453
+ ? `Consensus reached: ${Math.round(agreementRate * 100)}% agreement (threshold: ${Math.round(this.config.threshold * 100)}%)`
454
+ : `No consensus: ${Math.round(agreementRate * 100)}% agreement (need: ${Math.round(this.config.threshold * 100)}%)`,
455
+ summary: shouldTerminate
456
+ ? `Conversation ended with ${Math.round(agreementRate * 100)}% participant agreement.`
457
+ : undefined,
458
+ data: {
459
+ agreementRate,
460
+ totalVoters,
461
+ agreements,
462
+ threshold: this.config.threshold,
463
+ },
464
+ };
465
+ }
466
+ }
467
+
468
+ /**
469
+ * Handler for custom termination conditions
470
+ */
471
+ export class CustomHandler implements TerminationHandler {
472
+ private condition: TerminationCondition;
473
+
474
+ /**
475
+ * Create a custom handler
476
+ * @param condition - Custom termination condition
477
+ */
478
+ constructor(condition: TerminationCondition) {
479
+ this.condition = condition;
480
+ }
481
+
482
+ /**
483
+ * Evaluate the custom condition
484
+ * @param messages - Conversation messages
485
+ * @param participants - Chat participants
486
+ * @param context - Current chat context
487
+ * @returns Termination result
488
+ */
489
+ async evaluate(
490
+ messages: Message[],
491
+ participants: ChatParticipant[],
492
+ context: ChatContext,
493
+ ): Promise<TerminationResult> {
494
+ // If there's a custom evaluator, use it
495
+ if (this.condition.evaluator) {
496
+ return this.condition.evaluator(messages, participants, context);
497
+ }
498
+
499
+ // Otherwise, try to interpret the value
500
+ const value = this.condition.value;
501
+
502
+ if (typeof value === 'function') {
503
+ return (value as TerminationEvaluator)(messages, participants, context);
504
+ }
505
+
506
+ // Default: no termination
507
+ return {
508
+ shouldTerminate: false,
509
+ reason: 'Custom condition not properly configured',
510
+ };
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Manager for multiple termination conditions
516
+ */
517
+ export class TerminationManager {
518
+ private handlers: Map<string, TerminationHandler> = new Map();
519
+ private conditions: TerminationCondition[] = [];
520
+
521
+ /**
522
+ * Create a termination manager
523
+ * @param conditions - Initial termination conditions
524
+ */
525
+ constructor(conditions: TerminationCondition[] = []) {
526
+ for (const condition of conditions) {
527
+ this.addCondition(condition);
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Add a termination condition
533
+ * @param condition - Condition to add
534
+ */
535
+ addCondition(condition: TerminationCondition): void {
536
+ const id = `${condition.type}-${this.conditions.length}`;
537
+ const handler = createTerminationHandler(condition);
538
+ this.handlers.set(id, handler);
539
+ this.conditions.push(condition);
540
+ }
541
+
542
+ /**
543
+ * Remove a termination condition by type
544
+ * @param type - Condition type to remove
545
+ */
546
+ removeCondition(type: TerminationConditionType): void {
547
+ const indicesToRemove: number[] = [];
548
+
549
+ this.conditions.forEach((condition, index) => {
550
+ if (condition.type === type) {
551
+ indicesToRemove.push(index);
552
+ }
553
+ });
554
+
555
+ // Remove in reverse order to maintain indices
556
+ for (let i = indicesToRemove.length - 1; i >= 0; i--) {
557
+ const index = indicesToRemove[i];
558
+ if (index !== undefined) {
559
+ const id = `${type}-${index}`;
560
+ this.handlers.delete(id);
561
+ this.conditions.splice(index, 1);
562
+ }
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Clear all termination conditions
568
+ */
569
+ clearConditions(): void {
570
+ this.handlers.clear();
571
+ this.conditions = [];
572
+ }
573
+
574
+ /**
575
+ * Evaluate all termination conditions
576
+ * @param messages - Conversation messages
577
+ * @param participants - Chat participants
578
+ * @param context - Current chat context
579
+ * @returns Combined termination result
580
+ */
581
+ async evaluate(
582
+ messages: Message[],
583
+ participants: ChatParticipant[],
584
+ context: ChatContext,
585
+ ): Promise<TerminationResult> {
586
+ const results: Array<TerminationResult & { type: string }> = [];
587
+
588
+ for (const [id, handler] of this.handlers.entries()) {
589
+ const result = await handler.evaluate(messages, participants, context);
590
+ results.push({ ...result, type: id });
591
+
592
+ // Early exit if any condition triggers termination
593
+ if (result.shouldTerminate) {
594
+ return {
595
+ shouldTerminate: true,
596
+ reason: result.reason,
597
+ summary: result.summary,
598
+ data: {
599
+ triggeredBy: id,
600
+ allResults: results,
601
+ },
602
+ };
603
+ }
604
+ }
605
+
606
+ return {
607
+ shouldTerminate: false,
608
+ data: { allResults: results },
609
+ };
610
+ }
611
+
612
+ /**
613
+ * Get all configured conditions
614
+ * @returns Array of termination conditions
615
+ */
616
+ getConditions(): TerminationCondition[] {
617
+ return [...this.conditions];
618
+ }
619
+
620
+ /**
621
+ * Check if a specific condition type is configured
622
+ * @param type - Condition type to check
623
+ * @returns Whether the condition type exists
624
+ */
625
+ hasCondition(type: TerminationConditionType): boolean {
626
+ return this.conditions.some(c => c.type === type);
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Common termination condition presets
632
+ */
633
+ export const TerminationPresets = {
634
+ /**
635
+ * Create a preset for task completion detection
636
+ */
637
+ taskCompletion(): TerminationCondition {
638
+ return {
639
+ type: 'keyword',
640
+ value: ['TASK_COMPLETE', 'DONE', 'FINISHED', 'COMPLETED', 'END_TASK'],
641
+ description: 'Terminate when task completion keyword is detected',
642
+ };
643
+ },
644
+
645
+ /**
646
+ * Create a preset for approval workflows
647
+ */
648
+ approval(): TerminationCondition {
649
+ const consensusValue: ConsensusConfigType = {
650
+ threshold: 0.75,
651
+ agreementKeywords: ['approve', 'approved', 'lgtm', 'ship it'],
652
+ disagreementKeywords: ['reject', 'denied', 'needs work'],
653
+ minParticipants: 2,
654
+ };
655
+ return {
656
+ type: 'consensus',
657
+ value: consensusValue,
658
+ description: 'Terminate when approval consensus is reached',
659
+ };
660
+ },
661
+
662
+ /**
663
+ * Create a preset for quick discussions
664
+ * @param rounds - Maximum rounds
665
+ */
666
+ quickDiscussion(rounds = 5): TerminationCondition[] {
667
+ return [
668
+ {
669
+ type: 'max_rounds',
670
+ value: rounds,
671
+ description: `Maximum ${rounds} rounds`,
672
+ },
673
+ {
674
+ type: 'keyword',
675
+ value: ['TERMINATE', 'END'],
676
+ description: 'Manual termination keywords',
677
+ },
678
+ ];
679
+ },
680
+
681
+ /**
682
+ * Create a preset for long-running tasks
683
+ * @param timeoutMinutes - Timeout in minutes
684
+ */
685
+ longRunning(timeoutMinutes = 30): TerminationCondition[] {
686
+ return [
687
+ {
688
+ type: 'timeout',
689
+ value: timeoutMinutes * 60 * 1000,
690
+ description: `${timeoutMinutes} minute timeout`,
691
+ },
692
+ {
693
+ type: 'max_messages',
694
+ value: 100,
695
+ description: 'Maximum 100 messages',
696
+ },
697
+ {
698
+ type: 'keyword',
699
+ value: ['TERMINATE', 'ABORT', 'CANCEL'],
700
+ description: 'Emergency termination keywords',
701
+ },
702
+ ];
703
+ },
704
+ };