@theihtisham/ai-agent-starter-kit 1.0.0
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/.env.example +33 -0
- package/Dockerfile +35 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/docker-compose.yml +28 -0
- package/next-env.d.ts +5 -0
- package/next.config.mjs +17 -0
- package/package.json +85 -0
- package/postcss.config.js +6 -0
- package/prisma/schema.prisma +157 -0
- package/prisma/seed.ts +46 -0
- package/src/app/(auth)/forgot-password/page.tsx +56 -0
- package/src/app/(auth)/layout.tsx +7 -0
- package/src/app/(auth)/login/page.tsx +83 -0
- package/src/app/(auth)/signup/page.tsx +108 -0
- package/src/app/(dashboard)/agents/[id]/edit/page.tsx +68 -0
- package/src/app/(dashboard)/agents/[id]/page.tsx +114 -0
- package/src/app/(dashboard)/agents/new/page.tsx +43 -0
- package/src/app/(dashboard)/agents/page.tsx +63 -0
- package/src/app/(dashboard)/api-keys/page.tsx +139 -0
- package/src/app/(dashboard)/dashboard/page.tsx +79 -0
- package/src/app/(dashboard)/layout.tsx +16 -0
- package/src/app/(dashboard)/settings/billing/page.tsx +59 -0
- package/src/app/(dashboard)/settings/page.tsx +45 -0
- package/src/app/(dashboard)/usage/page.tsx +46 -0
- package/src/app/api/agents/[id]/chat/route.ts +100 -0
- package/src/app/api/agents/[id]/chats/route.ts +36 -0
- package/src/app/api/agents/[id]/route.ts +97 -0
- package/src/app/api/agents/route.ts +84 -0
- package/src/app/api/api-keys/[id]/route.ts +25 -0
- package/src/app/api/api-keys/route.ts +72 -0
- package/src/app/api/auth/[...nextauth]/route.ts +5 -0
- package/src/app/api/auth/register/route.ts +53 -0
- package/src/app/api/health/route.ts +26 -0
- package/src/app/api/stripe/checkout/route.ts +37 -0
- package/src/app/api/stripe/plans/route.ts +16 -0
- package/src/app/api/stripe/portal/route.ts +29 -0
- package/src/app/api/stripe/webhook/route.ts +45 -0
- package/src/app/api/usage/route.ts +43 -0
- package/src/app/globals.css +59 -0
- package/src/app/layout.tsx +22 -0
- package/src/app/page.tsx +32 -0
- package/src/app/pricing/page.tsx +25 -0
- package/src/components/agents/agent-form.tsx +137 -0
- package/src/components/agents/model-selector.tsx +35 -0
- package/src/components/agents/tool-selector.tsx +48 -0
- package/src/components/auth-provider.tsx +17 -0
- package/src/components/billing/plan-badge.tsx +23 -0
- package/src/components/billing/pricing-table.tsx +95 -0
- package/src/components/billing/usage-meter.tsx +39 -0
- package/src/components/chat/chat-input.tsx +68 -0
- package/src/components/chat/chat-interface.tsx +152 -0
- package/src/components/chat/chat-message.tsx +50 -0
- package/src/components/chat/chat-sidebar.tsx +49 -0
- package/src/components/chat/code-block.tsx +38 -0
- package/src/components/chat/markdown-renderer.tsx +56 -0
- package/src/components/chat/streaming-text.tsx +46 -0
- package/src/components/dashboard/agent-card.tsx +52 -0
- package/src/components/dashboard/header.tsx +75 -0
- package/src/components/dashboard/sidebar.tsx +52 -0
- package/src/components/dashboard/stat-card.tsx +42 -0
- package/src/components/dashboard/usage-chart.tsx +42 -0
- package/src/components/landing/cta.tsx +30 -0
- package/src/components/landing/features.tsx +75 -0
- package/src/components/landing/hero.tsx +42 -0
- package/src/components/landing/pricing.tsx +28 -0
- package/src/components/ui/avatar.tsx +24 -0
- package/src/components/ui/badge.tsx +24 -0
- package/src/components/ui/button.tsx +39 -0
- package/src/components/ui/card.tsx +50 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/dropdown.tsx +77 -0
- package/src/components/ui/input.tsx +23 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +48 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +20 -0
- package/src/hooks/use-agent.ts +44 -0
- package/src/hooks/use-streaming.ts +82 -0
- package/src/hooks/use-subscription.ts +40 -0
- package/src/hooks/use-usage.ts +43 -0
- package/src/hooks/use-user.ts +13 -0
- package/src/lib/agents/index.ts +60 -0
- package/src/lib/agents/memory/long-term.ts +241 -0
- package/src/lib/agents/memory/manager.ts +154 -0
- package/src/lib/agents/memory/short-term.ts +155 -0
- package/src/lib/agents/memory/types.ts +68 -0
- package/src/lib/agents/orchestration/debate.ts +170 -0
- package/src/lib/agents/orchestration/index.ts +103 -0
- package/src/lib/agents/orchestration/parallel.ts +143 -0
- package/src/lib/agents/orchestration/router.ts +199 -0
- package/src/lib/agents/orchestration/sequential.ts +127 -0
- package/src/lib/agents/orchestration/types.ts +68 -0
- package/src/lib/agents/tools/calculator.ts +131 -0
- package/src/lib/agents/tools/code-executor.ts +191 -0
- package/src/lib/agents/tools/file-reader.ts +129 -0
- package/src/lib/agents/tools/index.ts +48 -0
- package/src/lib/agents/tools/registry.ts +182 -0
- package/src/lib/agents/tools/web-search.ts +83 -0
- package/src/lib/ai/agent.ts +275 -0
- package/src/lib/ai/context.ts +68 -0
- package/src/lib/ai/memory.ts +98 -0
- package/src/lib/ai/models.ts +80 -0
- package/src/lib/ai/streaming.ts +80 -0
- package/src/lib/ai/tools.ts +149 -0
- package/src/lib/auth/middleware.ts +41 -0
- package/src/lib/auth/nextauth.ts +69 -0
- package/src/lib/db/client.ts +15 -0
- package/src/lib/rate-limit/limiter.ts +93 -0
- package/src/lib/rate-limit/rules.ts +38 -0
- package/src/lib/stripe/client.ts +25 -0
- package/src/lib/stripe/plans.ts +75 -0
- package/src/lib/stripe/usage.ts +123 -0
- package/src/lib/stripe/webhooks.ts +96 -0
- package/src/lib/utils/api-response.ts +85 -0
- package/src/lib/utils/errors.ts +73 -0
- package/src/lib/utils/helpers.ts +50 -0
- package/src/lib/utils/id.ts +21 -0
- package/src/lib/utils/logger.ts +38 -0
- package/src/lib/utils/validation.ts +44 -0
- package/src/middleware.ts +13 -0
- package/src/types/agent.ts +31 -0
- package/src/types/api.ts +38 -0
- package/src/types/billing.ts +35 -0
- package/src/types/chat.ts +30 -0
- package/src/types/next-auth.d.ts +19 -0
- package/tailwind.config.ts +72 -0
- package/tsconfig.json +28 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debate Orchestrator — Agents discuss a topic, moderator synthesizes final answer.
|
|
3
|
+
*
|
|
4
|
+
* Multiple agents present their views on the same topic across rounds,
|
|
5
|
+
* then a moderator agent synthesizes the discussion into a final answer.
|
|
6
|
+
*
|
|
7
|
+
* Example: Three analysts debate market strategy, moderator picks the best approach.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
AgentConfig,
|
|
12
|
+
AgentMessage,
|
|
13
|
+
OrchestrationResult,
|
|
14
|
+
OrchestratorOptions,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
export interface DebateOptions extends OrchestratorOptions {
|
|
18
|
+
/** Number of debate rounds (default: 2) */
|
|
19
|
+
rounds?: number;
|
|
20
|
+
/** The moderator agent that synthesizes the final answer */
|
|
21
|
+
moderator: AgentConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class DebateOrchestrator {
|
|
25
|
+
private agents: AgentConfig[];
|
|
26
|
+
private debateOptions: DebateOptions;
|
|
27
|
+
|
|
28
|
+
constructor(agents: AgentConfig[], options: DebateOptions) {
|
|
29
|
+
if (agents.length === 0) {
|
|
30
|
+
throw new Error('DebateOrchestrator requires at least one debating agent');
|
|
31
|
+
}
|
|
32
|
+
if (!options.moderator) {
|
|
33
|
+
throw new Error('DebateOrchestrator requires a moderator agent');
|
|
34
|
+
}
|
|
35
|
+
this.agents = agents;
|
|
36
|
+
this.debateOptions = {
|
|
37
|
+
rounds: 2,
|
|
38
|
+
timeoutMs: 120_000,
|
|
39
|
+
maxRetries: 1,
|
|
40
|
+
...options,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Run a debate across multiple rounds, then have the moderator synthesize.
|
|
46
|
+
*/
|
|
47
|
+
async run(topic: string): Promise<OrchestrationResult> {
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
const trace: AgentMessage[] = [];
|
|
50
|
+
const rounds = this.debateOptions.rounds ?? 2;
|
|
51
|
+
const timeoutMs = this.debateOptions.timeoutMs ?? 120_000;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const debateHistory: string[] = [];
|
|
55
|
+
let roundPrompt = `Topic: ${topic}\n\nPlease present your analysis and perspective on this topic.`;
|
|
56
|
+
|
|
57
|
+
// Run debate rounds
|
|
58
|
+
for (let round = 0; round < rounds; round++) {
|
|
59
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
60
|
+
return {
|
|
61
|
+
content: 'Debate timed out.',
|
|
62
|
+
agentIds: this.agents.map((a) => a.id),
|
|
63
|
+
trace,
|
|
64
|
+
durationMs: Date.now() - startTime,
|
|
65
|
+
success: false,
|
|
66
|
+
error: `Timeout after ${timeoutMs}ms`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const roundMessages: string[] = [];
|
|
71
|
+
|
|
72
|
+
for (const agent of this.agents) {
|
|
73
|
+
const inputMessage: AgentMessage = {
|
|
74
|
+
content: roundPrompt,
|
|
75
|
+
fromAgent: round === 0 ? 'user' : 'debate',
|
|
76
|
+
toAgent: agent.id,
|
|
77
|
+
metadata: { round: round + 1, topic },
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const maxRetries = this.debateOptions.maxRetries ?? 1;
|
|
83
|
+
let result: AgentMessage | undefined;
|
|
84
|
+
|
|
85
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
86
|
+
try {
|
|
87
|
+
result = await agent.execute(inputMessage, agent);
|
|
88
|
+
break;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (attempt >= maxRetries) {
|
|
91
|
+
result = {
|
|
92
|
+
content: `[Agent "${agent.name}" encountered an error: ${(err as Error).message}]`,
|
|
93
|
+
fromAgent: agent.id,
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
trace.push(inputMessage);
|
|
101
|
+
trace.push(result!);
|
|
102
|
+
this.debateOptions.onAgentMessage?.(result!);
|
|
103
|
+
|
|
104
|
+
roundMessages.push(`**${agent.name}** (Round ${round + 1}):\n${result!.content}`);
|
|
105
|
+
debateHistory.push(`[${agent.name} — Round ${round + 1}]: ${result!.content}`);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const errorMsg = `[Agent "${agent.name}" failed: ${(err as Error).message}]`;
|
|
108
|
+
roundMessages.push(errorMsg);
|
|
109
|
+
debateHistory.push(errorMsg);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Build prompt for next round incorporating previous responses
|
|
114
|
+
if (round < rounds - 1) {
|
|
115
|
+
roundPrompt = [
|
|
116
|
+
`Topic: ${topic}`,
|
|
117
|
+
'',
|
|
118
|
+
`Previous round responses:`,
|
|
119
|
+
...roundMessages,
|
|
120
|
+
'',
|
|
121
|
+
'Consider the above perspectives. You may revise your position, address counterarguments, or strengthen your points.',
|
|
122
|
+
].join('\n');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Moderator synthesis
|
|
127
|
+
const moderatorPrompt = [
|
|
128
|
+
`You are moderating a debate on the following topic: "${topic}"`,
|
|
129
|
+
'',
|
|
130
|
+
`Here is the complete debate history:`,
|
|
131
|
+
...debateHistory.map((h, i) => `${i + 1}. ${h}`),
|
|
132
|
+
'',
|
|
133
|
+
'Please synthesize the key points, areas of agreement and disagreement, and provide a balanced final conclusion.',
|
|
134
|
+
].join('\n');
|
|
135
|
+
|
|
136
|
+
const moderatorInput: AgentMessage = {
|
|
137
|
+
content: moderatorPrompt,
|
|
138
|
+
fromAgent: 'debate',
|
|
139
|
+
toAgent: this.debateOptions.moderator.id,
|
|
140
|
+
metadata: { role: 'moderator', topic, rounds },
|
|
141
|
+
timestamp: Date.now(),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const moderatorResult = await this.debateOptions.moderator.execute(
|
|
145
|
+
moderatorInput,
|
|
146
|
+
this.debateOptions.moderator,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
trace.push(moderatorInput);
|
|
150
|
+
trace.push(moderatorResult);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
content: moderatorResult.content,
|
|
154
|
+
agentIds: [...this.agents.map((a) => a.id), this.debateOptions.moderator.id],
|
|
155
|
+
trace,
|
|
156
|
+
durationMs: Date.now() - startTime,
|
|
157
|
+
success: true,
|
|
158
|
+
};
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return {
|
|
161
|
+
content: '',
|
|
162
|
+
agentIds: [],
|
|
163
|
+
trace,
|
|
164
|
+
durationMs: Date.now() - startTime,
|
|
165
|
+
success: false,
|
|
166
|
+
error: (err as Error).message,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Agent Orchestration -- Factory and re-exports.
|
|
3
|
+
*
|
|
4
|
+
* Provides a factory function to create orchestrators by pattern name,
|
|
5
|
+
* plus re-exports of all orchestrator classes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AgentConfig,
|
|
10
|
+
OrchestrationPattern,
|
|
11
|
+
OrchestrationResult,
|
|
12
|
+
OrchestratorOptions,
|
|
13
|
+
RouterClassifier,
|
|
14
|
+
} from './types';
|
|
15
|
+
import { SequentialOrchestrator } from './sequential';
|
|
16
|
+
import { ParallelOrchestrator } from './parallel';
|
|
17
|
+
import { RouterOrchestrator, keywordClassifier } from './router';
|
|
18
|
+
import { DebateOrchestrator } from './debate';
|
|
19
|
+
|
|
20
|
+
export { SequentialOrchestrator } from './sequential';
|
|
21
|
+
export { ParallelOrchestrator } from './parallel';
|
|
22
|
+
export { RouterOrchestrator, keywordClassifier } from './router';
|
|
23
|
+
export { DebateOrchestrator } from './debate';
|
|
24
|
+
export type { DebateOptions } from './debate';
|
|
25
|
+
export type {
|
|
26
|
+
AgentConfig as OrchestrationAgentConfig,
|
|
27
|
+
AgentMessage,
|
|
28
|
+
Handoff,
|
|
29
|
+
OrchestrationPattern,
|
|
30
|
+
OrchestrationResult,
|
|
31
|
+
OrchestratorOptions,
|
|
32
|
+
RouterClassifier,
|
|
33
|
+
} from './types';
|
|
34
|
+
|
|
35
|
+
export interface CreateOrchestratorOptions extends OrchestratorOptions {
|
|
36
|
+
/** Required for 'router' pattern -- the classifier function */
|
|
37
|
+
classifier?: RouterClassifier;
|
|
38
|
+
/** Required for 'debate' pattern -- the moderator agent */
|
|
39
|
+
moderator?: AgentConfig;
|
|
40
|
+
/** Number of debate rounds (default: 2, only for 'debate' pattern) */
|
|
41
|
+
rounds?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface IOrchestrator {
|
|
45
|
+
run(input: string): Promise<OrchestrationResult>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type AnyOrchestrator = IOrchestrator;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Factory function -- create an orchestrator by pattern name.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const orchestrator = createOrchestrator('sequential', agents);
|
|
56
|
+
* const result = await orchestrator.run("Analyze this data");
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function createOrchestrator(
|
|
60
|
+
pattern: OrchestrationPattern,
|
|
61
|
+
agents: AgentConfig[],
|
|
62
|
+
options: CreateOrchestratorOptions = {},
|
|
63
|
+
): AnyOrchestrator {
|
|
64
|
+
switch (pattern) {
|
|
65
|
+
case 'sequential':
|
|
66
|
+
return new SequentialOrchestrator(agents, options);
|
|
67
|
+
|
|
68
|
+
case 'parallel':
|
|
69
|
+
return new ParallelOrchestrator(agents, options);
|
|
70
|
+
|
|
71
|
+
case 'router': {
|
|
72
|
+
const classifier = options.classifier ?? keywordClassifier();
|
|
73
|
+
return new RouterOrchestrator(agents, classifier, options);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case 'debate': {
|
|
77
|
+
if (!options.moderator) {
|
|
78
|
+
throw new Error('Debate pattern requires a moderator agent in options.moderator');
|
|
79
|
+
}
|
|
80
|
+
return new DebateOrchestrator(agents, {
|
|
81
|
+
...options,
|
|
82
|
+
moderator: options.moderator,
|
|
83
|
+
rounds: options.rounds,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
default:
|
|
88
|
+
throw new Error(`Unknown orchestration pattern: ${pattern}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Quick helper -- run a pattern with agents and input in one call.
|
|
94
|
+
*/
|
|
95
|
+
export async function orchestrate(
|
|
96
|
+
pattern: OrchestrationPattern,
|
|
97
|
+
agents: AgentConfig[],
|
|
98
|
+
input: string,
|
|
99
|
+
options: CreateOrchestratorOptions = {},
|
|
100
|
+
): Promise<OrchestrationResult> {
|
|
101
|
+
const orchestrator = createOrchestrator(pattern, agents, options);
|
|
102
|
+
return orchestrator.run(input);
|
|
103
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel Orchestrator — Runs multiple agents simultaneously.
|
|
3
|
+
*
|
|
4
|
+
* All agents receive the same input, and their results are merged.
|
|
5
|
+
* Useful for getting multiple perspectives or running independent analyses.
|
|
6
|
+
*
|
|
7
|
+
* Example: Sentiment Agent + Facts Agent + Grammar Agent all process the same text
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
AgentConfig,
|
|
12
|
+
AgentMessage,
|
|
13
|
+
OrchestrationResult,
|
|
14
|
+
OrchestratorOptions,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
export class ParallelOrchestrator {
|
|
18
|
+
private agents: AgentConfig[];
|
|
19
|
+
private options: OrchestratorOptions;
|
|
20
|
+
|
|
21
|
+
constructor(agents: AgentConfig[], options: OrchestratorOptions = {}) {
|
|
22
|
+
if (agents.length === 0) {
|
|
23
|
+
throw new Error('ParallelOrchestrator requires at least one agent');
|
|
24
|
+
}
|
|
25
|
+
this.agents = agents;
|
|
26
|
+
this.options = {
|
|
27
|
+
timeoutMs: 60_000,
|
|
28
|
+
maxRetries: 1,
|
|
29
|
+
...options,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Run all agents in parallel and merge their results.
|
|
35
|
+
* The merged output combines all agent responses, labeled by agent name.
|
|
36
|
+
*/
|
|
37
|
+
async run(input: string): Promise<OrchestrationResult> {
|
|
38
|
+
const startTime = Date.now();
|
|
39
|
+
const trace: AgentMessage[] = [];
|
|
40
|
+
const timeoutMs = this.options.timeoutMs ?? 60_000;
|
|
41
|
+
|
|
42
|
+
const inputMessage: AgentMessage = {
|
|
43
|
+
content: input,
|
|
44
|
+
fromAgent: 'user',
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Execute all agents concurrently with a timeout
|
|
50
|
+
const results = await Promise.allSettled(
|
|
51
|
+
this.agents.map(async (agent) => {
|
|
52
|
+
const maxRetries = this.options.maxRetries ?? 1;
|
|
53
|
+
let lastError: Error | undefined;
|
|
54
|
+
|
|
55
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
56
|
+
try {
|
|
57
|
+
const result = await agent.execute(inputMessage, agent);
|
|
58
|
+
this.options.onAgentMessage?.(result);
|
|
59
|
+
return { agent, result };
|
|
60
|
+
} catch (err) {
|
|
61
|
+
lastError = err as Error;
|
|
62
|
+
if (attempt < maxRetries) {
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
agent,
|
|
70
|
+
result: {
|
|
71
|
+
content: `[Error: Agent "${agent.name}" failed — ${lastError?.message}]`,
|
|
72
|
+
fromAgent: agent.id,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
} satisfies AgentMessage,
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Process results
|
|
80
|
+
const successfulResults: Array<{ agent: AgentConfig; result: AgentMessage }> = [];
|
|
81
|
+
const errors: string[] = [];
|
|
82
|
+
|
|
83
|
+
for (const settled of results) {
|
|
84
|
+
if (settled.status === 'fulfilled') {
|
|
85
|
+
successfulResults.push(settled.value);
|
|
86
|
+
trace.push(settled.value.result);
|
|
87
|
+
} else {
|
|
88
|
+
errors.push(settled.reason?.message ?? 'Unknown error');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check if we got any results
|
|
93
|
+
if (successfulResults.length === 0) {
|
|
94
|
+
return {
|
|
95
|
+
content: 'All agents failed to produce results.',
|
|
96
|
+
agentIds: this.agents.map((a) => a.id),
|
|
97
|
+
trace,
|
|
98
|
+
durationMs: Date.now() - startTime,
|
|
99
|
+
success: false,
|
|
100
|
+
error: errors.join('; '),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Merge results into a single response
|
|
105
|
+
const mergedContent = this.mergeResults(successfulResults);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
content: mergedContent,
|
|
109
|
+
agentIds: successfulResults.map((r) => r.agent.id),
|
|
110
|
+
trace,
|
|
111
|
+
durationMs: Date.now() - startTime,
|
|
112
|
+
success: errors.length === 0,
|
|
113
|
+
error: errors.length > 0 ? `Partial failures: ${errors.join('; ')}` : undefined,
|
|
114
|
+
};
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return {
|
|
117
|
+
content: '',
|
|
118
|
+
agentIds: [],
|
|
119
|
+
trace,
|
|
120
|
+
durationMs: Date.now() - startTime,
|
|
121
|
+
success: false,
|
|
122
|
+
error: (err as Error).message,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Merge results from all agents into a single coherent response.
|
|
129
|
+
*/
|
|
130
|
+
private mergeResults(
|
|
131
|
+
results: Array<{ agent: AgentConfig; result: AgentMessage }>,
|
|
132
|
+
): string {
|
|
133
|
+
if (results.length === 1) {
|
|
134
|
+
return results[0]!.result.content;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const sections = results.map(({ agent, result }) => {
|
|
138
|
+
return `### ${agent.name}\n${result.content}`;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return `## Combined Analysis\n\n${sections.join('\n\n---\n\n')}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Orchestrator — Uses a classifier to route tasks to the best agent.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes the input to determine which agent (or agents) is best suited
|
|
5
|
+
* to handle the request. Supports both single-agent routing and
|
|
6
|
+
* multi-agent fan-out.
|
|
7
|
+
*
|
|
8
|
+
* Example: Route coding questions to CodeAgent, math to CalculatorAgent, etc.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
AgentConfig,
|
|
13
|
+
AgentMessage,
|
|
14
|
+
OrchestrationResult,
|
|
15
|
+
OrchestratorOptions,
|
|
16
|
+
RouterClassifier,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
export class RouterOrchestrator {
|
|
20
|
+
private agents: AgentConfig[];
|
|
21
|
+
private classifier: RouterClassifier;
|
|
22
|
+
private options: OrchestratorOptions;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
agents: AgentConfig[],
|
|
26
|
+
classifier: RouterClassifier,
|
|
27
|
+
options: OrchestratorOptions = {},
|
|
28
|
+
) {
|
|
29
|
+
if (agents.length === 0) {
|
|
30
|
+
throw new Error('RouterOrchestrator requires at least one agent');
|
|
31
|
+
}
|
|
32
|
+
this.agents = agents;
|
|
33
|
+
this.classifier = classifier;
|
|
34
|
+
this.options = {
|
|
35
|
+
timeoutMs: 60_000,
|
|
36
|
+
maxRetries: 2,
|
|
37
|
+
...options,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Route the input to the best agent(s) and execute.
|
|
43
|
+
* If multiple agents are selected by the classifier, they run in parallel.
|
|
44
|
+
*/
|
|
45
|
+
async run(input: string): Promise<OrchestrationResult> {
|
|
46
|
+
const startTime = Date.now();
|
|
47
|
+
const trace: AgentMessage[] = [];
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Step 1: Classify the input to select the best agent(s)
|
|
51
|
+
const selected = await this.classifier(input, this.agents);
|
|
52
|
+
const selectedAgents = Array.isArray(selected) ? selected : [selected];
|
|
53
|
+
|
|
54
|
+
if (selectedAgents.length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
content: 'No suitable agent found for this request.',
|
|
57
|
+
agentIds: [],
|
|
58
|
+
trace,
|
|
59
|
+
durationMs: Date.now() - startTime,
|
|
60
|
+
success: false,
|
|
61
|
+
error: 'Classifier returned no agents',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Step 2: Execute selected agent(s)
|
|
66
|
+
const inputMessage: AgentMessage = {
|
|
67
|
+
content: input,
|
|
68
|
+
fromAgent: 'user',
|
|
69
|
+
timestamp: Date.now(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (selectedAgents.length === 1) {
|
|
73
|
+
// Single agent — execute directly
|
|
74
|
+
const agent = selectedAgents[0]!;
|
|
75
|
+
const result = await this.executeWithRetries(agent, inputMessage);
|
|
76
|
+
trace.push(inputMessage);
|
|
77
|
+
trace.push(result);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
content: result.content,
|
|
81
|
+
agentIds: [agent.id],
|
|
82
|
+
trace,
|
|
83
|
+
durationMs: Date.now() - startTime,
|
|
84
|
+
success: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Multiple agents — execute in parallel
|
|
89
|
+
const results = await Promise.allSettled(
|
|
90
|
+
selectedAgents.map(async (agent) => {
|
|
91
|
+
const result = await this.executeWithRetries(agent, inputMessage);
|
|
92
|
+
this.options.onAgentMessage?.(result);
|
|
93
|
+
return { agent, result };
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const successfulResults: Array<{ agent: AgentConfig; result: AgentMessage }> = [];
|
|
98
|
+
for (const settled of results) {
|
|
99
|
+
if (settled.status === 'fulfilled') {
|
|
100
|
+
successfulResults.push(settled.value);
|
|
101
|
+
trace.push(settled.value.result);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (successfulResults.length === 0) {
|
|
106
|
+
return {
|
|
107
|
+
content: 'All selected agents failed.',
|
|
108
|
+
agentIds: selectedAgents.map((a) => a.id),
|
|
109
|
+
trace,
|
|
110
|
+
durationMs: Date.now() - startTime,
|
|
111
|
+
success: false,
|
|
112
|
+
error: 'All agents failed',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Merge multiple results
|
|
117
|
+
const merged =
|
|
118
|
+
successfulResults.length === 1
|
|
119
|
+
? successfulResults[0]!.result.content
|
|
120
|
+
: successfulResults
|
|
121
|
+
.map(({ agent, result }) => `**${agent.name}**: ${result.content}`)
|
|
122
|
+
.join('\n\n');
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
content: merged,
|
|
126
|
+
agentIds: successfulResults.map((r) => r.agent.id),
|
|
127
|
+
trace,
|
|
128
|
+
durationMs: Date.now() - startTime,
|
|
129
|
+
success: true,
|
|
130
|
+
};
|
|
131
|
+
} catch (err) {
|
|
132
|
+
return {
|
|
133
|
+
content: '',
|
|
134
|
+
agentIds: [],
|
|
135
|
+
trace,
|
|
136
|
+
durationMs: Date.now() - startTime,
|
|
137
|
+
success: false,
|
|
138
|
+
error: (err as Error).message,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Execute an agent with retry logic */
|
|
144
|
+
private async executeWithRetries(
|
|
145
|
+
agent: AgentConfig,
|
|
146
|
+
input: AgentMessage,
|
|
147
|
+
): Promise<AgentMessage> {
|
|
148
|
+
const maxRetries = this.options.maxRetries ?? 2;
|
|
149
|
+
let lastError: Error | undefined;
|
|
150
|
+
|
|
151
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
152
|
+
try {
|
|
153
|
+
return await agent.execute(input, agent);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
lastError = err as Error;
|
|
156
|
+
if (attempt < maxRetries) {
|
|
157
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
throw lastError ?? new Error(`Agent "${agent.name}" failed`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Built-in keyword-based classifier for simple routing.
|
|
168
|
+
* Matches input keywords to agent descriptions and names.
|
|
169
|
+
*/
|
|
170
|
+
export function keywordClassifier(): RouterClassifier {
|
|
171
|
+
return async (input: string, agents: AgentConfig[]) => {
|
|
172
|
+
const inputLower = input.toLowerCase();
|
|
173
|
+
const scored = agents.map((agent) => {
|
|
174
|
+
const descLower = agent.description.toLowerCase();
|
|
175
|
+
const nameLower = agent.name.toLowerCase();
|
|
176
|
+
const keywords = [...descLower.split(/\s+/), ...nameLower.split(/\s+/)];
|
|
177
|
+
|
|
178
|
+
let score = 0;
|
|
179
|
+
for (const keyword of keywords) {
|
|
180
|
+
if (keyword.length < 3) continue;
|
|
181
|
+
if (inputLower.includes(keyword)) {
|
|
182
|
+
score += 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { agent, score };
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
scored.sort((a, b) => b.score - a.score);
|
|
189
|
+
|
|
190
|
+
// Return top agent if it scored above threshold
|
|
191
|
+
const best = scored[0];
|
|
192
|
+
if (best && best.score > 0) {
|
|
193
|
+
return best.agent;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Fallback to first agent
|
|
197
|
+
return agents[0]!;
|
|
198
|
+
};
|
|
199
|
+
}
|